Files
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

122 lines
3.5 KiB
Go

package ingest
import (
"os"
"path/filepath"
"testing"
"github.com/tiennm99/thptqg/parser/internal/reader"
)
func cells(vals ...string) []reader.Cell {
out := make([]reader.Cell, len(vals))
for i, v := range vals {
out[i] = reader.Cell{Str: v, IsEmpty: v == ""}
}
return out
}
var stdTokens = []string{"HO_TEN", "HỌ TÊN", "STT"}
func TestHeaderDetectsHoTen(t *testing.T) {
if !IsHeaderRow(cells("HO_TEN", "NGAY_SINH", "SBD"), stdTokens) {
t.Error("HO_TEN header not detected")
}
}
func TestHeaderDetectsStt(t *testing.T) {
if !IsHeaderRow(cells("STT", "B", "C"), stdTokens) {
t.Error("STT header not detected")
}
}
func TestHeaderDetectsHoTenUnicode(t *testing.T) {
if !IsHeaderRow(cells("HỌ TÊN", "B", "C"), stdTokens) {
t.Error("HỌ TÊN header not detected")
}
}
func TestHeaderRejectsDataRow(t *testing.T) {
if IsHeaderRow(cells("Nguyen Van A", "01/01/2000", "12345678"), stdTokens) {
t.Error("data row wrongly detected as header")
}
}
// TestHeaderRejectsShortRow covers the <3-cell guard.
func TestHeaderRejectsShortRow(t *testing.T) {
if IsHeaderRow(cells("HO_TEN", ""), stdTokens) {
t.Error("a 2-cell row must never be a header, even with a matching token")
}
}
func TestHeaderCaseInsensitive(t *testing.T) {
if !IsHeaderRow(cells("ho_ten", "B", "C"), stdTokens) {
t.Error("lowercase header not detected")
}
}
// TestBlankRowDetection: the third cell is an empty *string* cell rather than an
// absent one, and must still count as blank.
func TestBlankRowDetection(t *testing.T) {
blank := []reader.Cell{{IsEmpty: true}, {IsEmpty: true}, {Str: ""}}
if !IsAllBlank(blank) {
t.Error("row of empty cells should be blank")
}
if IsAllBlank(cells("Nguyen", "", "")) {
t.Error("row with content should not be blank")
}
}
// TestIsAllBlankIgnoresIsEmptyFlag pins the rule that blankness is decided by
// the rendered string, never by Cell.IsEmpty, which is diagnostic only.
func TestIsAllBlankIgnoresIsEmptyFlag(t *testing.T) {
// Content present but IsEmpty wrongly set: still not blank.
if IsAllBlank([]reader.Cell{{Str: "x", IsEmpty: true}}) {
t.Error("a cell with content must not be blank regardless of IsEmpty")
}
// Whitespace only: blank.
if !IsAllBlank([]reader.Cell{{Str: " "}, {Str: "\t"}}) {
t.Error("whitespace-only cells should be blank")
}
}
// TestDatasetLabel covers the basename derivation that drives the stats wording.
func TestDatasetLabel(t *testing.T) {
for in, want := range map[string]string{
"data/2017": "2017",
"data/2017-old2": "2017-old2",
"data/2017-old2/": "2017-old2",
"/abs/path/2016": "2016",
} {
if got := DatasetLabel(in); got != want {
t.Errorf("DatasetLabel(%q) = %q, want %q", in, got, want)
}
}
}
// TestInputFilesSortedAndFiltered: the sort decides which duplicate SBD survives
// INSERT OR REPLACE, so it is behaviour, not presentation.
func TestInputFilesSortedAndFiltered(t *testing.T) {
dir := t.TempDir()
for _, name := range []string{"b.xlsx", "a.xls", "c.XLSX", "notes.txt", "d.csv"} {
if err := writeEmpty(filepath.Join(dir, name)); err != nil {
t.Fatal(err)
}
}
got, err := InputFiles(dir)
if err != nil {
t.Fatal(err)
}
want := []string{"a.xls", "b.xlsx", "c.XLSX"}
if len(got) != len(want) {
t.Fatalf("got %d files %v, want %d", len(got), got, len(want))
}
for i := range want {
if filepath.Base(got[i]) != want[i] {
t.Errorf("file %d = %q, want %q", i, filepath.Base(got[i]), want[i])
}
}
}
func writeEmpty(path string) error { return os.WriteFile(path, nil, 0o644) }