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
This commit is contained in:
1 parent
59637d35b1
commit
32c453d66e
1 file changed
+142
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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)
|
||||
}
|
||||
Reference in new issue
Block a user