Files
gasm-sdk/format/format_test.go
T
2026-09-19 23:48:47 +02:00

222 lines
6.3 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package format
import (
"os"
"strings"
"testing"
"sourcedock.dev/petrbalvin/gasm-devkit/lexer"
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
"sourcedock.dev/petrbalvin/gasm-devkit/token"
)
func TestGolden(t *testing.T) {
in := "#include \"textflag.h\"\n" +
"\n" +
"TEXT ·f(SB), NOSPLIT, $0\n" +
"MOVQ swin_base+0(FP), SI\n" +
"LEAQ (SI)(BX*4), R9\n" +
"ANDQ $-8, R10\n" +
"VFMADD231PD Z14, Z12, Z10\n" +
"RET\n"
want := "#include \"textflag.h\"\n" +
"\n" +
"TEXT ·f(SB), NOSPLIT, $0\n" +
"\tMOVQ swin_base+0(FP), SI\n" +
"\tLEAQ (SI)(BX*4), R9\n" +
"\tANDQ $-8, R10\n" +
"\tVFMADD231PD Z14, Z12, Z10\n" +
"\tRET\n"
got := Source(in)
if got != want {
t.Fatalf("formatting mismatch:\n--- got ---\n%q\n--- want ---\n%q", got, want)
}
}
// TestDocCommentIndent checks that a doc comment preceding a TEXT directive
// sits at column 0 even when another function (ending in RET) precedes it;
// the RET must terminate the previous body for indentation purposes.
func TestDocCommentIndent(t *testing.T) {
in := "#include \"textflag.h\"\n" +
"\n" +
"// func first()\n" +
"TEXT ·first(SB), NOSPLIT, $0\n" +
"XORQ AX, AX\n" +
"RET\n" +
"\n" +
"// func second()\n" +
"TEXT ·second(SB), NOSPLIT, $0\n" +
"RET\n"
want := "#include \"textflag.h\"\n" +
"\n" +
"// func first()\n" +
"TEXT ·first(SB), NOSPLIT, $0\n" +
"\tXORQ AX, AX\n" +
"\tRET\n" +
"\n" +
"// func second()\n" +
"TEXT ·second(SB), NOSPLIT, $0\n" +
"\tRET\n"
got := Source(in)
if got != want {
t.Fatalf("formatting mismatch:\n--- got ---\n%q\n--- want ---\n%q", got, want)
}
// Body comments stay indented.
body := "#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT, $0\n// inside the body\nXORQ AX, AX\nRET\n"
gotBody := Source(body)
if !strings.Contains(gotBody, "\t// inside the body\n") {
t.Fatalf("body comment must stay indented:\n%q", gotBody)
}
}
// TestBlankLines checks the blank-line canonicalisation: exactly one blank
// line before a new block (a label, or TEXT/GLOBL), runs of blanks collapsed
// to one, and no blank forced after TEXT, between stacked labels, or at the
// top of the file. Leading comments belong to the block they precede.
func TestBlankLines(t *testing.T) {
in := "#include \"textflag.h\"\n" +
"TEXT ·f(SB), NOSPLIT, $0\n" +
"first:\n" + // first label: no blank after TEXT
"XORQ AX, AX\n" +
"JMP next\n" + // unlabelled glue: fmt inserts a blank before next:
"next:\n" +
"stacked:\n" + // stacked labels share an address: no blank between
"INCQ AX\n" +
"\n" +
"\n" + // two blanks collapse to one
"// separated block\n" + // comment belongs to the label below
"later:\n" +
"RET\n" +
"// func g()\n" + // doc comment: blank goes before it
"TEXT ·g(SB), NOSPLIT, $0\n" +
"RET\n" +
"GLOBL ·mask(SB), RODATA, $8\n" + // blank before GLOBL…
"DATA ·mask+0(SB)/4, $1\n" + // …but not before DATA
"\n" +
"\n" +
"\n" // trailing blanks dropped
want := "#include \"textflag.h\"\n" +
"\n" +
"TEXT ·f(SB), NOSPLIT, $0\n" +
"first:\n" +
"\tXORQ AX, AX\n" +
"\tJMP next\n" +
"\n" +
"next:\n" +
"stacked:\n" +
"\tINCQ AX\n" +
"\n" +
"\t// separated block\n" + // body comment before a label stays indented
"later:\n" +
"\tRET\n" +
"\n" +
"// func g()\n" +
"TEXT ·g(SB), NOSPLIT, $0\n" +
"\tRET\n" +
"\n" +
"GLOBL ·mask(SB), RODATA, $8\n" +
"DATA ·mask+0(SB)/4, $1\n"
got := Source(in)
if got != want {
t.Fatalf("formatting mismatch:\n--- got ---\n%q\n--- want ---\n%q", got, want)
}
if again := Source(got); again != got {
t.Fatalf("not idempotent:\n%q", again)
}
}
func TestOperandSpacing(t *testing.T) {
cases := map[string]string{
"4(SI)": "4(SI)",
"(SI)(BX*4)": "(SI)(BX*4)",
"$-8": "$-8",
"$0x80020100": "$0x80020100",
"swin_base+0(FP)": "swin_base+0(FP)",
"mask24<>(SB)": "mask24<>(SB)",
"·idx16+0(SB)/4": "·idx16+0(SB)/4",
"NOSPLIT|DUPOK": "NOSPLIT|DUPOK",
}
for in, want := range cases {
toks := lexOperands(in)
if got := renderOps(toks); got != want {
t.Errorf("renderOps(%q) = %q, want %q", in, got, want)
}
}
}
// TestFlagListRoundTrip pins the '|' flag separator and the <ABIInternal>
// marker through a full format pass: the bars the Go toolchain requires and
// the ABI bracket must survive byte for byte, on TEXT and GLOBL alike.
func TestFlagListRoundTrip(t *testing.T) {
for _, in := range []string{
"TEXT ·f(SB), NOSPLIT|NOFRAME|DUPOK, $0\n\tRET\n",
"TEXT ·foo<ABIInternal>(SB), NOSPLIT, $-0-24\n\tRET\n",
"GLOBL ·mask(SB), RODATA|NOPTR, $8\n",
} {
if got := Source(in); got != in {
t.Fatalf("flag list did not round-trip:\n--- got ---\n%q\n--- want ---\n%q", got, in)
}
}
}
// TestCRLFInputIsNormalisedToLF checks that a CRLF file comes out with
// uniform LF endings: a // comment must not carry its line's trailing \r
// into the output.
func TestCRLFInputIsNormalisedToLF(t *testing.T) {
in := "// func f()\r\nTEXT ·f(SB), NOSPLIT, $0\r\nRET\r\n"
want := "// func f()\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n"
got := Source(in)
if got != want {
t.Fatalf("CRLF formatting mismatch:\n--- got ---\n%q\n--- want ---\n%q", got, want)
}
if strings.Contains(got, "\r") {
t.Fatalf("output still contains CR: %q", got)
}
}
// lexOperands lexes a single operand string and drops the EOF token.
func lexOperands(s string) []token.Token {
toks := lexer.Tokenize(s)
return toks[:len(toks)-1] // drop trailing EOF
}
func TestIdempotent(t *testing.T) {
src, err := os.ReadFile("../testdata/sample_amd64.s")
if err != nil {
t.Fatal(err)
}
once := Source(string(src))
twice := Source(once)
if once != twice {
t.Fatal("formatting is not idempotent on the fixture")
}
}
// TestRoundTrip checks that formatting produces source that still parses
// cleanly on the in-repository fixture.
func TestRoundTrip(t *testing.T) {
files := []string{"../testdata/sample_amd64.s"}
for _, path := range files {
src, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
formatted := Source(string(src))
if _, errs := parser.Parse(path, formatted); len(errs) > 0 {
t.Errorf("formatted %s no longer parses: %v", path, errs)
}
if strings.TrimSpace(formatted) == "" {
t.Errorf("formatted %s is empty", path)
}
}
}