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:
1 parent
32c453d66e
commit
21b6348dbe
1 file changed
+74
@@ -6,6 +6,7 @@ package parser
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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) {
|
func TestUnresolvableIncludeIsAnError(t *testing.T) {
|
||||||
_, errs := ParseWithOptions("t_amd64.s", "#include \"nothere.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n",
|
_, errs := ParseWithOptions("t_amd64.s", "#include \"nothere.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n",
|
||||||
Options{Expand: true, IncludeDirs: []string{t.TempDir()}})
|
Options{Expand: true, IncludeDirs: []string{t.TempDir()}})
|
||||||
|
|||||||
Reference in new issue
Block a user