// 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) }