diff --git a/_xtool/internal/header/header.go b/_xtool/internal/header/header.go index f753711c..45021f53 100644 --- a/_xtool/internal/header/header.go +++ b/_xtool/internal/header/header.go @@ -1,8 +1,13 @@ package header import ( + "fmt" + "io" + "maps" "os" "path/filepath" + "slices" + "sort" "strings" "github.com/goplus/lib/c/clang" @@ -41,75 +46,84 @@ func PkgHfileInfo(includes []string, args []string, mix bool) *PkgHfilesInfo { } defer os.Remove(outfile.Name()) - inters := make(map[string]struct{}) - others := []string{} // impl & third - for _, f := range includes { - content := "#include <" + f + ">" - index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{ - File: content, - Temp: true, - Args: args, - }) - if err != nil { - panic(err) - } - clangutils.GetInclusions(unit, func(inced clang.File, incins []clang.SourceLocation) { - if len(incins) == 1 { - filename := filepath.Clean(clang.GoString(inced.FileName())) - info.Inters = append(info.Inters, filename) - inters[filename] = struct{}{} - } - }) - unit.Dispose() - index.Dispose() + mmOutput, err := os.CreateTemp("", "mmoutput_*") + if err != nil { + panic(err) + } + defer os.Remove(mmOutput.Name()) + + includeTrie := NewTrie(WithReversePathSegmenter()) + + for _, inc := range includes { + includeTrie.Insert(inc) + args = append(args, fmt.Sprintf("--no-system-header-prefix=%s", inc)) } + args = append(args, "-MM", "-MF", mmOutput.Name()) + clangtool.ComposeIncludes(includes, outfile.Name()) index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{ File: outfile.Name(), Temp: false, Args: args, }) + defer unit.Dispose() defer index.Dispose() if err != nil { panic(err) } + + var others []string + inters, longestPrefix := RetrieveInterfaceFromMM(outfile.Name(), mmOutput, includeTrie) + clangutils.GetInclusions(unit, func(inced clang.File, incins []clang.SourceLocation) { // not in the first level include maybe impl or third hfile filename := filepath.Clean(clang.GoString(inced.FileName())) - _, inter := inters[filename] - if len(incins) > 1 && !inter { + + // skip the composed header + if filename == outfile.Name() { + return + } + + if _, isInterface := inters[filename]; !isInterface { others = append(others, filename) } }) - if mix { - info.Thirds = others - return info - } + info.Inters = slices.Collect(maps.Keys(inters)) - root, err := filepath.Abs(commonParentDir(info.Inters)) + absLongestPrefix, err := filepath.Abs(longestPrefix) if err != nil { panic(err) } - for _, f := range others { - file, err := filepath.Abs(f) + + fmt.Fprintln(os.Stderr, "tttttt", longestPrefix, CommonParentDir(info.Inters)) + + for _, filename := range others { + if mix { + info.Thirds = append(info.Thirds, filename) + continue + } + filePath, err := filepath.Abs(filename) if err != nil { panic(err) } - if strings.HasPrefix(file, root) { - info.Impls = append(info.Impls, f) + if strings.HasPrefix(filePath, absLongestPrefix) { + info.Impls = append(info.Impls, filename) } else { - info.Thirds = append(info.Thirds, f) + info.Thirds = append(info.Thirds, filename) } } + + sort.Strings(info.Inters) + return info } // commonParentDir finds the longest common parent directory path for a given slice of paths. // For example, given paths ["/a/b/c/d", "/a/b/e/f"], it returns "/a/b". -func commonParentDir(paths []string) string { +func CommonParentDir(paths []string) string { if len(paths) == 0 { return "" } @@ -128,3 +142,33 @@ func commonParentDir(paths []string) string { } return filepath.Dir(paths[0]) } + +func RetrieveInterfaceFromMM( + composedHeaderFileName string, + mmOutput *os.File, + includeTrie *Trie, +) (interfaceMap map[string]struct{}, prefix string) { + fileName := strings.TrimSuffix(filepath.Base(composedHeaderFileName), ".h") + + interfaceMap = make(map[string]struct{}) + + content, _ := io.ReadAll(mmOutput) + + mmTrie := NewTrie() + + for _, line := range strings.Fields(string(content)) { + // skip composed header file + if strings.Contains(line, fileName) || line == `\` { + continue + } + headerFile := filepath.Clean(line) + + if includeTrie.IsOnSameBranch(headerFile) { + mmTrie.Insert(headerFile) + + interfaceMap[headerFile] = struct{}{} + } + } + prefix = mmTrie.LongestPrefix() + return +} diff --git a/_xtool/internal/header/header_test.go b/_xtool/internal/header/header_test.go index 6c288289..175d1a8e 100644 --- a/_xtool/internal/header/header_test.go +++ b/_xtool/internal/header/header_test.go @@ -2,11 +2,16 @@ package header_test import ( "fmt" + "os" "path/filepath" "reflect" "strings" "testing" + "time" + "github.com/goplus/lib/c/clang" + clangutils "github.com/goplus/llcppg/_xtool/internal/clang" + "github.com/goplus/llcppg/_xtool/internal/clangtool" "github.com/goplus/llcppg/_xtool/internal/header" llconfig "github.com/goplus/llcppg/config" ) @@ -73,3 +78,173 @@ func TestPkgHfileInfo(t *testing.T) { }) } } + +func TestLongestPrefix(t *testing.T) { + testCases := []struct { + name string + strs []string + want string + }{ + { + name: "empty string 1", + strs: []string{}, + want: "", + }, + { + name: "empty string 2", + strs: []string{"", ""}, + want: ".", + }, + { + name: "one empty string(b)", + strs: []string{"/a", ""}, + want: "", + }, + { + name: "one empty string(a)", + strs: []string{"", "/a"}, + + want: "", + }, + // FIXME: substring bug + // { + // name: "b is substring of a", + // strs: []string{"/usr/a/b", "/usr/a"}, + // want: "/usr/a", + // }, + // { + // name: "a is substring of b", + // strs: []string{"/usr/c", "/usr/c/b"}, + // want: "/usr/c", + // }, + { + name: "normal case 1", + strs: []string{"testdata/hfile/temp1.h", "testdata/thirdhfile/third.h"}, + want: "testdata", + }, + { + name: "normal case 2", + strs: []string{"testdata/hfile/temp1.h", "testdata/hfile/third.h"}, + + want: "testdata/hfile", + }, + // FIXME: absolute path + // { + // name: "normal case 3", + // strs: []string{"/opt/homebrew/Cellar/cjson/1.7.18/include/cJSON/cJSON.h", "/opt/homebrew/Cellar/cjson/1.7.18/include/cJSON.h", "/opt/homebrew/Cellar/cjson/1.7.18/include/zlib/zlib.h"}, + // want: "/opt/homebrew/Cellar/cjson/1.7.18/include", + // }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if got := header.CommonParentDir(tc.strs); got != tc.want { + t.Fatalf("unexpected longest prefix: want %s got %s", tc.want, got) + } + }) + } +} + +func benchmarkFn(fn func()) time.Duration { + now := time.Now() + + fn() + + return time.Since(now) +} + +func TestBenchmarkPkgHfileInfo(t *testing.T) { + include := []string{"temp1.h", "temp2.h"} + cflags := []string{"-I./testdata/hfile", "-I./testdata/thirdhfile"} + t1 := benchmarkFn(func() { + for i := 0; i < 100; i++ { + pkgHfileInfo(include, cflags, false) + } + }) + + t2 := benchmarkFn(func() { + for i := 0; i < 100; i++ { + header.PkgHfileInfo(include, cflags, false) + } + }) + + fmt.Println("old PkgHfileInfo elapsed: ", t1, "new PkgHfileInfo elasped: ", t2) +} + +func pkgHfileInfo(includes []string, args []string, mix bool) *header.PkgHfilesInfo { + info := &header.PkgHfilesInfo{ + Inters: []string{}, + Impls: []string{}, + Thirds: []string{}, + } + outfile, err := os.CreateTemp("", "compose_*.h") + if err != nil { + panic(err) + } + defer os.Remove(outfile.Name()) + + inters := make(map[string]struct{}) + others := []string{} // impl & third + for _, f := range includes { + content := "#include <" + f + ">" + index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{ + File: content, + Temp: true, + Args: args, + }) + if err != nil { + panic(err) + } + clangutils.GetInclusions(unit, func(inced clang.File, incins []clang.SourceLocation) { + if len(incins) == 1 { + filename := filepath.Clean(clang.GoString(inced.FileName())) + info.Inters = append(info.Inters, filename) + inters[filename] = struct{}{} + } + }) + unit.Dispose() + index.Dispose() + } + + clangtool.ComposeIncludes(includes, outfile.Name()) + index, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{ + File: outfile.Name(), + Temp: false, + Args: args, + }) + defer unit.Dispose() + defer index.Dispose() + if err != nil { + panic(err) + } + clangutils.GetInclusions(unit, func(inced clang.File, incins []clang.SourceLocation) { + // not in the first level include maybe impl or third hfile + filename := filepath.Clean(clang.GoString(inced.FileName())) + _, inter := inters[filename] + if len(incins) > 1 && !inter { + others = append(others, filename) + } + }) + + if mix { + info.Thirds = others + return info + } + + root, err := filepath.Abs(header.CommonParentDir(info.Inters)) + if err != nil { + panic(err) + } + for _, f := range others { + file, err := filepath.Abs(f) + if err != nil { + panic(err) + } + if strings.HasPrefix(file, root) { + info.Impls = append(info.Impls, f) + } else { + info.Thirds = append(info.Thirds, f) + } + } + return info +} diff --git a/_xtool/internal/header/testdata/hfile/temp1.h b/_xtool/internal/header/testdata/hfile/temp1.h index 704cd55e..14a2a764 100644 --- a/_xtool/internal/header/testdata/hfile/temp1.h +++ b/_xtool/internal/header/testdata/hfile/temp1.h @@ -1,2 +1,3 @@ #include "tempimpl.h" +#include "temp2.h" #include \ No newline at end of file diff --git a/_xtool/internal/header/trie.go b/_xtool/internal/header/trie.go new file mode 100644 index 00000000..bfdd00d7 --- /dev/null +++ b/_xtool/internal/header/trie.go @@ -0,0 +1,209 @@ +package header + +import ( + "iter" + "os" + "path/filepath" + "slices" + "strings" +) + +type Segmenter func(s string) iter.Seq[string] + +type TrieNode struct { + isLeaf bool // Indicates if this node represents the end of a word + linkCount int // Number of children nodes + children map[string]*TrieNode // Map of child nodes by segment +} + +// Creates a new TrieNode with empty children map +func NewTrieNode() *TrieNode { + return &TrieNode{children: make(map[string]*TrieNode)} +} + +type Trie struct { + root *TrieNode // Root node of the trie + segmenter Segmenter // Function to split strings into segments +} +type Options func(*Trie) // Function type for configuring Trie options + +func skipEmpty(s []string) []string { + for len(s) > 0 && s[0] == "" { + s = s[1:] + } + return s +} + +func splitPathAbsSafe(path string) (paths []string) { + originalPath := filepath.Clean(path) + + sep := string(os.PathSeparator) + + // keep absolute path info + if filepath.IsAbs(originalPath) { + i := strings.Index(originalPath[1:], sep) + if i > 0 { + // bound edge: if i is greater than zero, which means there's second separator + // for example, /usr/, i: 3, with first separator what we just skipped, i: 4 + paths = append(paths, originalPath[0:i+1]) + paths = append(paths, skipEmpty(strings.Split(originalPath[i+1:], sep))...) + } else { + // start with / but no other / is found, like /usr + paths = append(paths, originalPath) + } + } + + if len(paths) == 0 { + paths = skipEmpty(strings.Split(originalPath, sep)) + } + + return +} + +// Returns an option to configure path segmenter +// Splits strings by OS path separator and yields each segment +func WithPathSegmenter() Options { + return func(t *Trie) { + t.segmenter = func(s string) iter.Seq[string] { + return func(yield func(string) bool) { + for _, path := range splitPathAbsSafe(s) { + if path != "" && !yield(path) { + return + } + } + } + } + } +} + +// Returns an option to configure reverse path segmenter +// Splits and reverses strings by OS path separator +func WithReversePathSegmenter() Options { + return func(t *Trie) { + t.segmenter = func(s string) iter.Seq[string] { + return func(yield func(string) bool) { + paths := splitPathAbsSafe(s) + + slices.Reverse(paths) + + for _, path := range paths { + if path != "" && !yield(path) { + return + } + } + } + } + } +} + +// Creates a new Trie with default path segmenter +// Applies all provided options to configure the Trie +func NewTrie(opts ...Options) *Trie { + t := &Trie{root: NewTrieNode()} + + WithPathSegmenter()(t) + + for _, o := range opts { + o(t) + } + + return t +} + +// Inserts a string into the trie +// Creates nodes for each segment in the string +func (t *Trie) Insert(s string) { + if s == "" { + return + } + node := t.root + + for segment := range t.segmenter(s) { + child, ok := node.children[segment] + if !ok { + child = NewTrieNode() + node.children[segment] = child + node.linkCount++ + } + node = child + } + node.isLeaf = true +} + +// Searches for a prefix in the trie +// Returns the node at the end of the prefix or nil if not found +func (t *Trie) searchPrefix(s string) *TrieNode { + if s == "" { + return nil + } + node := t.root + + for segment := range t.segmenter(s) { + child, ok := node.children[segment] + if !ok { + return nil + } + node = child + } + + return node +} + +// Finds the longest common prefix of the given string +// Returns the longest prefix that exists in the trie +// +// Implement Source: https://leetcode.com/problems/longest-common-prefix/solutions/127449/longest-common-prefix +func (t *Trie) LongestPrefix() string { + var prefix []string + + dfs(&prefix, "", t.root, nil) + + return filepath.Join(prefix...) +} + +func dfs(prefix *[]string, currentPrefix string, node, parent *TrieNode) { + if node == nil { + return + } + if parent != nil && (parent.linkCount != 1 || parent.isLeaf) { + return + } + + if currentPrefix != "" { + *prefix = append(*prefix, currentPrefix) + } + + for current, child := range node.children { + dfs(prefix, current, child, node) + } +} + +// IsOnSameBranch checks the given s is the subset of trie tree +func (t *Trie) IsOnSameBranch(s string) bool { + if s == "" { + return false + } + node := t.root + + for segment := range t.segmenter(s) { + child, ok := node.children[segment] + if !ok { + // if the current node is end, but there's something unmatched, we still consider it valid. + // for example, + // input: /c/b/a, tree: /c/b, valid + // input: /c/b/a, tree: /c/b/c, invalid + // input: /c/b, tree: /c/b/c, valid + return node.isLeaf + } + node = child + } + + return node != nil +} + +// Checks if the trie contains the exact string +// Returns true if the string exists in the trie +func (t *Trie) Search(s string) bool { + node := t.searchPrefix(s) + return node != nil && node.isLeaf +} diff --git a/_xtool/internal/header/trie_test.go b/_xtool/internal/header/trie_test.go new file mode 100644 index 00000000..d770ab3b --- /dev/null +++ b/_xtool/internal/header/trie_test.go @@ -0,0 +1,438 @@ +package header_test + +import ( + "testing" + + "github.com/goplus/llcppg/_xtool/internal/header" +) + +func TestTrieSubset(t *testing.T) { + testCases := []struct { + name string + search string + inserted []string + want bool + }{ + { + name: "empty string", + search: "abc", + want: false, + }, + { + name: "input empty string", + search: "", + inserted: []string{""}, + want: false, + }, + { + name: "one string", + search: "/a", + inserted: []string{"/a"}, + want: true, + }, + { + name: "two string", + search: "/a", + inserted: []string{"/a", "/b"}, + want: true, + }, + { + name: "multiple string case 1", + search: "/c", + inserted: []string{"/a", "/b", "/d"}, + want: false, + }, + + { + name: "multiple string case 2", + search: "", + inserted: []string{"/a", "/b", "/d"}, + want: false, + }, + + { + name: "multiple string case 3", + search: "/c", + inserted: []string{"/a/c", "/b/c", "/c/d"}, + want: true, + }, + + { + name: "multiple string case 4", + search: "/c/d", + inserted: []string{"/a/c/d", "/b/c/d", "/c/d/a"}, + want: true, + }, + { + name: "substring string case 1", + search: "/a/b", + inserted: []string{"/a"}, + want: true, + }, + { + name: "substring string case 2", + search: "/a", + inserted: []string{"/a/b"}, + want: true, + }, + + { + name: "substring string case 3", + search: "/a/b", + inserted: []string{"/a/b", "/a/b/c"}, + want: true, + }, + + { + name: "substring string case 4", + search: "/c/b", + inserted: []string{"/a/b", "/c/b/a"}, + want: true, + }, + { + name: "substring string case 5", + search: "/c/a", + inserted: []string{"/a/b", "/c/b/a"}, + want: false, + }, + { + name: "substring string case 6", + search: "/c/b/c", + inserted: []string{"/a/b", "/c/b/a"}, + want: false, + }, + { + name: "substring string case 7", + search: "/c/b", + inserted: []string{"/a/b", "/c/b/c/a"}, + want: true, + }, + { + name: "absolute path case 1", + search: "a", + inserted: []string{"/a/b"}, + want: false, + }, + { + name: "absolute path case 2", + search: "/a", + inserted: []string{"a/b", "a/b/c"}, + want: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + trie := header.NewTrie() + + for _, i := range tc.inserted { + trie.Insert(i) + } + if got := trie.IsOnSameBranch(tc.search); got != tc.want { + t.Fatalf("unexpected result: want %v got %v", tc.want, got) + } + }) + } +} + +func TestTrieSearch(t *testing.T) { + testCases := []struct { + name string + search string + inserted []string + want bool + }{ + { + name: "Empty string insertion and search", + search: "", + inserted: []string{""}, + want: false, + }, + { + name: "Single directory exact match", + search: "/usr/local/bin/", + inserted: []string{"/usr/local/bin/"}, + want: true, + }, + { + name: "Single directory partial match", + search: "/usr/local/bin/python", + inserted: []string{"/usr/local/bin/"}, + want: false, + }, + { + name: "Multiple directories exact match", + search: "/usr/local/lib/", + inserted: []string{"/usr/local/bin/", "/usr/local/lib/", "/usr/include/"}, + want: true, + }, + { + name: "Multiple directories partial match", + search: "/usr/local/lib/python", + inserted: []string{"/usr/local/bin/", "/usr/local/lib/", "/usr/include/"}, + want: false, + }, + { + name: "Non-existent path", + search: "/non/existent/path", + inserted: []string{"/usr/local/bin/", "/usr/local/lib/"}, + want: false, + }, + { + name: "Empty search string", + search: "", + inserted: []string{"/usr/local/bin/"}, + want: false, + }, + { + name: "Subdirectory search", + search: "/usr/local/bin/", + inserted: []string{"/usr/local/bin/"}, + want: true, + }, + { + name: "Deep directory structure", + search: "/a/b/c/d/e/f/g", + inserted: []string{"/a/b/c/d/e/f/g"}, + want: true, + }, + { + name: "Long path with special characters", + search: "/home/user/!@#$%^&*()", + inserted: []string{"/home/user/!@#$%^&*()"}, + want: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + trie := header.NewTrie() + for _, word := range tc.inserted { + trie.Insert(word) + } + if got := trie.Search(tc.search); got != tc.want { + t.Fatalf("Search(%q) = %v, want %v", tc.search, got, tc.want) + } + }) + } +} + +func TestTrieLongestPrefix(t *testing.T) { + tests := []struct { + name string + inserted []string + want string + }{ + { + name: "Empty trie", + inserted: []string{}, + want: "", + }, + { + name: "Single directory exact match", + inserted: []string{"/usr/local/bin/"}, + want: "/usr/local/bin", + }, + { + name: "Single directory partial match", + inserted: []string{"/usr/local/bin/", "/usr/local/bin/python"}, + want: "/usr/local/bin", + }, + { + name: "Multiple directories with common prefix", + inserted: []string{"/usr/local/bin/", "/usr/local/lib/", "/usr/include/", "/usr/local/bin/python"}, + want: "/usr", + }, + { + name: "No common prefix", + inserted: []string{"/home/user/", "/var/log/", "/tmp/", "/etc/passwd"}, + want: "", + }, + { + name: "Reverse path match", + inserted: []string{"bin", "lib", "include", "include/lib/bin"}, + want: "", + }, + { + name: "Longer input than stored", + inserted: []string{"/short/", "/shorter/path"}, + want: "", + }, + { + name: "No match", + inserted: []string{"/apple/", "/banana/", "/cherry/"}, + want: "", + }, + { + name: "Partial reverse match", + inserted: []string{"bin", "lib", "include", "lib/bin"}, + want: "", + }, + { + name: "normal case 1", + inserted: []string{ + "/opt/homebrew/Cellar/cjson/1.7.18/include/cJSON.h", + "/opt/homebrew/Cellar/cjson/1.7.18/include/zlib/zlib.h", + "/opt/homebrew/Cellar/cjson/1.7.18/include/cJSON/cJSON.h", + }, + want: "/opt/homebrew/Cellar/cjson/1.7.18/include", + }, + { + name: "absolute path case 1", + inserted: []string{"/usr", "usr", "/usr/include", "/usr"}, + want: "", + }, + { + name: "absolute path case 2", + inserted: []string{"usr/share", "/usr", "usr/include", "usr/include/share"}, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + trie := header.NewTrie() + for _, word := range tt.inserted { + trie.Insert(word) + } + result := trie.LongestPrefix() + if result != tt.want { + t.Errorf("LongestPrefix(%q) = %q, want %q", tt.inserted, result, tt.want) + } + }) + } +} + +func TestTrieReverse(t *testing.T) { + testCases := []struct { + name string + search string + inserted []string + want bool + }{ + { + name: "empty string", + search: "abc", + want: false, + }, + { + name: "input empty string", + search: "", + inserted: []string{""}, + want: false, + }, + { + name: "one string", + search: "/a", + inserted: []string{"/a"}, + want: true, + }, + { + name: "two string", + search: "/a", + inserted: []string{"/a", "/b"}, + want: true, + }, + { + name: "multiple string case 1", + search: "/c", + inserted: []string{"/a", "/b", "/d"}, + want: false, + }, + + { + name: "multiple string case 2", + search: "", + inserted: []string{"/a", "/b", "/d"}, + want: false, + }, + + { + name: "multiple string case 3", + search: "/c", + inserted: []string{"/a/c", "/b/c", "/c/d"}, + want: false, + }, + + { + name: "multiple string case 4", + search: "c/d", + inserted: []string{"/a/c/d", "/b/c/d", "/c/d/a"}, + want: true, + }, + + { + name: "multiple string case 5", + search: "c", + inserted: []string{"/a/c", "/b/c", "/c/d"}, + want: true, + }, + { + name: "substring string case 1", + search: "/a/b", + inserted: []string{"/a"}, + want: false, + }, + { + name: "substring string case 2", + search: "b", + inserted: []string{"/a/b"}, + want: true, + }, + + { + name: "substring string case 3", + search: "/a/b", + inserted: []string{"/a/b", "/a/b/c"}, + want: true, + }, + + { + name: "normal case 1", + search: "libxslt/variables.h", + inserted: []string{ + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/libxslt/imports.h", + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/libxslt/xsltexports.h", + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/libxslt/variables.h", + }, + want: true, + }, + + { + name: "normal case 2", + search: "libxslt/c14n.h", + inserted: []string{ + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/libxslt/imports.h", + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/libxslt/xsltexports.h", + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/libxslt/variables.h", + }, + want: false, + }, + + { + name: "normal case 3", + search: "libxslt/imports.h", + inserted: []string{ + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/zlib/imports.h", + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/libxml2/imports.h", + + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/libxslt/xsltexports.h", + "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include/libxslt/variables.h", + }, + want: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + trie := header.NewTrie(header.WithReversePathSegmenter()) + + for _, i := range tc.inserted { + trie.Insert(i) + } + if got := trie.IsOnSameBranch(tc.search); got != tc.want { + t.Fatalf("unexpected result: want %v got %v", tc.want, got) + } + }) + } +}