// Command gencorpus regenerates internal/sheets/calc/testdata/corpus/formulas.json // from a real spreadsheet. Run it when adding functions; commit the // result. // // go run ./tools/gencorpus -out internal/sheets/calc/testdata/corpus/formulas.json // // Requires LibreOffice. `soffice` is looked up on PATH by default; pass // -soffice /path/to/soffice when it is installed somewhere PATH does not // reach (a user-space install, for instance). CI does not run this — CI // reads the committed file, which is why the file is hash-pinned by // calc.TestCorpusIsPinned (internal/sheets/calc/corpus_test.go). After a // successful run, review the diff, then run // // go test ./internal/sheets/calc/ -run TestCorpusIsPinned -v // // and paste the printed sha256 into corpusSHA256. // // # Mechanism // // gencorpus writes a real .ods (OpenDocument spreadsheet) with one // formula per row in column D. Each cell carries an ODF-syntax // table:formula attribute (an "of:=" prefix, semicolon argument // separators — ODF's own formula language, not Excel's comma-separated // one), which is what makes LibreOffice treat the cell as a formula to // (re)compute, not literal text. It then shells out to // `soffice --headless --convert-to csv`, which opens the document, // recalculates, and exports the computed values as CSV — not the // formula source. Each output row is paired back up with the case that // produced it, by row order, into the corpus JSON. // // # First real run, and what it found // // This tool was written without a LibreOffice to run it against, and its // first real run (LibreOffice 26.8.0.3, 2026-08-31) was treated as its // own first test. It found three real defects, all now fixed and all // documented at the code that fixes them: // // - the mimetype zip entry was written with zip.CreateHeader, whose // data-descriptor flag makes LibreOffice refuse the whole document // ("source file could not be loaded") — see writeODS; // - content.xml did not declare xmlns:of, without which every formula // evaluates to Err:510 — see contentXML; // - soffice was invoked with no -env:UserInstallation, which can drop // it into an interactive first-run flow — see runSoffice. // // Recalculation on export is not assumed: every formula cell is written // with a poisoned office:value cache (poisonedCacheValue) that no case // can legitimately produce, and checkResults fails the run if that value // comes back. It does not — LibreOffice recalculates on convert-to. package main import ( "flag" "fmt" "os" "path/filepath" ) // corpusCase is one row to feed LibreOffice: Function names the // spreadsheet function under test (matched against // testdata/corpus/volatile-excluded.txt and used for // TestAgainstReferenceCorpus's per-row skip), Expr is the formula text as a // person would type it after "=" — Excel/English syntax, comma argument // separators; toODFFormula (ods.go) translates to ODF's own syntax for // the file this tool actually writes. type corpusCase struct { Function string Expr string } func main() { out := flag.String("out", "", "path to write the corpus JSON to") soffice := flag.String("soffice", "soffice", "path to the LibreOffice `soffice` binary") flag.Parse() if *out == "" { fmt.Fprintln(os.Stderr, "gencorpus: -out is required") os.Exit(2) } tmpDir, err := os.MkdirTemp("", "gencorpus-*") if err != nil { fatal(err) } defer os.RemoveAll(tmpDir) version, err := sofficeVersion(*soffice, tmpDir) if err != nil { fatal(fmt.Errorf("cannot run %q: %w (install LibreOffice, or pass -soffice /path/to/soffice)", *soffice, err)) } fmt.Printf("gencorpus: using %s (%s)\n", version, *soffice) // Refuse to silently narrow an existing corpus: if *out already has // more entries than this run is about to produce, something regressed // (a case dropped from the list above without a function actually // going away) and overwriting it would replace an honest "hand- // authored, unverified" corpus — or a real one — with a smaller file // that still claims full coverage. See cases' own doc comment for the // concrete incident this guards against. if existing, err := existingEntryCount(*out); err == nil && existing > len(cases) { fatal(fmt.Errorf("%s already has %d entries; this run would only produce %d — refusing to narrow the corpus (add the missing cases to gencorpus's own list, don't just let it overwrite)", *out, existing, len(cases))) } odsPath := filepath.Join(tmpDir, "corpus.ods") if err := writeODS(odsPath, cases); err != nil { fatal(err) } csvPath, err := runSoffice(*soffice, tmpDir, odsPath) if err != nil { fatal(err) } results, err := readResults(csvPath) if err != nil { fatal(err) } if err := checkResults(cases, results); err != nil { fatal(err) } if err := writeCorpusJSON(*out, cases, results, version); err != nil { fatal(err) } fmt.Printf("gencorpus: wrote %d entries to %s\n", len(cases), *out) fmt.Println("gencorpus: next: review the diff, then run `go test ./internal/sheets/calc/ -run TestCorpusIsPinned -v` and update corpusSHA256") } func fatal(err error) { fmt.Fprintln(os.Stderr, "gencorpus:", err) os.Exit(1) }