mirror of
https://github.com/tiennm99/thptqg.git
synced 2026-10-05 00:13:44 +00:00
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.
220 lines
5.9 KiB
Go
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 ""
|
|
}
|