test(parser): cover self-includes, deep chains and diamonds

The include guard's scope is now pinned from three sides: a file
including itself is a cycle diagnostic, a chain of two thousand distinct
headers completes with the deepest content spliced, and a header reached
again through a separate branch splices a second time and is refused as
a redefinition.

Assisted-by: GLM 5.3
This commit is contained in:
petrbalvin committed 2026-10-07 13:54:42 +02:00
1 parent 32c453d66e
commit 21b6348dbe
1 file changed
+74
+74
View File
@@ -6,6 +6,7 @@ package parser
import (
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
@@ -480,6 +481,79 @@ func TestIncludeCycleIsDetected(t *testing.T) {
}
}
// TestSelfIncludeIsDetected covers the shortest include cycle there is: a
// file that includes itself. The contract is the two-file cycle's, a
// diagnostic instead of unbounded recursion.
func TestSelfIncludeIsDetected(t *testing.T) {
path := filepath.Join(t.TempDir(), "self.s")
if err := os.WriteFile(path, []byte("#include \"self.s\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n"), 0o644); err != nil {
t.Fatal(err)
}
_, errs := ParseWithOptions(path, "#include \"self.s\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n", Options{Expand: true})
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "include cycle") {
t.Fatalf("self-include: got %v, want a cycle diagnostic, not a hang", errs)
}
}
// TestDeepIncludeChainCompletes pins the depth contract of include splicing:
// a chain of distinct files costs one recursion level each, and a long chain
// completes with the deepest file's content spliced in, rather than
// overflowing or revisiting files the cycle guard already closed.
func TestDeepIncludeChainCompletes(t *testing.T) {
const depth = 2000
dir := t.TempDir()
for i := 0; i < depth; i++ {
var content string
if i == depth-1 {
content = "#define DEEPEST $7\n"
} else {
content = "#include \"h" + strconv.Itoa(i+1) + ".h\"\n"
}
if err := os.WriteFile(filepath.Join(dir, "h"+strconv.Itoa(i)+".h"), []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
f, errs := ParseWithOptions(filepath.Join(dir, "h0.h"), "#include \"h1.h\"\n", Options{Expand: true})
if len(errs) != 0 {
t.Fatalf("deep include chain: %v", errs)
}
if !definesMacro(f, "DEEPEST") {
t.Fatal("the deepest header's definition did not survive the chain")
}
}
// definesMacro reports whether the parsed file carries a define of name.
func definesMacro(f *ast.File, name string) bool {
for _, d := range f.Decls {
if pp, ok := d.(*ast.Preproc); ok {
if strings.HasPrefix(pp.Raw, "define "+name+" ") {
return true
}
}
}
return false
}
// TestDiamondIncludeSplicesTwice pins the guard's scope: the cycle detector
// tracks the files currently being read, not the files ever read, so a header
// reached again through a separate branch splices a second time and its
// redefinition is refused, exactly as the toolchain's input stack behaves.
func TestDiamondIncludeSplicesTwice(t *testing.T) {
src := "#include \"left.h\"\n#include \"right.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n"
dir := includeTree(t, map[string]string{
"main.s": src,
"left.h": "#include \"shared.h\"\n",
"right.h": "#include \"shared.h\"\n",
"shared.h": "#define FROM left\n",
})
_, errs := ParseWithOptions(filepath.Join(dir, "main.s"), src, Options{Expand: true})
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "redefinition of macro FROM") {
// Silence would mean the shared header was spliced once and quietly
// skipped the second time, which loses its content on that path.
t.Fatalf("diamond include: got %v, want both splices to land", errs)
}
}
func TestUnresolvableIncludeIsAnError(t *testing.T) {
_, errs := ParseWithOptions("t_amd64.s", "#include \"nothere.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n",
Options{Expand: true, IncludeDirs: []string{t.TempDir()}})