// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package main import ( "os" "path/filepath" "strings" "testing" ) // writeTree writes a directory of files and returns its root. func writeTree(t *testing.T, files map[string]string) string { t.Helper() dir := t.TempDir() for name, content := range files { path := filepath.Join(dir, name) if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatal(err) } } return dir } // TestAsmMacroAndIncludeEndToEnd drives `gasm asm` over a source with an // in-file parameterised macro and an include resolved through -I, and checks // the assembled bytes came from the expansion (the loop body counts six // increments, two per expanded iteration). func TestAsmMacroAndIncludeEndToEnd(t *testing.T) { if testing.Short() { t.Skip("runs the assembler end to end") } dir := writeTree(t, map[string]string{ "inc/consts.h": "#define NITER 3\n", "main_amd64.s": "#include \"textflag.h\"\n" + "#include \"consts.h\"\n" + "#define STEP(r) ADDQ $1, r; ADDQ $1, r\n" + "TEXT ·f(SB), NOSPLIT, $0-8\n" + "\tXORQ AX, AX\n" + "\tMOVQ $NITER, CX\n" + "loop:\n" + "\tSTEP(AX)\n" + "\tDECQ CX\n" + "\tJNZ loop\n" + "\tMOVQ AX, ret+0(FP)\n" + "\tRET\n", }) stdout, stderr, code := capture(func() int { return cmdAsm([]string{"-I", filepath.Join(dir, "inc"), "-GOARCH", "amd64", filepath.Join(dir, "main_amd64.s")}) }) if code != 0 { t.Fatalf("gasm asm exited %d: %s%s", code, stdout, stderr) } // The macro expanded to two ADDQ $1 encodings in the static body; the // iteration count lives in the runtime loop. if n := strings.Count(stdout, "83 c0 01"); n != 2 { t.Errorf("found %d ADDQ $1 encodings in the image, want 2:\n%s", n, stdout) } } // TestAsmIncludeResolutionOrder pins the -I search order end to end: the // including file's directory wins over the -I directories. func TestAsmIncludeResolutionOrder(t *testing.T) { if testing.Short() { t.Skip("runs the assembler end to end") } dir := writeTree(t, map[string]string{ "src/main_amd64.s": "#include \"textflag.h\"\n" + "#include \"vals.h\"\n" + "TEXT ·f(SB), NOSPLIT, $0\n" + "\tMOVQ $VAL, AX\n" + "\tRET\n", "src/vals.h": "#define VAL 1\n", "late/vals.h": "#define VAL 2\n", "early/vals.h": "#define VAL 3\n", }) stdout, stderr, code := capture(func() int { return cmdAsm([]string{"-I", filepath.Join(dir, "early"), "-I", filepath.Join(dir, "late"), "-GOARCH", "amd64", filepath.Join(dir, "src", "main_amd64.s")}) }) if code != 0 { t.Fatalf("gasm asm exited %d: %s%s", code, stdout, stderr) } // VAL came from src/vals.h, not from either -I directory: the image // loads the immediate 1. if !strings.Contains(stdout, "b8 01 00 00 00") { t.Errorf("expected the source-directory VAL (immediate 1) in:\n%s", stdout) } } // TestAsmMissingIncludeIsAnError pins the diagnostic for an include that // resolves nowhere on the assembly path. func TestAsmMissingIncludeIsAnError(t *testing.T) { if testing.Short() { t.Skip("runs the assembler end to end") } path := writeTemp(t, "main_amd64.s", "#include \"textflag.h\"\n#include \"nothere.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n") _, stderr, code := capture(func() int { return cmdAsm([]string{"-GOARCH", "amd64", path}) }) if code == 0 { t.Fatal("gasm asm accepted a file whose include resolves nowhere") } if !strings.Contains(stderr, `#include "nothere.h"`) { t.Errorf("stderr does not name the failing include: %s", stderr) } }