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
136 lines
3.4 KiB
Go
136 lines
3.4 KiB
Go
package resolver
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// TestResolver_ConcurrentResolveFile guards against the daemon-crashing
|
|
// "concurrent map writes" panic in buildDirIndexes — two file-watcher
|
|
// debounce goroutines firing on the same per-repo Indexer both call
|
|
// Resolver.ResolveFile, both reset the dirIndex/lastDirIndex fields,
|
|
// fatal-error the runtime. Run under `go test -race` for full
|
|
// detection; the runtime fatal still triggers without -race when the
|
|
// scheduler interleaves the resets exactly.
|
|
func TestResolver_ConcurrentResolveFile(t *testing.T) {
|
|
g := buildSmallGraph(t)
|
|
r := New(g)
|
|
|
|
const goroutines = 16
|
|
const itersEach = 50
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(goroutines)
|
|
for i := 0; i < goroutines; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < itersEach; j++ {
|
|
_ = r.ResolveFile("a.go")
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
// TestCrossRepoResolver_ConcurrentResolveForRepo locks in the same
|
|
// guarantee for the multi-repo resolver. MultiWatcher fires per-repo,
|
|
// so concurrent ResolveForRepo calls on different prefixes are normal
|
|
// and must not race on the shared dirIndex maps.
|
|
func TestCrossRepoResolver_ConcurrentResolveForRepo(t *testing.T) {
|
|
g := buildSmallGraph(t)
|
|
cr := NewCrossRepo(g)
|
|
|
|
const goroutines = 16
|
|
const itersEach = 50
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(goroutines)
|
|
for i := 0; i < goroutines; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < itersEach; j++ {
|
|
_ = cr.ResolveForRepo("repo-a")
|
|
_ = cr.ResolveAll()
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
// TestResolver_CrossRepoResolver_SerializeOnGraph pins the cross-type
|
|
// race reported in the daemon: the per-repo Watcher's debounce timer
|
|
// fires Resolver.ResolveFile (which holds g.ResolveMutex) while
|
|
// MultiWatcher.forwardEvents fires CrossRepoResolver.ResolveForRepo.
|
|
// Both iterate graph.AllEdges()/AllNodes() and rewrite Edge.To in
|
|
// place on the shared graph, so they must share the same lock — not
|
|
// two different ones. Without the shared mu pointer, `go test -race`
|
|
// flags edge mutations between the two resolver types.
|
|
func TestResolver_CrossRepoResolver_SerializeOnGraph(t *testing.T) {
|
|
g := buildSmallGraph(t)
|
|
r := New(g)
|
|
cr := NewCrossRepo(g)
|
|
|
|
const iters = 200
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < iters; i++ {
|
|
_ = r.ResolveFile("a.go")
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < iters; i++ {
|
|
_ = cr.ResolveForRepo("repo-a")
|
|
}
|
|
}()
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
// buildSmallGraph populates a graph with a handful of file nodes plus
|
|
// one unresolved edge so the resolver actually has work to do during
|
|
// the race test. The shape doesn't matter — only that buildDirIndexes
|
|
// observes >0 file nodes and the resolveEdge inner loop runs.
|
|
func buildSmallGraph(t *testing.T) graph.Store {
|
|
t.Helper()
|
|
g := graph.New()
|
|
for _, fp := range []string{"repo-a/lib/a.go", "repo-a/lib/b.go", "repo-b/main.go"} {
|
|
g.AddNode(&graph.Node{
|
|
ID: fp,
|
|
Kind: graph.KindFile,
|
|
Name: fp,
|
|
FilePath: fp,
|
|
RepoPrefix: firstSegment(fp),
|
|
})
|
|
}
|
|
g.AddNode(&graph.Node{
|
|
ID: "a.go",
|
|
Kind: graph.KindFunction,
|
|
Name: "Foo",
|
|
FilePath: "a.go",
|
|
RepoPrefix: "repo-a",
|
|
})
|
|
g.AddEdge(&graph.Edge{
|
|
From: "a.go",
|
|
To: "unresolved::Bar",
|
|
Kind: graph.EdgeCalls,
|
|
})
|
|
return g
|
|
}
|
|
|
|
func firstSegment(p string) string {
|
|
for i, c := range p {
|
|
if c == '/' {
|
|
return p[:i]
|
|
}
|
|
}
|
|
return p
|
|
}
|