Files
thptqg/parser/internal/config/config_test.go
T
tiennm99 933adf70c9 refactor: comments state current behavior, not project history
Comments across the tree justified the code by pointing at a Rust
implementation that is no longer in the repository, citing files and line
numbers (config.rs:132, schema.rs:26-54, reader.rs:42) that cannot be
opened, plus crates and datasets that are equally gone. A reader could not
check any of it.

Every invariant those comments carried is kept and restated so it stands on
its own: the bytewise sort that decides which row survives a duplicate exam
number, the literal U+0300..U+036F range that must match the site's toAscii,
the trailing space in "SINH ", the BIFF and shared-string corrections, the
VACUUM-after-COMMIT rule, the deploy-from-main guard.

The reader's contract is now anchored to the frozen oracle in
parser/testdata, which still exists and is still checked, rather than to the
tool that originally produced it.

TestDDLMatchesRust becomes TestDDLIsFrozen: it compares against a copy of
the DDL inside the test and never read schema.rs, so both the name and the
failure message were misleading.

ToAscii no longer claims the d-replacement must precede lowercasing. Both
cases map to 'd' and ToLower runs last, so the order has no effect.
2026-08-14 09:40:41 +07:00

220 lines
5.9 KiB
Go

package config
import (
"os"
"path/filepath"
"testing"
)
// sampleYAML is a fixed-column (2017-style) config with every section populated.
const sampleYAML = `
reader:
sheet_mode: all
strip_blank_rows: false
columns:
ho_ten: 0
ngay_sinh: 1
so_bao_danh: 2
diem_thi: 3
validation:
require_numeric_sbd: false
require_nonempty_name: true
require_nonempty_sbd: true
header:
tokens: ["HO_TEN", "HỌ TÊN", "STT"]
`
func TestConfigRoundTrip(t *testing.T) {
cfg, err := Parse([]byte(sampleYAML))
if err != nil {
t.Fatalf("parse failed: %v", err)
}
if cfg.Reader.SheetMode != SheetModeAll {
t.Errorf("sheet_mode = %q, want all", cfg.Reader.SheetMode)
}
if cfg.Reader.StripBlankRows {
t.Error("strip_blank_rows should be false")
}
if cfg.Columns == nil {
t.Fatal("columns should be present")
}
if cfg.Columns.HoTen != 0 {
t.Errorf("ho_ten = %d, want 0", cfg.Columns.HoTen)
}
if cfg.Columns.DiemThi != 3 {
t.Errorf("diem_thi = %d, want 3", cfg.Columns.DiemThi)
}
if cfg.Validation.RequireNumericSbd {
t.Error("require_numeric_sbd should be false")
}
if !cfg.Validation.RequireNonemptyName {
t.Error("require_nonempty_name should be true")
}
if len(cfg.Header.Tokens) != 3 {
t.Errorf("tokens = %d, want 3", len(cfg.Header.Tokens))
}
if cfg.FormatDetection != nil {
t.Errorf("format_detection = %v, want nil", *cfg.FormatDetection)
}
}
// TestConfigRejectsLeftoverSQLSections is the load-bearing one: YAML decoders
// ignore unknown keys unless KnownFields(true) is set, and without it a stale
// schema: mapping would look effective while internal/schema silently drove the
// build — exactly the drift that produced two divergent schemas.
func TestConfigRejectsLeftoverSQLSections(t *testing.T) {
withDDL := sampleYAML + "\nschema:\n ddl: \"CREATE TABLE student (so_bao_danh TEXT);\"\n"
if _, err := Parse([]byte(withDDL)); err == nil {
t.Fatal("config with a leftover schema: mapping must be rejected")
}
}
// TestConfigRejectsUnknownScalarKey guards the same property for a stray scalar,
// not just a stray mapping.
func TestConfigRejectsUnknownScalarKey(t *testing.T) {
withKey := sampleYAML + "\nunexpected_key: 1\n"
if _, err := Parse([]byte(withKey)); err == nil {
t.Fatal("config with an unknown scalar key must be rejected")
}
}
func TestConfigFirstSheetMode(t *testing.T) {
src := []byte(replaceAll(sampleYAML, "sheet_mode: all", "sheet_mode: first"))
cfg, err := Parse(src)
if err != nil {
t.Fatalf("parse failed: %v", err)
}
if cfg.Reader.SheetMode != SheetModeFirst {
t.Errorf("sheet_mode = %q, want first", cfg.Reader.SheetMode)
}
}
// TestConfigRejectsUnknownSheetMode: sheet_mode accepts only "all" or "first",
// so anything else must fail rather than defaulting.
func TestConfigRejectsUnknownSheetMode(t *testing.T) {
src := []byte(replaceAll(sampleYAML, "sheet_mode: all", "sheet_mode: second"))
if _, err := Parse(src); err == nil {
t.Fatal("unknown sheet_mode must be rejected")
}
}
// TestConfigFormatDetectionField: a 2016-style config has no columns: mapping at all.
func TestConfigFormatDetectionField(t *testing.T) {
const src = `
format_detection: thptqg2016
reader:
sheet_mode: all
strip_blank_rows: false
validation:
require_numeric_sbd: false
require_nonempty_name: true
require_nonempty_sbd: true
header:
tokens: ["SBD", "SOBAODANH", "STT"]
`
cfg, err := Parse([]byte(src))
if err != nil {
t.Fatalf("parse failed: %v", err)
}
if cfg.FormatDetection == nil || *cfg.FormatDetection != "thptqg2016" {
t.Errorf("format_detection = %v, want thptqg2016", cfg.FormatDetection)
}
if cfg.Columns != nil {
t.Error("columns must be nil when format_detection drives the layout")
}
}
// TestLoadRealConfigs loads the shipped configs and pins the per-dataset
// differences between them.
func TestLoadRealConfigs(t *testing.T) {
root := repoRoot(t)
want := map[string]struct {
sheetMode SheetMode
stripBlank bool
numericSbd bool
hasColumns bool
formatDet string
tokenCount int
}{
"2016": {SheetModeAll, false, false, false, "thptqg2016", 6},
"2017": {SheetModeAll, false, false, true, "", 3},
}
for id, w := range want {
t.Run(id, func(t *testing.T) {
cfg, err := Load(filepath.Join(root, "parser", "configs", id+".yml"))
if err != nil {
t.Fatalf("load: %v", err)
}
if cfg.Reader.SheetMode != w.sheetMode {
t.Errorf("sheet_mode = %q, want %q", cfg.Reader.SheetMode, w.sheetMode)
}
if cfg.Reader.StripBlankRows != w.stripBlank {
t.Errorf("strip_blank_rows = %v, want %v", cfg.Reader.StripBlankRows, w.stripBlank)
}
if cfg.Validation.RequireNumericSbd != w.numericSbd {
t.Errorf("require_numeric_sbd = %v, want %v", cfg.Validation.RequireNumericSbd, w.numericSbd)
}
if (cfg.Columns != nil) != w.hasColumns {
t.Errorf("columns present = %v, want %v", cfg.Columns != nil, w.hasColumns)
}
got := ""
if cfg.FormatDetection != nil {
got = *cfg.FormatDetection
}
if got != w.formatDet {
t.Errorf("format_detection = %q, want %q", got, w.formatDet)
}
if len(cfg.Header.Tokens) != w.tokenCount {
t.Errorf("tokens = %d, want %d", len(cfg.Header.Tokens), w.tokenCount)
}
// Every dataset requires a non-empty name and SBD.
if !cfg.Validation.RequireNonemptyName || !cfg.Validation.RequireNonemptySbd {
t.Error("both non-empty validations should be true for every dataset")
}
})
}
}
func replaceAll(s, old, new string) string {
out := ""
for {
i := indexOf(s, old)
if i < 0 {
return out + s
}
out += s[:i] + new
s = s[i+len(old):]
}
}
func indexOf(s, sub string) int {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return i
}
}
return -1
}
func repoRoot(t *testing.T) string {
t.Helper()
dir, err := os.Getwd()
if err != nil {
t.Fatalf("getwd: %v", err)
}
for i := 0; i < 6; i++ {
if _, err := os.Stat(filepath.Join(dir, "parser", "configs")); err == nil {
return dir
}
dir = filepath.Dir(dir)
}
t.Fatal("could not locate repo root")
return ""
}