Add excelize oracle cross-check for the hand-authored formula corpus
calc/testdata/corpus/formulas.json was hand-authored (no soffice on any machine that has touched this repo), so TestAgainstReferenceCorpus only proved self-consistency with its own author's reasoning, not agreement with an independent implementation. Adds internal/sheets/xlsx/oracle_test.go (+ oracle_corpus_test.go, oracle_excelize_test.go, oracle_divergences_test.go), which drives every non-volatile corpus entry through excelize's own CalcCellValue and requires it to agree with calc.Eval's live answer, unless the exact disagreement is a reasoned, re-checked entry in oracleDivergences. Lives in xlsx (not calc) because excelize_confinement_test.go confines excelize imports to that tree even for test files, and xlsx already bridges excelize and calc for the same reason (import.go/export.go). Result: 60 entries, 59 agree, 1 reviewed divergence (excelize v2.11.0's TRIM is strings.TrimSpace only, contradicting its own doc comment and real Excel's documented interior-whitespace-collapsing behavior — ours is correct), 0 unavailable. No changes to internal/sheets/calc.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APtNStXDZ5H2fAc1ZuGqtS
4 files changed,
+506
−0
internal/sheets/xlsx/oracle_corpus_test.go+91 −0internal/sheets/xlsx/oracle_divergences_test.go+62 −0internal/sheets/xlsx/oracle_excelize_test.go+123 −0internal/sheets/xlsx/oracle_test.go+230 −0
Intent
diff --git a/internal/sheets/xlsx/oracle_corpus_test.go b/internal/sheets/xlsx/oracle_corpus_test.go| new file mode 100644 |
| index 0000000..a3cfcc0 |
| --- /dev/null |
| +++ b/internal/sheets/xlsx/oracle_corpus_test.go |
| @@ -0,0 +1,91 @@ |
| +package xlsx_test |
| + |
| +import ( |
| + "encoding/json" |
| + "os" |
| + "path/filepath" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +// oracleCorpusPath and oracleExclusionsPath point at the SAME committed |
| +// files internal/sheets/calc/corpus_test.go reads (corpusPath, |
| +// exclusionsPath there) — one corpus, cross-checked from two packages. |
| +// This package cannot import calc's _test.go types (Go test files are |
| +// not part of a package's importable surface), so the JSON shape is |
| +// duplicated here rather than shared; keep it in sync with corpus_test.go |
| +// if formulas.json's wire format ever changes. |
| +const ( |
| + oracleCorpusPath = "../calc/testdata/corpus/formulas.json" |
| + oracleExclusionsPath = "../calc/testdata/corpus/volatile-excluded.txt" |
| +) |
| + |
| +// oracleCorpusValue mirrors calc's corpusValue (corpus_test.go): only |
| +// the field named by Kind is meaningful. |
| +type oracleCorpusValue struct { |
| + Kind string `json:"kind"` |
| + Num float64 `json:"num,omitempty"` |
| + Str string `json:"str,omitempty"` |
| + Bool bool `json:"bool,omitempty"` |
| + Err string `json:"err,omitempty"` |
| +} |
| + |
| +// oracleCorpusEntry mirrors calc's corpusEntry. Want is carried through |
| +// for context in failure/log messages, but this harness's actual |
| +// comparison is our engine's live Eval output against excelize's live |
| +// CalcCellValue output — Want plays no part in that comparison, unlike |
| +// TestAgainstReferenceCorpus in the calc package, which checks Eval |
| +// against Want directly. |
| +type oracleCorpusEntry struct { |
| + Function string `json:"function"` |
| + Expr string `json:"expr"` |
| + Want oracleCorpusValue `json:"want"` |
| +} |
| + |
| +type oracleCorpusFile struct { |
| + Note string `json:"note"` |
| + Entries []oracleCorpusEntry `json:"entries"` |
| +} |
| + |
| +// loadOracleCorpus reads and parses the same formulas.json |
| +// TestAgainstReferenceCorpus (calc package) uses. |
| +func loadOracleCorpus(t *testing.T) []oracleCorpusEntry { |
| + t.Helper() |
| + data, err := os.ReadFile(filepath.FromSlash(oracleCorpusPath)) |
| + if err != nil { |
| + t.Fatalf("loadOracleCorpus: %v", err) |
| + } |
| + var cf oracleCorpusFile |
| + if err := json.Unmarshal(data, &cf); err != nil { |
| + t.Fatalf("loadOracleCorpus: %v", err) |
| + } |
| + if len(cf.Entries) == 0 { |
| + t.Fatal("loadOracleCorpus: corpus has no entries") |
| + } |
| + return cf.Entries |
| +} |
| + |
| +// loadOracleExclusions parses the same volatile-excluded.txt |
| +// TestAgainstReferenceCorpus honors, for the same reason: NOW/TODAY |
| +// return a different answer on every evaluation (calc's env.Now() is |
| +// pinned per-pass to a fixed instant; excelize's NOW()/TODAY() read the |
| +// real wall clock), so no captured pair of answers from the two engines |
| +// can ever be expected to agree, independent of whether either engine |
| +// is "right." Skipping here mirrors calc's own reasoning, it does not |
| +// invent a new one. |
| +func loadOracleExclusions(t *testing.T) map[string]bool { |
| + t.Helper() |
| + data, err := os.ReadFile(filepath.FromSlash(oracleExclusionsPath)) |
| + if err != nil { |
| + t.Fatalf("loadOracleExclusions: %v", err) |
| + } |
| + out := map[string]bool{} |
| + for _, line := range strings.Split(string(data), "\n") { |
| + line = strings.TrimSpace(line) |
| + if line == "" || strings.HasPrefix(line, "#") { |
| + continue |
| + } |
| + out[line] = true |
| + } |
| + return out |
| +} |
diff --git a/internal/sheets/xlsx/oracle_divergences_test.go b/internal/sheets/xlsx/oracle_divergences_test.go| new file mode 100644 |
| index 0000000..b789923 |
| --- /dev/null |
| +++ b/internal/sheets/xlsx/oracle_divergences_test.go |
| @@ -0,0 +1,62 @@ |
| +package xlsx_test |
| + |
| +// oracleDivergence records one reviewed disagreement between this |
| +// package's own formula engine (internal/sheets/calc) and excelize's |
| +// independent one, for one exact corpus expression. This is NOT a |
| +// filter — TestAgainstExcelizeOracle (oracle_test.go) does not consult |
| +// this list to decide whether to run a comparison, only to decide |
| +// whether an already-found disagreement is one a human has already |
| +// looked at. A new disagreement not listed here fails the test; nothing |
| +// here can silently widen to cover one nobody has actually reviewed. |
| +// |
| +// OurAnswer and OracleAnswer are the exact strings the two engines |
| +// produced at the time this entry was written (calc's Value rendered by |
| +// renderCalcValue, excelize's raw CalcCellValue string) — the test |
| +// re-checks both engines' CURRENT answers against these on every run |
| +// (see the "stale divergence record" branch in TestAgainstExcelizeOracle) |
| +// so a later change to either engine that resolves — or changes the |
| +// shape of — a listed divergence cannot pass silently: either the |
| +// disagreement is gone (fix the entry by deleting it) or it has |
| +// changed shape (fix the entry to match, and re-review Note). |
| +type oracleDivergence struct { |
| + Expr string |
| + OurAnswer string |
| + OracleAnswer string |
| + Note string // one line: which engine we believe, and why |
| +} |
| + |
| +// oracleDivergences is the full, currently-reviewed list. As of this |
| +// harness's first run against all 60 corpus entries (30-ish functions), |
| +// exactly one disagreement was found — see the entry below for the |
| +// investigation. That is a real finding, not a placeholder: 59 of 60 |
| +// entries have excelize, an entirely independent formula engine, |
| +// agreeing with ours. |
| +var oracleDivergences = []oracleDivergence{ |
| + { |
| + Expr: `TRIM(" a b ")`, |
| + OurAnswer: "a b", |
| + OracleAnswer: "a b", |
| + // Investigated 2026-08-30: real Excel's TRIM "removes all spaces |
| + // from text except for single spaces between words" (Microsoft's |
| + // own documented behavior) -- i.e. it collapses interior runs of |
| + // whitespace to one space, not just leading/trailing. Our fnTrim |
| + // (fn_text.go) implements exactly that via strings.Fields, which |
| + // is what produces "a b" here. |
| + // |
| + // excelize v2.11.0's TRIM (calc.go, the TRIM function) is |
| + // `newStringFormulaArg(strings.TrimSpace(...))` -- leading/ |
| + // trailing only, no interior collapse -- even though its OWN doc |
| + // comment two lines above claims the correct behavior ("removes |
| + // extra spaces ... except for single spaces between words"). Read |
| + // directly from |
| + // $(go env GOMODCACHE)/github.com/xuri/excelize/v2@v2.11.0/calc.go. |
| + // This is excelize's implementation contradicting its own |
| + // documented contract, not a legitimate alternate reading of |
| + // TRIM's semantics. |
| + // |
| + // We believe OUR engine here: it matches real Excel's documented |
| + // and observable behavior; excelize's TRIM does not match its own |
| + // doc comment, let alone Excel's. |
| + Note: "excelize v2.11.0's TRIM is strings.TrimSpace only (does not collapse interior whitespace runs), contradicting both its own doc comment and real Excel's documented TRIM behavior. Ours is correct.", |
| + }, |
| +} |
diff --git a/internal/sheets/xlsx/oracle_excelize_test.go b/internal/sheets/xlsx/oracle_excelize_test.go| new file mode 100644 |
| index 0000000..fd2a789 |
| --- /dev/null |
| +++ b/internal/sheets/xlsx/oracle_excelize_test.go |
| @@ -0,0 +1,123 @@ |
| +package xlsx_test |
| + |
| +import ( |
| + "strings" |
| + "testing" |
| + |
| + "github.com/xuri/excelize/v2" |
| +) |
| + |
| +// oracleCell is the same fixed lookup table calc's corpusEnv |
| +// (corpus_test.go) sets up — A1:A3 hold 10/20/30, B1:B3 their English |
| +// names, C1 deliberately left unset — because VLOOKUP/INDEX/MATCH corpus |
| +// entries reference it by address. Anything a corpus entry needs beyond |
| +// this small table is out of scope for this harness (matching the |
| +// current corpus's own scope: every entry is either self-contained or |
| +// reads only this table). |
| +func newOracleWorkbook() (f *excelize.File, sheet string) { |
| + f = excelize.NewFile() |
| + sheet = f.GetSheetList()[0] |
| + must(f.SetCellFloat(sheet, "A1", 10, -1, 64)) |
| + must(f.SetCellStr(sheet, "B1", "ten")) |
| + must(f.SetCellFloat(sheet, "A2", 20, -1, 64)) |
| + must(f.SetCellStr(sheet, "B2", "twenty")) |
| + must(f.SetCellFloat(sheet, "A3", 30, -1, 64)) |
| + must(f.SetCellStr(sheet, "B3", "thirty")) |
| + // C1 intentionally unset. |
| + return f, sheet |
| +} |
| + |
| +// oracleFormulaCell is where each corpus expression's formula is |
| +// written — outside A1:C3 so it can never collide with the support |
| +// table above. |
| +const oracleFormulaCell = "Z1" |
| + |
| +// oracleKnownErrorTokens are the spreadsheet error strings excelize (and |
| +// our own ErrKind.String, errors.go) can produce as a genuine cell |
| +// value, as opposed to excelize's own diagnostic text for a formula it |
| +// could not evaluate at all. "#CIRCULAR!" is deliberately absent: it is |
| +// this app's own invented token (errors.go's ErrKind.String), not a |
| +// real Excel error string, and never appears in excelize output. |
| +var oracleKnownErrorTokens = map[string]bool{ |
| + "#DIV/0!": true, "#VALUE!": true, "#REF!": true, "#NAME?": true, |
| + "#NUM!": true, "#N/A": true, "#NULL!": true, "#SPILL!": true, |
| + "#CALC!": true, "#GETTING_DATA": true, |
| +} |
| + |
| +// oracleAnswer is one corpus expression's outcome from excelize: |
| +// Available is false when excelize could not produce a real spreadsheet |
| +// answer at all (an unimplemented function, a formula it refuses to |
| +// parse) — see classifyOracleResult's doc comment for the full |
| +// reasoning. Value is only meaningful when Available is true. |
| +type oracleAnswer struct { |
| + Available bool |
| + Value string |
| + Reason string // why Available is false, or empty |
| +} |
| + |
| +// computeOracleAnswer writes expr into a fresh excelize workbook (with |
| +// the same support table calc's corpusEnv provides) and asks excelize to |
| +// evaluate it via CalcCellValue — the independent-implementation half of |
| +// this harness's cross-check. See classifyOracleResult for how its |
| +// (value, error) pair is turned into an oracleAnswer. |
| +func computeOracleAnswer(t *testing.T, expr string) oracleAnswer { |
| + t.Helper() |
| + f, sheet := newOracleWorkbook() |
| + defer func() { |
| + if err := f.Close(); err != nil { |
| + t.Errorf("close oracle workbook for %q: %v", expr, err) |
| + } |
| + }() |
| + |
| + if err := f.SetCellFormula(sheet, oracleFormulaCell, "="+expr); err != nil { |
| + return oracleAnswer{Available: false, Reason: "excelize rejected the formula: " + err.Error()} |
| + } |
| + val, err := f.CalcCellValue(sheet, oracleFormulaCell) |
| + return classifyOracleResult(val, err) |
| +} |
| + |
| +// classifyOracleResult turns CalcCellValue's (value, error) pair into an |
| +// oracleAnswer. excelize is NOT consistent about what it returns for a |
| +// formula error (verified against v2.11.0 directly, not assumed): |
| +// |
| +// - A real spreadsheet error the formula itself produces (division by |
| +// zero, VLOOKUP with no match, ...) comes back as BOTH a non-nil err |
| +// AND a val that IS one of the standard error tokens (e.g. val="#N/A", |
| +// err="VLOOKUP no result found" — err's own text is not the token). |
| +// That is a genuine answer, not an unavailable oracle: val is used. |
| +// - A function excelize has not implemented ALSO comes back with a val |
| +// that happens to look like a token (val="#VALUE!") but err's text |
| +// contains "not support" — checked first, specifically so this case |
| +// is never mistaken for the first one just because val looks legit. |
| +// - A formula excelize's parser rejects outright comes back with |
| +// val="" and err="formula not valid". |
| +// |
| +// Anything else (an err whose text is not a recognized shape) is treated |
| +// as unavailable too, conservatively: better to under-claim oracle |
| +// coverage than to silently accept a value from a state this harness |
| +// does not actually understand. |
| +func classifyOracleResult(val string, err error) oracleAnswer { |
| + if err == nil { |
| + return oracleAnswer{Available: true, Value: val} |
| + } |
| + msg := err.Error() |
| + if strings.Contains(msg, "not support") { |
| + return oracleAnswer{Available: false, Reason: "unimplemented in excelize: " + msg} |
| + } |
| + if msg == "formula not valid" { |
| + return oracleAnswer{Available: false, Reason: "excelize could not parse the formula: " + msg} |
| + } |
| + if oracleKnownErrorTokens[msg] { |
| + return oracleAnswer{Available: true, Value: msg} |
| + } |
| + if oracleKnownErrorTokens[val] { |
| + return oracleAnswer{Available: true, Value: val} |
| + } |
| + return oracleAnswer{Available: false, Reason: "unrecognized excelize error: " + msg} |
| +} |
| + |
| +func must(err error) { |
| + if err != nil { |
| + panic(err) |
| + } |
| +} |
diff --git a/internal/sheets/xlsx/oracle_test.go b/internal/sheets/xlsx/oracle_test.go| new file mode 100644 |
| index 0000000..e3cd739 |
| --- /dev/null |
| +++ b/internal/sheets/xlsx/oracle_test.go |
| @@ -0,0 +1,230 @@ |
| +// Cross-checking against excelize — why this lives here, not in calc. |
| +// |
| +// calc/testdata/corpus/formulas.json (internal/sheets/calc) was meant to |
| +// be generated by driving a real LibreOffice headless, so the formula |
| +// engine is checked against an independent implementation rather than |
| +// its own author's reasoning. No machine that has touched this repo has |
| +// had soffice on PATH, so the corpus was hand-authored instead — see |
| +// loadCorpus's doc comment in calc/corpus_test.go. That corpus proves |
| +// self-consistency, not conformance: it shipped, for a while, with |
| +// COUNT(1,2,"a",TRUE) expecting 2 (a wrong implementation's answer) until |
| +// a human reviewer noticed real spreadsheets return 3 — a mistake the |
| +// corpus, checking only itself, could not have caught. |
| +// |
| +// excelize (github.com/xuri/excelize/v2, already in go.mod for XLSX |
| +// import/export) ships its own formula engine, CalcCellValue — a |
| +// different codebase by different authors, independent of ours by |
| +// construction. TestAgainstExcelizeOracle below drives it against every |
| +// non-volatile corpus entry and cross-checks its answer against calc's |
| +// live Eval — not against the corpus's "want" field, which is what |
| +// TestAgainstReferenceCorpus (calc package) already checks and is |
| +// exactly the self-consistency this harness exists to get past. |
| +// |
| +// This lives in internal/sheets/xlsx, not internal/sheets/calc, for one |
| +// mechanical reason and one architectural one: |
| +// - internal/guards/excelize_confinement_test.go fails any file outside |
| +// internal/sheets/xlsx (any depth) that imports excelize, with no |
| +// exemption for _test.go files. A file in calc that imported excelize |
| +// — even test-only — would fail that guard. |
| +// - xlsx already imports both excelize and calc (import.go, export.go) |
| +// for exactly the same reason this harness needs both: it is the one |
| +// package whose whole job is bridging excelize's world and ours |
| +// (import.go's package doc: "excelize is confined to this package |
| +// deliberately ... letting any other package import it would couple |
| +// our storage to theirs"). A cross-check between the two engines is |
| +// that same bridging job, just for verification instead of import. |
| +// |
| +// A brand-new internal/oracle package was the other option considered: |
| +// rejected because it would need excelizeHome widened in the guard to |
| +// cover it, for no benefit over reusing the tree that already exists for |
| +// exactly this pairing of dependencies. |
| +package xlsx_test |
| + |
| +import ( |
| + "math" |
| + "strconv" |
| + "testing" |
| + "time" |
| + |
| + "amadan.net/oficina/sheets/internal/sheets/calc" |
| +) |
| + |
| +// oracleEnv is calc.Env backed by the identical fixed lookup table |
| +// computeOracleAnswer's excelize workbook sets up (oracle_excelize_test.go): |
| +// A1:A3 hold 10/20/30, B1:B3 their English names, C1 deliberately unset. |
| +// Now is pinned rather than wired to the real clock because NOW/TODAY are |
| +// excluded from this comparison anyway (loadOracleExclusions) — nothing |
| +// here ever reads it for a real answer, but calc.Env requires it. |
| +type oracleEnv map[calc.CellKey]calc.Value |
| + |
| +func (e oracleEnv) CellValue(k calc.CellKey) calc.Value { return e[k] } |
| +func (e oracleEnv) Now() time.Time { return time.Date(2026, 8, 30, 12, 0, 0, 0, time.UTC) } |
| + |
| +var oracleCellEnv = oracleEnv{ |
| + {RowID: 1, ColID: 1}: {Kind: calc.KindNum, Num: 10}, |
| + {RowID: 1, ColID: 2}: {Kind: calc.KindStr, Str: "ten"}, |
| + {RowID: 2, ColID: 1}: {Kind: calc.KindNum, Num: 20}, |
| + {RowID: 2, ColID: 2}: {Kind: calc.KindStr, Str: "twenty"}, |
| + {RowID: 3, ColID: 1}: {Kind: calc.KindNum, Num: 30}, |
| + {RowID: 3, ColID: 2}: {Kind: calc.KindStr, Str: "thirty"}, |
| +} |
| + |
| +// renderCalcValue renders a calc.Value the same way an excelize |
| +// CalcCellValue answer would read as text, so the two sides of the |
| +// comparison — and oracleDivergences' recorded strings — are directly |
| +// comparable and directly printable. |
| +func renderCalcValue(v calc.Value) string { |
| + switch v.Kind { |
| + case calc.KindNum: |
| + return strconv.FormatFloat(v.Num, 'g', -1, 64) |
| + case calc.KindStr: |
| + return v.Str |
| + case calc.KindBool: |
| + if v.Bool { |
| + return "TRUE" |
| + } |
| + return "FALSE" |
| + case calc.KindErr: |
| + return v.Err.String() |
| + default: // calc.KindEmpty |
| + return "" |
| + } |
| +} |
| + |
| +// agreesWithOracle compares our engine's Value against excelize's raw |
| +// answer string. Numeric comparison tolerates a small epsilon — matching |
| +// calc's own corpus_test.go valuesMatch — because excelize's |
| +// CalcCellValue renders floats to fewer significant digits than Go's |
| +// full round-trip precision (observed directly: SQRT(2) comes back as |
| +// "1.4142135623731", 13 significant digits, not |
| +// "1.4142135623730951"), which is a rendering difference, not a |
| +// disagreement about the value. |
| +func agreesWithOracle(our calc.Value, oracleVal string) bool { |
| + switch our.Kind { |
| + case calc.KindNum: |
| + f, err := strconv.ParseFloat(oracleVal, 64) |
| + if err != nil { |
| + return false |
| + } |
| + return f == our.Num || math.Abs(f-our.Num) < 1e-9 |
| + case calc.KindBool: |
| + want := "FALSE" |
| + if our.Bool { |
| + want = "TRUE" |
| + } |
| + return oracleVal == want |
| + case calc.KindErr: |
| + return oracleVal == our.Err.String() |
| + case calc.KindStr: |
| + return oracleVal == our.Str |
| + default: // calc.KindEmpty |
| + return oracleVal == "" |
| + } |
| +} |
| + |
| +// mustParseOracle parses expr with calc.Parse, failing the test on a |
| +// parse error — every corpus expression is already known-parseable |
| +// (TestAgainstReferenceCorpus parses the same file), so a failure here |
| +// means the corpus and this harness have drifted apart. |
| +func mustParseOracle(t *testing.T, expr string) calc.Node { |
| + t.Helper() |
| + n, err := calc.Parse(expr) |
| + if err != nil { |
| + t.Fatalf("mustParseOracle(%q): %v", expr, err) |
| + } |
| + return n |
| +} |
| + |
| +// TestAgainstExcelizeOracle is the cross-check itself. For every |
| +// non-volatile corpus entry it evaluates the SAME expression with calc's |
| +// own engine and with excelize's, and requires them to agree UNLESS the |
| +// exact disagreement is already reviewed in oracleDivergences. |
| +// |
| +// Three outcomes per entry, all counted and none silently dropped: |
| +// - agree: the common case. |
| +// - known divergence: logged with its reasoned note, not a failure — |
| +// but the recorded strings are checked against what both engines |
| +// currently say, so the record cannot rot out from under a later |
| +// change to either engine. |
| +// - unavailable: excelize could not produce a real answer at all |
| +// (unimplemented function, parse rejection) — logged explicitly as |
| +// its own category, never merged into "agree" or silently skipped. |
| +// |
| +// Anything else — a disagreement with no matching entry in |
| +// oracleDivergences — fails the test outright. |
| +func TestAgainstExcelizeOracle(t *testing.T) { |
| + entries := loadOracleCorpus(t) |
| + excluded := loadOracleExclusions(t) |
| + known := indexOracleDivergences(t) |
| + |
| + var agree, unavailable, knownDivergent, compared int |
| + for _, e := range entries { |
| + if excluded[e.Function] { |
| + continue |
| + } |
| + compared++ |
| + |
| + ourVal := calc.Eval(mustParseOracle(t, e.Expr), oracleCellEnv) |
| + ourStr := renderCalcValue(ourVal) |
| + |
| + oa := computeOracleAnswer(t, e.Expr) |
| + if !oa.Available { |
| + unavailable++ |
| + t.Logf("ORACLE UNAVAILABLE: %s %q: %s", e.Function, e.Expr, oa.Reason) |
| + continue |
| + } |
| + |
| + if agreesWithOracle(ourVal, oa.Value) { |
| + agree++ |
| + continue |
| + } |
| + |
| + d, isKnown := known[e.Expr] |
| + if !isKnown { |
| + t.Errorf("NEW DISAGREEMENT: %s %q: ours=%q excelize=%q — not in oracleDivergences (oracle_divergences_test.go); investigate before adding it there, do not assume we are wrong", e.Function, e.Expr, ourStr, oa.Value) |
| + continue |
| + } |
| + if d.OurAnswer != ourStr || d.OracleAnswer != oa.Value { |
| + t.Errorf("STALE DIVERGENCE RECORD: %s %q: recorded ours=%q/excelize=%q, now ours=%q/excelize=%q — update oracle_divergences_test.go (either the disagreement is gone, or it changed shape and needs re-review)", e.Function, e.Expr, d.OurAnswer, d.OracleAnswer, ourStr, oa.Value) |
| + continue |
| + } |
| + knownDivergent++ |
| + t.Logf("KNOWN DIVERGENCE: %s %q: ours=%q excelize=%q — %s", e.Function, e.Expr, ourStr, oa.Value, d.Note) |
| + } |
| + |
| + t.Logf("oracle cross-check: %d entries compared (%d volatile excluded), agree=%d known-divergence=%d unavailable=%d", |
| + compared, len(entries)-compared, agree, knownDivergent, unavailable) |
| +} |
| + |
| +// indexOracleDivergences keys oracleDivergences by Expr and fails fast |
| +// on a duplicate key — two records for the same expression is itself a |
| +// list-rot bug (which one is authoritative?), not a valid state. |
| +func indexOracleDivergences(t *testing.T) map[string]oracleDivergence { |
| + t.Helper() |
| + out := make(map[string]oracleDivergence, len(oracleDivergences)) |
| + for _, d := range oracleDivergences { |
| + if _, dup := out[d.Expr]; dup { |
| + t.Fatalf("oracleDivergences: duplicate entry for expr %q", d.Expr) |
| + } |
| + out[d.Expr] = d |
| + } |
| + return out |
| +} |
| + |
| +// TestOracleDivergencesMatchCorpus fails on an oracleDivergences entry |
| +// whose Expr does not match any current corpus entry — an orphaned |
| +// divergence record (the corpus changed under it, or it was mistyped) is |
| +// the same kind of silent rot TestExclusionsAreRealFunctions |
| +// (calc/corpus_test.go) guards against for the exclusion list. |
| +func TestOracleDivergencesMatchCorpus(t *testing.T) { |
| + entries := loadOracleCorpus(t) |
| + exprs := make(map[string]bool, len(entries)) |
| + for _, e := range entries { |
| + exprs[e.Expr] = true |
| + } |
| + for _, d := range oracleDivergences { |
| + if !exprs[d.Expr] { |
| + t.Errorf("oracleDivergences: %q does not match any corpus entry — stale record", d.Expr) |
| + } |
| + } |
| +} |