2026-07-06 09:49:50 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"io"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const clean = "#include \"textflag.h\"\n" +
|
|
|
|
|
"TEXT ·f(SB), NOSPLIT, $0\n" +
|
|
|
|
|
"\tMOVQ AX, BX\n" +
|
|
|
|
|
"loop:\n" +
|
|
|
|
|
"\tJMP loop\n" +
|
|
|
|
|
"\tRET\n"
|
|
|
|
|
|
|
|
|
|
const buggy = "#include \"textflag.h\"\n" +
|
|
|
|
|
"TEXT ·f(SB), NOSPLIT, $0\n" +
|
|
|
|
|
"\tBOGUS AX, BX\n" +
|
|
|
|
|
"\tJMP nowhere\n" +
|
|
|
|
|
"\tRET\n"
|
|
|
|
|
|
|
|
|
|
func writeTemp(t *testing.T, name, content string) string {
|
|
|
|
|
t.Helper()
|
|
|
|
|
path := filepath.Join(t.TempDir(), name)
|
|
|
|
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
return path
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// capture runs fn with stdout and stderr redirected and returns both plus the
|
|
|
|
|
// exit code fn produced.
|
|
|
|
|
func capture(fn func() int) (stdout, stderr string, code int) {
|
|
|
|
|
oldOut, oldErr := os.Stdout, os.Stderr
|
|
|
|
|
rOut, wOut, _ := os.Pipe()
|
|
|
|
|
rErr, wErr, _ := os.Pipe()
|
|
|
|
|
os.Stdout, os.Stderr = wOut, wErr
|
|
|
|
|
|
|
|
|
|
code = fn()
|
|
|
|
|
|
|
|
|
|
wOut.Close()
|
|
|
|
|
wErr.Close()
|
|
|
|
|
os.Stdout, os.Stderr = oldOut, oldErr
|
|
|
|
|
ob, _ := io.ReadAll(rOut)
|
|
|
|
|
eb, _ := io.ReadAll(rErr)
|
|
|
|
|
return string(ob), string(eb), code
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 21:24:41 +02:00
|
|
|
// TestCmdFmtRecursive checks the go-fmt-style directory mode: with no
|
|
|
|
|
// arguments every .s file below the working directory is formatted in place
|
|
|
|
|
// ("." and "_" directories skipped), changed files are listed, and a second
|
|
|
|
|
// run is a no-op.
|
|
|
|
|
func TestCmdFmtRecursive(t *testing.T) {
|
|
|
|
|
tmp := t.TempDir()
|
|
|
|
|
t.Chdir(tmp)
|
|
|
|
|
unformatted := []byte("TEXT ·f(SB),NOSPLIT,$0\nRET\n")
|
|
|
|
|
write := func(path string) {
|
|
|
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(path, unformatted, 0o644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
write("a_amd64.s")
|
|
|
|
|
write(filepath.Join("sub", "b_amd64.s"))
|
|
|
|
|
write(filepath.Join("_refs", "c_amd64.s"))
|
|
|
|
|
write(filepath.Join(".git", "d_amd64.s"))
|
|
|
|
|
|
|
|
|
|
out, errOut, code := capture(func() int { return cmdFmt(nil) })
|
|
|
|
|
if code != 0 {
|
|
|
|
|
t.Fatalf("code = %d (%s)", code, errOut)
|
|
|
|
|
}
|
|
|
|
|
if out != "a_amd64.s\n"+filepath.Join("sub", "b_amd64.s")+"\n" {
|
|
|
|
|
t.Errorf("listed files unexpected:\n%s", out)
|
|
|
|
|
}
|
|
|
|
|
for _, p := range []string{"a_amd64.s", filepath.Join("sub", "b_amd64.s")} {
|
|
|
|
|
b, _ := os.ReadFile(p)
|
|
|
|
|
if !strings.Contains(string(b), "\tRET") {
|
|
|
|
|
t.Errorf("%s not formatted in place:\n%s", p, b)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, p := range []string{filepath.Join("_refs", "c_amd64.s"), filepath.Join(".git", "d_amd64.s")} {
|
|
|
|
|
b, _ := os.ReadFile(p)
|
|
|
|
|
if string(b) != string(unformatted) {
|
|
|
|
|
t.Errorf("%s must not be touched:\n%s", p, b)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Second pass: everything is canonical, nothing is listed.
|
|
|
|
|
out, _, code = capture(func() int { return cmdFmt(nil) })
|
|
|
|
|
if code != 0 || out != "" {
|
|
|
|
|
t.Errorf("second pass: code=%d out=%q, want a no-op", code, out)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 09:49:50 +02:00
|
|
|
func TestCmdTokens(t *testing.T) {
|
|
|
|
|
path := writeTemp(t, "f_amd64.s", clean)
|
|
|
|
|
out, _, code := capture(func() int { return cmdTokens([]string{path}) })
|
|
|
|
|
if code != 0 {
|
|
|
|
|
t.Fatalf("code = %d", code)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(out, "IDENT") || !strings.Contains(out, "TEXT") {
|
|
|
|
|
t.Errorf("token dump missing expected tokens:\n%s", out)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCmdParseOK(t *testing.T) {
|
|
|
|
|
path := writeTemp(t, "f_amd64.s", clean)
|
|
|
|
|
out, _, code := capture(func() int { return cmdParse([]string{path}) })
|
|
|
|
|
if code != 0 {
|
|
|
|
|
t.Fatalf("code = %d", code)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(out, "OK") || !strings.Contains(out, "1 functions") {
|
|
|
|
|
t.Errorf("parse output = %q", out)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCmdParseError(t *testing.T) {
|
|
|
|
|
path := writeTemp(t, "bad_amd64.s", "TEXT ·f(SB), NOSPLIT, $0\n) (\n\tRET\n")
|
|
|
|
|
_, errOut, code := capture(func() int { return cmdParse([]string{path}) })
|
|
|
|
|
if code != 1 {
|
|
|
|
|
t.Fatalf("code = %d, want 1", code)
|
|
|
|
|
}
|
|
|
|
|
if errOut == "" {
|
|
|
|
|
t.Error("expected a parse error on stderr")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCmdParseMissingFile(t *testing.T) {
|
|
|
|
|
_, _, code := capture(func() int { return cmdParse([]string{"/nonexistent/file.s"}) })
|
|
|
|
|
if code != 1 {
|
|
|
|
|
t.Fatalf("code = %d, want 1", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCmdLintClean(t *testing.T) {
|
|
|
|
|
path := writeTemp(t, "f_amd64.s", clean)
|
|
|
|
|
_, _, code := capture(func() int { return cmdLint([]string{path}) })
|
|
|
|
|
if code != 0 {
|
|
|
|
|
t.Fatalf("clean file should lint with code 0, got %d", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCmdLintErrors(t *testing.T) {
|
|
|
|
|
path := writeTemp(t, "f_amd64.s", buggy)
|
|
|
|
|
out, _, code := capture(func() int { return cmdLint([]string{path}) })
|
|
|
|
|
if code != 1 {
|
|
|
|
|
t.Fatalf("code = %d, want 1", code)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(out, "unknown-instruction") || !strings.Contains(out, "undefined-label") {
|
|
|
|
|
t.Errorf("lint output missing expected codes:\n%s", out)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCmdLintDisable(t *testing.T) {
|
|
|
|
|
path := writeTemp(t, "f_amd64.s", buggy)
|
|
|
|
|
args := []string{"-disable", "unknown-instruction,undefined-label", path}
|
|
|
|
|
_, _, code := capture(func() int { return cmdLint(args) })
|
|
|
|
|
if code != 0 {
|
|
|
|
|
t.Fatalf("disabling both rules should yield code 0, got %d", code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCmdFmtStdout(t *testing.T) {
|
|
|
|
|
path := writeTemp(t, "f_amd64.s", "TEXT ·f(SB),NOSPLIT,$0\nMOVQ AX,BX\nRET\n")
|
|
|
|
|
out, _, code := capture(func() int { return cmdFmt([]string{path}) })
|
|
|
|
|
if code != 0 {
|
|
|
|
|
t.Fatalf("code = %d", code)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(out, "TEXT ·f(SB), NOSPLIT, $0") || !strings.Contains(out, "\tMOVQ AX, BX") {
|
|
|
|
|
t.Errorf("formatted output unexpected:\n%s", out)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCmdFmtWrite(t *testing.T) {
|
|
|
|
|
path := writeTemp(t, "f_amd64.s", "TEXT ·f(SB),NOSPLIT,$0\nMOVQ AX,BX\nRET\n")
|
|
|
|
|
_, _, code := capture(func() int { return cmdFmt([]string{"-w", path}) })
|
|
|
|
|
if code != 0 {
|
|
|
|
|
t.Fatalf("code = %d", code)
|
|
|
|
|
}
|
|
|
|
|
b, _ := os.ReadFile(path)
|
|
|
|
|
if !strings.Contains(string(b), "\tMOVQ AX, BX") {
|
|
|
|
|
t.Errorf("file not rewritten:\n%s", b)
|
|
|
|
|
}
|
|
|
|
|
// Idempotent: a second -w pass leaves the file unchanged.
|
|
|
|
|
_, _, _ = capture(func() int { return cmdFmt([]string{"-w", path}) })
|
|
|
|
|
b2, _ := os.ReadFile(path)
|
|
|
|
|
if string(b) != string(b2) {
|
|
|
|
|
t.Error("fmt -w is not idempotent")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUsage(t *testing.T) {
|
|
|
|
|
var b bytes.Buffer
|
|
|
|
|
usage(&b)
|
|
|
|
|
if !strings.Contains(b.String(), "gasm") {
|
|
|
|
|
t.Errorf("usage text unexpected:\n%s", b.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCmdArgErrors(t *testing.T) {
|
2026-07-12 21:24:41 +02:00
|
|
|
// A missing path is an error (code 1); cmdFmt with no arguments is the
|
|
|
|
|
// recursive mode now, covered by TestCmdFmtRecursive.
|
|
|
|
|
if _, _, code := capture(func() int { return cmdFmt([]string{"no/such/path"}) }); code != 1 {
|
|
|
|
|
t.Errorf("cmdFmt(missing path) code = %d, want 1", code)
|
2026-07-06 09:49:50 +02:00
|
|
|
}
|
|
|
|
|
if _, _, code := capture(func() int { return cmdLint(nil) }); code != 2 {
|
|
|
|
|
t.Errorf("cmdLint() code = %d, want 2", code)
|
|
|
|
|
}
|
|
|
|
|
if _, _, code := capture(func() int { return cmdTokens(nil) }); code != 2 {
|
|
|
|
|
t.Errorf("cmdTokens() code = %d, want 2", code)
|
|
|
|
|
}
|
|
|
|
|
if _, _, code := capture(func() int { return cmdParse(nil) }); code != 2 {
|
|
|
|
|
t.Errorf("cmdParse() code = %d, want 2", code)
|
|
|
|
|
}
|
|
|
|
|
}
|