Files
zzet--gortex/internal/resolver/master_concurrent_test.go
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

89 lines
2.6 KiB
Go

package resolver
import (
"fmt"
"runtime"
"strings"
"sync"
"sync/atomic"
"testing"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
)
// TestResolveAll_ConcurrentEdits is the safety gate for the chunked master
// (same-repo) resolve. It runs Resolver.ResolveAll while an editor goroutine
// evicts and re-indexes caller files under the same resolve mutex an
// interactive edit takes — the interleaving the chunked path enables. Without
// the apply / guardCrossPackageCallEdges liveness guards this corrupts the
// graph (ReindexEdge half-resurrects an evicted edge and later panics with an
// index-out-of-range during eviction). Run with -race -count=N.
func TestResolveAll_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("r/a%d.go", k) }
// Same-repo targets, one unique Helper per (file,caller) slot.
for k := 0; k < files; k++ {
for i := 0; i < callersPerFile; i++ {
g.AddNode(&graph.Node{
ID: fmt.Sprintf("r/b%d_%d.go::Helper%d_%d", k, i, k, i), Kind: graph.KindFunction,
Name: fmt.Sprintf("Helper%d_%d", k, i), FilePath: fmt.Sprintf("r/b%d_%d.go", k, i),
Language: "go", RepoPrefix: "r",
})
}
}
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: "r",
})
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,
})
}
}
for k := 0; k < files; k++ {
addCallerFile(k)
}
var resolveDone atomic.Bool
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
New(g).ResolveAll()
resolveDone.Store(true)
}()
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")
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)
}
}