chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user