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
103 lines
3.4 KiB
Go
103 lines
3.4 KiB
Go
package resolver
|
|
|
|
import (
|
|
"fmt"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// TestCrossRepoResolveAll_ConcurrentEdits is the safety gate for the chunked
|
|
// resolve. It runs CrossRepoResolver.ResolveAll while an "editor" goroutine
|
|
// repeatedly evicts and re-indexes caller files, taking the SAME resolve mutex
|
|
// an interactive single-file edit takes — exactly the interleaving the chunked
|
|
// path enables. Without the resolveEdge liveness guards this corrupts the graph
|
|
// (ReindexEdge half-resurrects an evicted edge and later panics with an
|
|
// index-out-of-range during eviction); with them the run is clean and every
|
|
// resolved edge points at a live node.
|
|
//
|
|
// Run with -race -count=N for scheduling variation.
|
|
func TestCrossRepoResolveAll_ConcurrentEdits(t *testing.T) {
|
|
const (
|
|
files = 40
|
|
callersPerFile = 600 // 24000 pending -> ~12 chunks at the default 2048
|
|
)
|
|
g := graph.New()
|
|
|
|
callerFile := func(k int) string { return fmt.Sprintf("repoA/a%d.go", k) }
|
|
|
|
// repoB targets: one Helper per (file,caller) slot.
|
|
for k := 0; k < files; k++ {
|
|
for i := 0; i < callersPerFile; i++ {
|
|
id := fmt.Sprintf("repoB/c%d_%d.go::Helper%d_%d", k, i, k, i)
|
|
g.AddNode(&graph.Node{
|
|
ID: id, Kind: graph.KindFunction, Name: fmt.Sprintf("Helper%d_%d", k, i),
|
|
FilePath: fmt.Sprintf("repoB/c%d_%d.go", k, i), Language: "go", RepoPrefix: "repoB",
|
|
})
|
|
}
|
|
}
|
|
|
|
// addCallerFile (re)indexes one repoA caller file: its caller functions, the
|
|
// unresolved call edges into repoB, and the import-reachability evidence.
|
|
addCallerFile := func(k int) {
|
|
for i := 0; i < callersPerFile; i++ {
|
|
g.AddNode(&graph.Node{
|
|
ID: fmt.Sprintf("%s::Caller%d_%d", callerFile(k), k, i), Kind: graph.KindFunction,
|
|
Name: fmt.Sprintf("Caller%d_%d", k, i), FilePath: callerFile(k), Language: "go", RepoPrefix: "repoA",
|
|
})
|
|
g.AddEdge(&graph.Edge{
|
|
From: fmt.Sprintf("%s::Caller%d_%d", callerFile(k), k, i),
|
|
To: fmt.Sprintf("unresolved::Helper%d_%d", k, i),
|
|
Kind: graph.EdgeCalls, FilePath: callerFile(k), Line: i + 1,
|
|
})
|
|
}
|
|
wireImport(g, callerFile(k), "repoB", fmt.Sprintf("repoB/c%d_0.go", k))
|
|
}
|
|
for k := 0; k < files; k++ {
|
|
addCallerFile(k)
|
|
}
|
|
|
|
var resolveDone atomic.Bool
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
cr := NewCrossRepo(g)
|
|
cr.ResolveAll()
|
|
resolveDone.Store(true)
|
|
}()
|
|
|
|
// Editor: between ResolveAll's chunks (when it yields the resolve mutex),
|
|
// re-index a caller file — evict it (drops its nodes + the call edges the
|
|
// resolver's pending snapshot still points at) and add it back. Takes the
|
|
// resolve mutex, exactly as an interactive edit's ResolveFileAndIncoming does.
|
|
mu := g.ResolveMutex()
|
|
var edits int
|
|
for k := 0; !resolveDone.Load(); k = (k + 1) % files {
|
|
mu.Lock()
|
|
g.EvictFile(callerFile(k))
|
|
addCallerFile(k)
|
|
mu.Unlock()
|
|
edits++
|
|
runtime.Gosched()
|
|
}
|
|
wg.Wait()
|
|
|
|
require.Greater(t, edits, 0, "editor never interleaved — increase the work size")
|
|
|
|
// No resolved edge may point at a missing node: that is the dangling-edge /
|
|
// half-resurrection signature the guards exist to prevent.
|
|
for _, e := range g.AllEdges() {
|
|
if e == nil || strings.HasPrefix(e.To, "unresolved::") || isSyntheticResolveTarget(e.To) {
|
|
continue
|
|
}
|
|
require.NotNilf(t, g.GetNode(e.To),
|
|
"edge %s -> %s resolved to a node not in the graph (dangling)", e.From, e.To)
|
|
}
|
|
}
|