From 21b6348dbe6376a57b306fce73ac048178469ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Wed, 7 Oct 2026 12:38:35 +0200 Subject: [PATCH] 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 --- parser/preproc_test.go | 74 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/parser/preproc_test.go b/parser/preproc_test.go index 50270a1..ce5af92 100644 --- a/parser/preproc_test.go +++ b/parser/preproc_test.go @@ -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()}})