项目文件夹

文件
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

246 行
8.5 KiB
Go

package indexer
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/excludes"
"github.com/zzet/gortex/internal/graph"
)
// TestIncrementalReindex_PreservesExcludedFiles is the regression test for
// the deletion-classification bug: a file that's tracked in fileMtimes but
// absent from the discovery walk must NOT be purged unless it's truly gone
// from disk. Otherwise an exclude-list change between passes silently
// destroys legitimate graph state.
func TestIncrementalReindex_PreservesExcludedFiles(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "keep.go"), `package main
func Kept() {}
`)
writeFile(t, filepath.Join(dir, "drop.go"), `package main
func Dropped() {}
`)
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
require.NotEmpty(t, g.FindNodesByName("Kept"))
require.NotEmpty(t, g.FindNodesByName("Dropped"))
// Mid-flight exclusion change: future discoveries will not visit
// drop.go, but it's still on disk. The old code would treat it as
// deleted and purge its nodes; the new code stats it and preserves.
idx.config.Exclude = append(append([]string{}, excludes.Builtin...), "drop.go")
idx.excludes = nil
idx.excludeOnce = sync.Once{}
_, err = idx.IncrementalReindex(dir)
require.NoError(t, err)
assert.NotEmpty(t, g.FindNodesByName("Kept"), "kept.go was not excluded; nodes must survive")
assert.NotEmpty(t, g.FindNodesByName("Dropped"), "drop.go was excluded but still on disk; nodes must be preserved")
}
// TestIncrementalReindex_EvictsTrulyDeletedFiles is the control case: a
// file that has actually been removed from disk should still be evicted.
func TestIncrementalReindex_EvictsTrulyDeletedFiles(t *testing.T) {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "keep.go"), `package main
func Kept() {}
`)
gonePath := filepath.Join(dir, "gone.go")
writeFile(t, gonePath, `package main
func Gone() {}
`)
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
require.NotEmpty(t, g.FindNodesByName("Gone"))
require.NoError(t, os.Remove(gonePath))
_, err = idx.IncrementalReindex(dir)
require.NoError(t, err)
assert.NotEmpty(t, g.FindNodesByName("Kept"))
assert.Empty(t, g.FindNodesByName("Gone"), "gone.go was deleted from disk; nodes must be evicted")
}
// canonicalGraph renders a graph as a deterministic, sorted projection
// of its structural identity (node identities + edge triples). Two
// graphs with an equal projection are byte-identical for every query
// the engine can answer.
func canonicalGraph(g graph.Store) string {
var lines []string
for _, n := range g.AllNodes() {
if n == nil {
continue
}
lines = append(lines, fmt.Sprintf("N|%s|%s|%s|%s|%d|%d|%s",
n.ID, n.Kind, n.Name, n.FilePath, n.StartLine, n.EndLine, n.Language))
}
for _, e := range g.AllEdges() {
if e == nil {
continue
}
lines = append(lines, fmt.Sprintf("E|%s|%s|%s", e.From, e.To, e.Kind))
}
sort.Strings(lines)
return strings.Join(lines, "\n")
}
// bumpMtime rewrites a file and pushes its mtime forward so the
// mtime-keyed staleness check always classifies it as changed,
// regardless of filesystem timestamp resolution.
func bumpMtime(t *testing.T, path, content string) {
t.Helper()
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
future := time.Now().Add(2 * time.Second)
require.NoError(t, os.Chtimes(path, future, future))
}
// TestIncrementalReindex_ConvergesToFullIndex is the consistency
// invariant: a graph built incrementally — a cold index followed by
// per-file edits each reconciled with IncrementalReindex — must equal
// a single cold index of the same final disk state. Incremental
// reindex that drifted from a full index would silently serve a stale
// or wrong graph.
func TestIncrementalReindex_ConvergesToFullIndex(t *testing.T) {
build := func(dir string) {
require.NoError(t, os.MkdirAll(filepath.Join(dir, "pkg"), 0o755))
writeFile(t, filepath.Join(dir, "main.go"),
"package main\n\nfunc main() { helper() }\n\nfunc helper() {}\n")
writeFile(t, filepath.Join(dir, "pkg", "util.go"),
"package pkg\n\ntype Config struct{ Port int }\n\nfunc New() *Config { return &Config{} }\n")
writeFile(t, filepath.Join(dir, "extra.go"),
"package main\n\nfunc Extra() {}\n")
}
// Path A: incremental — a cold index, then a sequence of edits
// each reconciled with IncrementalReindex.
dir := t.TempDir()
build(dir)
gA := graph.New()
idxA := newTestIndexer(gA)
_, err := idxA.Index(dir)
require.NoError(t, err)
bumpMtime(t, filepath.Join(dir, "main.go"),
"package main\n\nfunc main() { helper(); helper() }\n\nfunc helper() {}\n")
_, err = idxA.IncrementalReindex(dir)
require.NoError(t, err)
bumpMtime(t, filepath.Join(dir, "pkg", "util.go"),
"package pkg\n\ntype Config struct{ Port int }\n\nfunc New() *Config { return &Config{} }\n\nfunc Reset(c *Config) {}\n")
_, err = idxA.IncrementalReindex(dir)
require.NoError(t, err)
require.NoError(t, os.Remove(filepath.Join(dir, "extra.go")))
_, err = idxA.IncrementalReindex(dir)
require.NoError(t, err)
// Path B: a single cold index of the same final disk state.
gB := graph.New()
idxB := newTestIndexer(gB)
_, err = idxB.Index(dir)
require.NoError(t, err)
assert.Equal(t, canonicalGraph(gB), canonicalGraph(gA),
"incremental reindex must converge to the same graph as a full index")
}
// TestIncrementalReindex_FailedFileSurfacedAndRetried checks the
// failed-chunk replay surface: a stale file that cannot be indexed is
// reported on IndexResult.FailedFiles (after one in-pass retry), its
// mtime is left unrecorded so it stays stale, and a later pass
// recovers it once the obstruction clears.
func TestIncrementalReindex_FailedFileSurfacedAndRetried(t *testing.T) {
if os.Geteuid() == 0 {
t.Skip("an unreadable-file test is meaningless as root")
}
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "ok.go"), "package main\n\nfunc OK() {}\n")
bad := filepath.Join(dir, "bad.go")
writeFile(t, bad, "package main\n\nfunc Bad() {}\n")
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
require.NotEmpty(t, g.FindNodesByName("Bad"))
// Make bad.go unreadable and stale: the incremental pass discovers
// it (stat works) but fails to read its content.
require.NoError(t, os.Chmod(bad, 0o000))
t.Cleanup(func() { _ = os.Chmod(bad, 0o644) })
future := time.Now().Add(2 * time.Second)
require.NoError(t, os.Chtimes(bad, future, future))
res, err := idx.IncrementalReindex(dir)
require.NoError(t, err)
assert.Contains(t, res.FailedFiles, bad,
"an unreadable stale file must be surfaced on FailedFiles")
// Readable again: the file is still stale (its failed pass never
// recorded an mtime), so the next incremental pass recovers it.
require.NoError(t, os.Chmod(bad, 0o644))
res2, err := idx.IncrementalReindex(dir)
require.NoError(t, err)
assert.Empty(t, res2.FailedFiles, "the file indexes cleanly once readable")
assert.NotEmpty(t, g.FindNodesByName("Bad"))
}
// TestIncrementalReindex_MerkleMode exercises the BLAKE3 Merkle change
// detector: a content edit is re-indexed, but a file merely touched
// (new mtime, identical content) is not — the content-addressed tree
// ignores the mtime false positive that the bare-mtime path would
// re-index needlessly.
func TestIncrementalReindex_MerkleMode(t *testing.T) {
t.Setenv("GORTEX_MERKLE", "1")
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "edited.go"), "package main\n\nfunc Edited() {}\n")
writeFile(t, filepath.Join(dir, "touched.go"), "package main\n\nfunc Touched() {}\n")
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
require.NotEmpty(t, g.FindNodesByName("Edited"))
require.NotEmpty(t, g.FindNodesByName("Touched"))
require.FileExists(t, filepath.Join(dir, ".gortex", "merkle.json"),
"a full index in Merkle mode must persist a baseline tree")
// Edit one file's content; touch the other without changing it.
bumpMtime(t, filepath.Join(dir, "edited.go"),
"package main\n\nfunc Edited() {}\n\nfunc AlsoEdited() {}\n")
future := time.Now().Add(2 * time.Second)
require.NoError(t, os.Chtimes(filepath.Join(dir, "touched.go"), future, future))
res, err := idx.IncrementalReindex(dir)
require.NoError(t, err)
assert.NotEmpty(t, g.FindNodesByName("AlsoEdited"),
"a content edit must be re-indexed under Merkle mode")
assert.Equal(t, 1, res.StaleFileCount,
"only the content-changed file is stale; a bare touch is not")
}