From 32c453d66eae17d014df91981362a4e0d5b7ad4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Wed, 7 Oct 2026 12:37:34 +0200 Subject: [PATCH] test(format): corpus invariants over GOROOT assembly Three property tests walk every .s file under the installed GOROOT plus the repository's kernels: formatting is idempotent and preserves the token stream, a clean parse keeps its tree through a format pass, and a cleanly expanding file expands to the same statements afterwards. The walks skip under -short so the push suite keeps its budget. Assisted-by: GLM 5.3 --- format/corpus_test.go | 142 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 format/corpus_test.go diff --git a/format/corpus_test.go b/format/corpus_test.go new file mode 100644 index 0000000..7c5a7b8 --- /dev/null +++ b/format/corpus_test.go @@ -0,0 +1,142 @@ +// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package format + +import ( + "io/fs" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + + "sourcedock.dev/petrbalvin/gasm-sdk/parser" +) + +// corpusSources collects every .s file the formatter is held against: the +// repository's own kernels first, then the whole of the installed GOROOT, the +// richest body of real Plan 9 assembly on the machine. +func corpusSources(t *testing.T) []string { + t.Helper() + var paths []string + for _, pattern := range []string{"../testdata/*.s", "../testdata/verify/*.s"} { + files, _ := filepath.Glob(pattern) + paths = append(paths, files...) + } + root := runtime.GOROOT() + filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if err == nil && !d.IsDir() && strings.HasSuffix(path, ".s") { + paths = append(paths, path) + } + return nil + }) + if len(paths) == 0 { + t.Fatal("no corpus sources found") + } + return paths +} + +// corpusSkip keeps the corpus walks out of the -short push suite: they need +// the machine to themselves the way the suite workflow's long tests do. +func corpusSkip(t *testing.T) { + t.Helper() + if testing.Short() { + t.Skip("the corpus belongs to the full suite; -short replays the seed corpora only") + } +} + +// TestCorpusIdempotenceAndTokens runs the formatter's two standing +// invariants over the whole corpus: formatting is idempotent +// (Source(Source(x)) == Source(x)), and the output re-lexes to the same +// tokens as the input, so formatting changes layout, never meaning. +func TestCorpusIdempotenceAndTokens(t *testing.T) { + corpusSkip(t) + var files, failures int + for _, path := range corpusSources(t) { + b, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + files++ + src := string(b) + once := Source(src) + if twice := Source(once); twice != once { + failures++ + t.Errorf("formatting %s is not idempotent", path) + } + if in, out := tokenView(src), tokenView(once); !sameView(in, out) { + failures++ + t.Errorf("formatting %s changed the token stream:\ninput: %v\noutput: %v", path, viewString(in), viewString(out)) + } + } + t.Logf("idempotence and token view checked over %d files, %d failures", files, failures) +} + +// TestCorpusParseRoundTrip runs the parser's side of the contract over the +// corpus: a file that parses cleanly still parses cleanly after formatting, +// and its tree, positions aside, is unchanged. parse(format(parse(x)) +// therefore sees the same file as parse(x). +func TestCorpusParseRoundTrip(t *testing.T) { + corpusSkip(t) + var files, clean, failures int + for _, path := range corpusSources(t) { + b, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + files++ + src := string(b) + file, errs := parser.Parse(path, src) + if len(errs) > 0 || hasStrayIllegal(src) { + continue // no clean tree to preserve; the token test covers it + } + clean++ + reFile, errs := parser.Parse(path, Source(src)) + if len(errs) > 0 { + failures++ + t.Errorf("formatted %s no longer parses: %v", path, errs[0]) + continue + } + if got := scrub(reFile); !reflect.DeepEqual(got, scrub(file)) { + failures++ + t.Errorf("formatting %s changed the tree", path) + } + } + t.Logf("tree round-trip checked over %d files, %d clean parses, %d failures", files, clean, failures) +} + +// TestCorpusExpansionRoundTrip runs the assembly path's contract over the +// corpus: a file the expander reads cleanly expands to exactly the same +// statement sequence after formatting. Files whose includes cannot be +// resolved without the go command's generated headers are skipped on both +// sides of the comparison alike. +func TestCorpusExpansionRoundTrip(t *testing.T) { + corpusSkip(t) + var files, expandable, failures int + for _, path := range corpusSources(t) { + b, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + files++ + src := string(b) + before, errs := parser.ParseWithOptions(path, src, parser.Options{Expand: true}) + if len(errs) > 0 || hasStrayIllegal(src) { + continue + } + expandable++ + after, errs := parser.ParseWithOptions(path, Source(src), parser.Options{Expand: true}) + if len(errs) > 0 { + failures++ + t.Errorf("formatted %s no longer expands: %v", path, errs[0]) + continue + } + if got, want := stmtSignature(after), stmtSignature(before); !reflect.DeepEqual(got, want) { + failures++ + t.Errorf("formatting %s changed the expansion", path) + } + } + t.Logf("expansion round-trip checked over %d files, %d expandable, %d failures", files, expandable, failures) +}