Files
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

264 lines
10 KiB
Go

package indexer
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"github.com/zzet/gortex/internal/config"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/graph/store_sqlite"
"github.com/zzet/gortex/internal/parser"
"github.com/zzet/gortex/internal/parser/languages"
)
// newSQLiteIndexer builds an indexer over a sqlite-backed graph, the
// configuration the affected-by pass's persisted reverse lookup runs on.
func newSQLiteIndexer(t *testing.T) (*Indexer, *store_sqlite.Store) {
t.Helper()
store, err := store_sqlite.Open(filepath.Join(t.TempDir(), "g.sqlite"))
require.NoError(t, err)
t.Cleanup(func() { _ = store.Close() })
reg := parser.NewRegistry()
languages.RegisterAll(reg)
cfg := config.Default().Index
cfg.Workers = 1
return New(store, reg, cfg, zap.NewNop()), store
}
// TestAffectedBy_SignatureChange_ReresolvesCaller is the headline case
// on the in-memory backend (whose reverse lookup is the pre-evict
// in-edge snapshot): b.go calls F defined in a.go; changing F's
// SIGNATURE re-resolves b.go — its call edge lands on the fresh F node
// and exactly one bounded affected-by pass ran over exactly one file.
func TestAffectedBy_SignatureChange_ReresolvesCaller(t *testing.T) {
dir := t.TempDir()
aPath := filepath.Join(dir, "a.go")
bPath := filepath.Join(dir, "b.go")
writeFile(t, aPath, "package p\n\nfunc F(x int) int { return x }\n")
writeFile(t, bPath, "package p\n\nfunc Caller() int { return F(1) }\n")
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
fID := fnNodeID(t, g, "a.go", "F")
callerID := fnNodeID(t, g, "b.go", "Caller")
require.Equal(t, fID, callTargetFrom(t, g, callerID),
"baseline: Caller's call must resolve to F")
bumpMtime(t, aPath, "package p\n\nfunc F(x int, y int) int { return x + y }\n")
_, err = idx.IncrementalReindexPaths(dir, []string{aPath})
require.NoError(t, err)
newFID := fnNodeID(t, g, "a.go", "F")
assert.Equal(t, newFID, callTargetFrom(t, g, callerID),
"after F's signature changed, Caller's edge must be re-resolved to the fresh F")
passes, files, dropped := idx.AffectedByCounts()
assert.Equal(t, int64(1), passes, "a signature change must trigger exactly one affected-by pass")
assert.Equal(t, int64(1), files, "the pass must re-resolve exactly the one referencing file")
assert.Equal(t, int64(0), dropped)
}
// TestAffectedBy_BodyOnlyEdit_NoFanout proves the gate: an edit that
// changes only a function BODY produces no signature delta and must not
// fan out — the whole point of delta detection is that the common case
// (a body edit) costs nothing beyond the changed file itself. Driven
// through whole-root IncrementalReindex to cover that sync route too.
func TestAffectedBy_BodyOnlyEdit_NoFanout(t *testing.T) {
dir := t.TempDir()
aPath := filepath.Join(dir, "a.go")
writeFile(t, aPath, "package p\n\nfunc F(x int) int { return x }\n")
writeFile(t, filepath.Join(dir, "b.go"), "package p\n\nfunc Caller() int { return F(1) }\n")
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
callerID := fnNodeID(t, g, "b.go", "Caller")
bumpMtime(t, aPath, "package p\n\nfunc F(x int) int { return x + 1 }\n")
res, err := idx.IncrementalReindex(dir)
require.NoError(t, err)
require.Equal(t, 1, res.StaleFileCount)
passes, files, _ := idx.AffectedByCounts()
assert.Equal(t, int64(0), passes, "a body-only edit must not trigger an affected-by pass")
assert.Equal(t, int64(0), files)
// The caller's edge still survives the definition re-index via the
// existing restub + reverse-resolve pair.
assert.Equal(t, fnNodeID(t, g, "a.go", "F"), callTargetFrom(t, g, callerID),
"the caller edge must still be bound after a body-only re-index")
}
// TestAffectedBy_PerSaveIndexFile_ReresolvesCaller drives the same
// signature change through IndexFile directly — the watcher's per-save
// patch path — proving every sync route shares the hook.
func TestAffectedBy_PerSaveIndexFile_ReresolvesCaller(t *testing.T) {
dir := t.TempDir()
aPath := filepath.Join(dir, "a.go")
writeFile(t, aPath, "package p\n\nfunc F() int { return 0 }\n")
writeFile(t, filepath.Join(dir, "b.go"), "package p\n\nfunc Caller() int { return F() }\n")
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
callerID := fnNodeID(t, g, "b.go", "Caller")
writeFile(t, aPath, "package p\n\nfunc F(n int) int { return n }\n")
require.NoError(t, idx.IndexFile(aPath))
assert.Equal(t, fnNodeID(t, g, "a.go", "F"), callTargetFrom(t, g, callerID))
passes, files, _ := idx.AffectedByCounts()
assert.Equal(t, int64(1), passes, "the per-save IndexFile route must run the pass")
assert.Equal(t, int64(1), files)
}
// TestAffectedBy_RemovedSymbol_SQLite removes a called symbol from its
// definition file on a sqlite-backed graph: the caller's edge must
// degrade to the resolver's normal unresolved stub (no dangling old-ID
// edge), and the caller's now-stale persisted reference fact must be
// dropped by the pass's re-persist.
func TestAffectedBy_RemovedSymbol_SQLite(t *testing.T) {
dir := t.TempDir()
aPath := filepath.Join(dir, "a.go")
writeFile(t, aPath, "package p\n\nfunc F() {}\n\nfunc Keep() {}\n")
writeFile(t, filepath.Join(dir, "b.go"), "package p\n\nfunc Caller() { F() }\n")
idx, store := newSQLiteIndexer(t)
_, err := idx.Index(dir)
require.NoError(t, err)
idx.ResolveAll() // seeds the persisted ref-facts sidecar
g := idx.Graph()
fID := fnNodeID(t, g, "a.go", "F")
callerID := fnNodeID(t, g, "b.go", "Caller")
require.Equal(t, fID, callTargetFrom(t, g, callerID))
// The reverse lookup must already know b.go references F.
byFile, err := store.LoadRefFactsByTargets("", []string{fID})
require.NoError(t, err)
require.Contains(t, byFile, "b.go",
"the seeded sidecar must answer the by-target reverse lookup")
bumpMtime(t, aPath, "package p\n\nfunc Keep() {}\n")
_, err = idx.IncrementalReindexPaths(dir, []string{aPath})
require.NoError(t, err)
target := callTargetFrom(t, g, callerID)
assert.True(t, graph.IsUnresolvedTarget(target),
"the caller's edge must degrade to an unresolved stub, got %q", target)
assert.Equal(t, "F", graph.UnresolvedName(target))
facts, err := store.LoadRefFactsByFiles("", []string{"b.go"})
require.NoError(t, err)
for _, f := range facts {
assert.NotEqual(t, fID, f.ToID,
"the stale fact pointing at the removed symbol must be re-persisted away")
}
passes, _, _ := idx.AffectedByCounts()
assert.Equal(t, int64(1), passes)
}
// TestAffectedBy_SidecarDiscovery_SQLite proves the persisted reverse
// lookup is a real discovery source, not just a mirror of the live
// graph: the caller's in-edge is parked on an unresolved stub BEFORE
// the change (so the pre-evict snapshot sees no in-edge for F), and the
// affected file is still found — via LoadRefFactsByTargets — and
// re-resolved.
func TestAffectedBy_SidecarDiscovery_SQLite(t *testing.T) {
dir := t.TempDir()
aPath := filepath.Join(dir, "a.go")
writeFile(t, aPath, "package p\n\nfunc F() int { return 0 }\n")
writeFile(t, filepath.Join(dir, "b.go"), "package p\n\nfunc Caller() int { return F() }\n")
idx, _ := newSQLiteIndexer(t)
_, err := idx.Index(dir)
require.NoError(t, err)
idx.ResolveAll()
g := idx.Graph()
callerID := fnNodeID(t, g, "b.go", "Caller")
require.Equal(t, fnNodeID(t, g, "a.go", "F"), callTargetFrom(t, g, callerID))
// Park the caller's live edge on a stub — the state a prior evict
// leaves behind — so only the sidecar can name b.go as affected.
idx.restubIncomingRefs("a.go")
require.True(t, graph.IsUnresolvedTarget(callTargetFrom(t, g, callerID)),
"precondition: the live in-edge must be parked on a stub")
bumpMtime(t, aPath, "package p\n\nfunc F(n int) int { return n }\n")
_, err = idx.IncrementalReindexPaths(dir, []string{aPath})
require.NoError(t, err)
passes, files, _ := idx.AffectedByCounts()
assert.Equal(t, int64(1), passes,
"the pass must run even with no live in-edges — discovery comes from the sidecar")
assert.Equal(t, int64(1), files)
assert.Equal(t, fnNodeID(t, g, "a.go", "F"), callTargetFrom(t, g, callerID),
"the sidecar-discovered caller must be re-resolved to the fresh F")
}
// TestAffectedBy_CapBoundsFanout configures a fan-out cap of 1 with
// three referencing files: the pass must re-resolve exactly one file
// and account for the two it dropped — the cap is loud, not silent.
func TestAffectedBy_CapBoundsFanout(t *testing.T) {
dir := t.TempDir()
aPath := filepath.Join(dir, "a.go")
writeFile(t, aPath, "package p\n\nfunc F(x int) int { return x }\n")
writeFile(t, filepath.Join(dir, "b.go"), "package p\n\nfunc CallerB() int { return F(1) }\n")
writeFile(t, filepath.Join(dir, "c.go"), "package p\n\nfunc CallerC() int { return F(2) }\n")
writeFile(t, filepath.Join(dir, "d.go"), "package p\n\nfunc CallerD() int { return F(3) }\n")
g := graph.New()
reg := parser.NewRegistry()
reg.Register(languages.NewGoExtractor())
cfg := config.Default().Index
cfg.Workers = 1
cfg.AffectedByReresolveMax = 1
idx := New(g, reg, cfg, zap.NewNop())
_, err := idx.Index(dir)
require.NoError(t, err)
bumpMtime(t, aPath, "package p\n\nfunc F(x int, y int) int { return x + y }\n")
_, err = idx.IncrementalReindexPaths(dir, []string{aPath})
require.NoError(t, err)
passes, files, dropped := idx.AffectedByCounts()
assert.Equal(t, int64(1), passes)
assert.Equal(t, int64(1), files, "the cap must bound the re-resolve set")
assert.Equal(t, int64(2), dropped, "the truncated files must be accounted, not silently lost")
}
// TestAffectedBy_DeferredBatchPath_NoFanout proves the batch guard: a
// caller that defers global passes (warmup, ReconcileAll) runs one
// resolve at the end of the batch, so the per-file affected-by pass
// must stay off even for a genuine signature change.
func TestAffectedBy_DeferredBatchPath_NoFanout(t *testing.T) {
dir := t.TempDir()
aPath := filepath.Join(dir, "a.go")
writeFile(t, aPath, "package p\n\nfunc F(x int) int { return x }\n")
writeFile(t, filepath.Join(dir, "b.go"), "package p\n\nfunc Caller() int { return F(1) }\n")
g := graph.New()
idx := newTestIndexer(g)
_, err := idx.Index(dir)
require.NoError(t, err)
idx.SetDeferGlobalPasses(true)
bumpMtime(t, aPath, "package p\n\nfunc F(x int, y int) int { return x + y }\n")
require.NoError(t, idx.IndexFile(aPath))
passes, _, _ := idx.AffectedByCounts()
assert.Equal(t, int64(0), passes,
"deferred-batch indexing must not fan out per file — the batch caller resolves once at the end")
}