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

240 lines
8.7 KiB
Go

package graph
import "testing"
// addMethodNode / addTypeNode / addFieldNode / addEdge are tiny
// builders so each test case can pin exactly which nodes and edges
// exist. The classifier is graph-only, so a hand-built graph is the
// right fixture — no parser in the loop.
func addNode(g *Graph, id string, kind NodeKind, meta map[string]any) {
g.AddNode(&Node{ID: id, Kind: kind, Name: shortName(id), FilePath: "x.go", Meta: meta})
}
func shortName(id string) string {
for i := len(id) - 1; i >= 0; i-- {
if id[i] == '.' || id[i] == ':' {
return id[i+1:]
}
}
return id
}
func addCEdge(g *Graph, from, to string, kind EdgeKind, meta map[string]any) {
g.AddEdge(&Edge{From: from, To: to, Kind: kind, FilePath: "x.go", Origin: OriginASTResolved, Meta: meta})
}
func TestIsLockTypeName(t *testing.T) {
cases := []struct {
name string
in string
want bool
}{
{"go mutex", "sync.Mutex", true},
{"go rwmutex", "sync.RWMutex", true},
{"go pointer mutex", "*sync.Mutex", true},
{"go embedded mutex", "Mutex", true},
{"rust mutex generic", "Mutex<State>", true},
{"rust qualified rwlock", "std::sync::RwLock<Data>", true},
{"rust ref mutex", "&Mutex<u32>", true},
{"java reentrant lock", "ReentrantLock", true},
{"java qualified rwlock", "java.util.concurrent.locks.ReentrantReadWriteLock", true},
{"java lock iface", "Lock", true},
{"csharp semaphoreslim", "SemaphoreSlim", true},
{"csharp rwlockslim", "ReaderWriterLockSlim", true},
{"plain int not a lock", "int", false},
{"string not a lock", "string", false},
{"map not a lock", "map[string]int", false},
{"empty", "", false},
{"unrelated struct", "bytes.Buffer", false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := isLockTypeName(tc.in); got != tc.want {
t.Errorf("isLockTypeName(%q) = %v, want %v", tc.in, got, tc.want)
}
})
}
}
// TestClassifyConcurrency_SyncGuarded is the table-driven check that a
// method on a mutex-holding type is flagged sync_guarded and a method
// on a lock-free type is not.
func TestClassifyConcurrency_SyncGuarded(t *testing.T) {
cases := []struct {
name string
fieldType string // declared type of the receiver type's single field
fieldKind NodeKind
want bool
}{
{"go mutex field", "sync.Mutex", KindField, true},
{"go rwmutex field", "sync.RWMutex", KindField, true},
{"go embedded mutex", "Mutex", KindField, true},
{"rust mutex field", "Mutex<State>", KindField, true},
{"java reentrant lock field", "ReentrantLock", KindField, true},
{"lock-free int field", "int", KindField, false},
{"lock-free string field", "string", KindField, false},
// A class property modelled as KindVariable (TS / PHP) is not
// a KindField, so it must not be picked up — sync_guarded is
// honestly not reported for those languages.
{"variable-modelled property ignored", "Mutex", KindVariable, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
g := New()
addNode(g, "p.go::Store", KindType, nil)
addNode(g, "p.go::Store.Get", KindMethod, nil)
addCEdge(g, "p.go::Store.Get", "p.go::Store", EdgeMemberOf, nil)
addNode(g, "p.go::Store.f", tc.fieldKind, map[string]any{"field_type": tc.fieldType})
addCEdge(g, "p.go::Store.f", "p.go::Store", EdgeMemberOf, nil)
ann := ClassifyConcurrency(g, "p.go::Store.Get")
if ann.SyncGuarded != tc.want {
t.Errorf("SyncGuarded = %v, want %v", ann.SyncGuarded, tc.want)
}
if tc.want && ann.SyncGuardedWhy == "" {
t.Error("SyncGuarded true but SyncGuardedWhy is empty")
}
if !tc.want && ann.SyncGuardedWhy != "" {
t.Errorf("SyncGuarded false but SyncGuardedWhy = %q", ann.SyncGuardedWhy)
}
})
}
}
// TestClassifyConcurrency_PlainFunctionNeverGuarded confirms a free
// function (no receiver type) is never flagged sync_guarded.
func TestClassifyConcurrency_PlainFunctionNeverGuarded(t *testing.T) {
g := New()
addNode(g, "p.go::doWork", KindFunction, nil)
ann := ClassifyConcurrency(g, "p.go::doWork")
if ann.SyncGuarded {
t.Error("a plain function must not be sync_guarded")
}
}
// TestClassifyConcurrency_CrossConcurrent checks that a symbol that is
// the target of an EdgeSpawns edge is flagged cross_concurrent and a
// plainly-called symbol is not.
func TestClassifyConcurrency_CrossConcurrent(t *testing.T) {
cases := []struct {
name string
spawnMode string // "" means no spawn edge — plain call
spawned bool
want bool
wantWord string // substring expected in the explanation
}{
{"goroutine spawn", "goroutine", true, true, "goroutine"},
{"async spawn", "async", true, true, "async"},
{"promise spawn", "promise", true, true, "promise"},
{"worker pool spawn", "worker_pool", true, true, "worker pool"},
{"spawn without mode meta", "", true, true, "spawned"},
{"plain call only", "", false, false, ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
g := New()
addNode(g, "p.go::Caller", KindFunction, nil)
addNode(g, "p.go::Target", KindFunction, nil)
// A plain call edge is always present; it must never on
// its own make the target cross_concurrent.
addCEdge(g, "p.go::Caller", "p.go::Target", EdgeCalls, nil)
if tc.spawned {
var meta map[string]any
if tc.spawnMode != "" {
meta = map[string]any{"mode": tc.spawnMode}
}
addCEdge(g, "p.go::Caller", "p.go::Target", EdgeSpawns, meta)
}
ann := ClassifyConcurrency(g, "p.go::Target")
if ann.CrossConcurrent != tc.want {
t.Errorf("CrossConcurrent = %v, want %v", ann.CrossConcurrent, tc.want)
}
if tc.want {
if ann.CrossConcurrentWhy == "" {
t.Error("CrossConcurrent true but explanation empty")
}
if tc.wantWord != "" && !contains(ann.CrossConcurrentWhy, tc.wantWord) {
t.Errorf("explanation %q missing %q", ann.CrossConcurrentWhy, tc.wantWord)
}
} else if ann.CrossConcurrentWhy != "" {
t.Errorf("CrossConcurrent false but explanation = %q", ann.CrossConcurrentWhy)
}
})
}
}
// TestClassifyConcurrency_ClosureInheritsReceiverType verifies that a
// closure spawned inside a method resolves to the method's receiver
// type, so a goroutine-launched closure on a mutex-holding type is
// flagged sync_guarded.
func TestClassifyConcurrency_ClosureInheritsReceiverType(t *testing.T) {
g := New()
addNode(g, "p.go::Store", KindType, nil)
addNode(g, "p.go::Store.mu", KindField, map[string]any{"field_type": "sync.Mutex"})
addCEdge(g, "p.go::Store.mu", "p.go::Store", EdgeMemberOf, nil)
addNode(g, "p.go::Store.Run", KindMethod, nil)
addCEdge(g, "p.go::Store.Run", "p.go::Store", EdgeMemberOf, nil)
// Closure defined inside the method; member_of points at the method.
addNode(g, "p.go::Store.Run#closure@10", KindClosure, nil)
addCEdge(g, "p.go::Store.Run#closure@10", "p.go::Store.Run", EdgeMemberOf, nil)
// The method launches the closure as a goroutine.
addCEdge(g, "p.go::Store.Run", "p.go::Store.Run#closure@10", EdgeSpawns,
map[string]any{"mode": "goroutine"})
ann := ClassifyConcurrency(g, "p.go::Store.Run#closure@10")
if !ann.SyncGuarded {
t.Error("closure on a mutex-holding receiver type must be sync_guarded")
}
if !ann.CrossConcurrent {
t.Error("goroutine-launched closure must be cross_concurrent")
}
}
// TestClassifyConcurrency_BothFlags confirms the two flags are
// independent — a goroutine-launched method on a mutex type carries
// both.
func TestClassifyConcurrency_BothFlags(t *testing.T) {
g := New()
addNode(g, "p.go::Cache", KindType, nil)
addNode(g, "p.go::Cache.lock", KindField, map[string]any{"field_type": "sync.RWMutex"})
addCEdge(g, "p.go::Cache.lock", "p.go::Cache", EdgeMemberOf, nil)
addNode(g, "p.go::Cache.refresh", KindMethod, nil)
addCEdge(g, "p.go::Cache.refresh", "p.go::Cache", EdgeMemberOf, nil)
addNode(g, "p.go::Manager.Start", KindMethod, nil)
addCEdge(g, "p.go::Manager.Start", "p.go::Cache.refresh", EdgeSpawns,
map[string]any{"mode": "goroutine"})
ann := ClassifyConcurrency(g, "p.go::Cache.refresh")
if !ann.SyncGuarded || !ann.CrossConcurrent {
t.Errorf("expected both flags set, got sync_guarded=%v cross_concurrent=%v",
ann.SyncGuarded, ann.CrossConcurrent)
}
if !ann.Any() {
t.Error("Any() must be true when flags are set")
}
}
// TestClassifyConcurrency_MissingNode returns a zero-value annotation
// for an unknown node, and a nil reader is also tolerated.
func TestClassifyConcurrency_MissingNode(t *testing.T) {
g := New()
if ann := ClassifyConcurrency(g, "p.go::nope"); ann.Any() {
t.Error("unknown node must yield a zero-value annotation")
}
if ann := ClassifyConcurrency(nil, "p.go::nope"); ann.Any() {
t.Error("nil reader must yield a zero-value annotation")
}
}
func contains(haystack, needle string) bool {
if needle == "" {
return true
}
for i := 0; i+len(needle) <= len(haystack); i++ {
if haystack[i:i+len(needle)] == needle {
return true
}
}
return false
}