142 lines
4.5 KiB
Go
142 lines
4.5 KiB
Go
// 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)
|
||
|
|
}
|