// Copyright (c) 2026 Petr Balvín (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 } 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) { // Missing file arguments produce a usage error (code 2). if _, _, code := capture(func() int { return cmdFmt(nil) }); code != 2 { t.Errorf("cmdFmt() code = %d, want 2", code) } 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) } }