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
251 lines
11 KiB
Go
251 lines
11 KiB
Go
package resolver
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// grpcTestGraph is a builder for the minimal graph shape the
|
|
// ResolveGRPCStubCalls pass consumes: a client function with a
|
|
// grpc.stub call edge, and a server-side handler discoverable via
|
|
// registration and/or interface satisfaction.
|
|
type grpcTestGraph struct {
|
|
g graph.Store
|
|
}
|
|
|
|
func newGRPCTestGraph() *grpcTestGraph { return &grpcTestGraph{g: graph.New()} }
|
|
|
|
// addCaller adds a client-side function node.
|
|
func (b *grpcTestGraph) addCaller(id, filePath, repo string) {
|
|
b.g.AddNode(&graph.Node{ID: id, Kind: graph.KindFunction, Name: lastSeg(id), FilePath: filePath, RepoPrefix: repo})
|
|
}
|
|
|
|
// addStubCall adds the grpc.stub EdgeCalls placeholder edge from caller.
|
|
func (b *grpcTestGraph) addStubCall(callerID, service, method, filePath string) *graph.Edge {
|
|
e := &graph.Edge{
|
|
From: callerID, To: grpcStubPlaceholder(service, method),
|
|
Kind: graph.EdgeCalls, FilePath: filePath, Line: 10,
|
|
Meta: map[string]any{"via": "grpc.stub", "grpc_service": service, "grpc_method": method},
|
|
}
|
|
b.g.AddEdge(e)
|
|
return e
|
|
}
|
|
|
|
// addServerImpl adds a server impl type plus one method per name, wired
|
|
// with EdgeMemberOf. Returns the method node IDs keyed by name.
|
|
func (b *grpcTestGraph) addServerImpl(typeName, dir, repo string, methods ...string) map[string]string {
|
|
typeID := dir + "/impl.go::" + typeName
|
|
b.g.AddNode(&graph.Node{ID: typeID, Kind: graph.KindType, Name: typeName, FilePath: dir + "/impl.go", RepoPrefix: repo})
|
|
out := map[string]string{}
|
|
for _, m := range methods {
|
|
mid := dir + "/impl.go::" + typeName + "." + m
|
|
b.g.AddNode(&graph.Node{
|
|
ID: mid, Kind: graph.KindMethod, Name: m, FilePath: dir + "/impl.go", RepoPrefix: repo,
|
|
Meta: map[string]any{"receiver": typeName},
|
|
})
|
|
b.g.AddEdge(&graph.Edge{From: mid, To: typeID, Kind: graph.EdgeMemberOf, FilePath: dir + "/impl.go", Line: 1})
|
|
out[m] = mid
|
|
}
|
|
return out
|
|
}
|
|
|
|
// addRegistration adds the server-side registration call edge that
|
|
// names typeName as the impl for service.
|
|
func (b *grpcTestGraph) addRegistration(service, typeName, regFuncID, regFilePath, repo string) {
|
|
b.g.AddNode(&graph.Node{ID: regFuncID, Kind: graph.KindFunction, Name: lastSeg(regFuncID), FilePath: regFilePath, RepoPrefix: repo})
|
|
b.g.AddEdge(&graph.Edge{
|
|
From: regFuncID, To: "unresolved::extern::example.com/proto::Register" + service + "Server",
|
|
Kind: graph.EdgeCalls, FilePath: regFilePath, Line: 5,
|
|
Meta: map[string]any{"grpc_register_service": service, "grpc_register_impl": typeName},
|
|
})
|
|
}
|
|
|
|
// addServerInterface adds a `<Service>Server` interface and an
|
|
// EdgeImplements edge from the impl type to it.
|
|
func (b *grpcTestGraph) addServerInterface(service, implTypeID, ifaceDir, repo string) {
|
|
ifaceID := ifaceDir + "/grpc.pb.go::" + service + "Server"
|
|
b.g.AddNode(&graph.Node{ID: ifaceID, Kind: graph.KindInterface, Name: service + "Server", FilePath: ifaceDir + "/grpc.pb.go", RepoPrefix: repo})
|
|
b.g.AddEdge(&graph.Edge{From: implTypeID, To: ifaceID, Kind: graph.EdgeImplements, FilePath: ifaceDir + "/grpc.pb.go", Line: 1})
|
|
}
|
|
|
|
func lastSeg(id string) string {
|
|
for i := len(id) - 1; i >= 0; i-- {
|
|
if id[i] == ':' || id[i] == '.' || id[i] == '/' {
|
|
return id[i+1:]
|
|
}
|
|
}
|
|
return id
|
|
}
|
|
|
|
func TestResolveGRPCStubCalls_Registration(t *testing.T) {
|
|
b := newGRPCTestGraph()
|
|
b.addCaller("cli/main.go::run", "cli/main.go", "cli")
|
|
call := b.addStubCall("cli/main.go::run", "UserService", "GetUser", "cli/main.go")
|
|
methods := b.addServerImpl("userServer", "svc", "svc", "GetUser", "ListUsers")
|
|
b.addRegistration("UserService", "userServer", "svc/main.go::main", "svc/main.go", "svc")
|
|
|
|
resolved := ResolveGRPCStubCalls(b.g)
|
|
assert.Equal(t, 1, resolved)
|
|
assert.Equal(t, methods["GetUser"], call.To, "stub call must land on the registered handler")
|
|
assert.Equal(t, graph.OriginASTResolved, call.Origin)
|
|
assert.Equal(t, 0.9, call.Confidence)
|
|
assert.Equal(t, "EXTRACTED", call.ConfidenceLabel)
|
|
assert.Equal(t, graph.OriginASTResolved, call.Meta["grpc_resolution"])
|
|
|
|
// Edge buckets stay consistent after ReindexEdge.
|
|
assert.Equal(t, call, firstOutEdgeByKind(b.g, "cli/main.go::run", graph.EdgeCalls))
|
|
require.Len(t, b.g.GetInEdges(methods["GetUser"]), 1, "handler must see the inbound call edge")
|
|
}
|
|
|
|
func TestResolveGRPCStubCalls_InterfaceFallback(t *testing.T) {
|
|
b := newGRPCTestGraph()
|
|
b.addCaller("cli/main.go::run", "cli/main.go", "cli")
|
|
call := b.addStubCall("cli/main.go::run", "UserService", "GetUser", "cli/main.go")
|
|
methods := b.addServerImpl("userServer", "svc", "svc", "GetUser")
|
|
b.addServerInterface("UserService", "svc/impl.go::userServer", "svc", "svc")
|
|
// No registration — interface satisfaction is the only signal.
|
|
|
|
resolved := ResolveGRPCStubCalls(b.g)
|
|
assert.Equal(t, 1, resolved)
|
|
assert.Equal(t, methods["GetUser"], call.To)
|
|
assert.Equal(t, graph.OriginASTInferred, call.Origin)
|
|
assert.Equal(t, 0.7, call.Confidence)
|
|
}
|
|
|
|
func TestResolveGRPCStubCalls_RegistrationPreferredOverInterface(t *testing.T) {
|
|
b := newGRPCTestGraph()
|
|
b.addCaller("cli/main.go::run", "cli/main.go", "cli")
|
|
call := b.addStubCall("cli/main.go::run", "UserService", "GetUser", "cli/main.go")
|
|
// The real handler, found via registration.
|
|
real := b.addServerImpl("userServer", "svc", "svc", "GetUser")
|
|
b.addRegistration("UserService", "userServer", "svc/main.go::main", "svc/main.go", "svc")
|
|
// A decoy type also structurally implements the interface.
|
|
decoy := b.addServerImpl("decoyServer", "decoy", "svc", "GetUser")
|
|
b.addServerInterface("UserService", "decoy/impl.go::decoyServer", "decoy", "svc")
|
|
|
|
ResolveGRPCStubCalls(b.g)
|
|
assert.Equal(t, real["GetUser"], call.To, "registration signal must win over interface signal")
|
|
assert.NotEqual(t, decoy["GetUser"], call.To)
|
|
}
|
|
|
|
func TestResolveGRPCStubCalls_UnimplementedSkipped(t *testing.T) {
|
|
b := newGRPCTestGraph()
|
|
b.addCaller("cli/main.go::run", "cli/main.go", "cli")
|
|
call := b.addStubCall("cli/main.go::run", "UserService", "GetUser", "cli/main.go")
|
|
// Only the generated Unimplemented stub implements the interface;
|
|
// the interface signal must skip it and resolve nothing.
|
|
b.addServerImpl("UnimplementedUserServiceServer", "svc", "svc", "GetUser")
|
|
b.addServerInterface("UserService", "svc/impl.go::UnimplementedUserServiceServer", "svc", "svc")
|
|
|
|
resolved := ResolveGRPCStubCalls(b.g)
|
|
assert.Equal(t, 0, resolved)
|
|
assert.Equal(t, grpcStubPlaceholder("UserService", "GetUser"), call.To)
|
|
}
|
|
|
|
func TestResolveGRPCStubCalls_NoHandlerStaysPlaceholder(t *testing.T) {
|
|
b := newGRPCTestGraph()
|
|
b.addCaller("cli/main.go::run", "cli/main.go", "cli")
|
|
call := b.addStubCall("cli/main.go::run", "UserService", "GetUser", "cli/main.go")
|
|
|
|
resolved := ResolveGRPCStubCalls(b.g)
|
|
assert.Equal(t, 0, resolved)
|
|
assert.Equal(t, grpcStubPlaceholder("UserService", "GetUser"), call.To)
|
|
assert.Empty(t, call.Origin)
|
|
}
|
|
|
|
func TestResolveGRPCStubCalls_Idempotent(t *testing.T) {
|
|
b := newGRPCTestGraph()
|
|
b.addCaller("cli/main.go::run", "cli/main.go", "cli")
|
|
call := b.addStubCall("cli/main.go::run", "UserService", "GetUser", "cli/main.go")
|
|
methods := b.addServerImpl("userServer", "svc", "svc", "GetUser")
|
|
b.addRegistration("UserService", "userServer", "svc/main.go::main", "svc/main.go", "svc")
|
|
|
|
first := ResolveGRPCStubCalls(b.g)
|
|
second := ResolveGRPCStubCalls(b.g)
|
|
third := ResolveGRPCStubCalls(b.g)
|
|
assert.Equal(t, 1, first)
|
|
assert.Equal(t, 1, second)
|
|
assert.Equal(t, 1, third)
|
|
assert.Equal(t, methods["GetUser"], call.To)
|
|
// No duplicate inbound edges accreted across re-runs.
|
|
require.Len(t, b.g.GetInEdges(methods["GetUser"]), 1)
|
|
}
|
|
|
|
func TestResolveGRPCStubCalls_ReorphanWhenSignalLost(t *testing.T) {
|
|
b := newGRPCTestGraph()
|
|
b.addCaller("cli/main.go::run", "cli/main.go", "cli")
|
|
call := b.addStubCall("cli/main.go::run", "UserService", "GetUser", "cli/main.go")
|
|
b.addServerImpl("userServer", "svc", "svc", "GetUser")
|
|
b.addRegistration("UserService", "userServer", "svc/main.go::main", "svc/main.go", "svc")
|
|
|
|
ResolveGRPCStubCalls(b.g)
|
|
require.NotEqual(t, grpcStubPlaceholder("UserService", "GetUser"), call.To)
|
|
|
|
// The wiring file is reindexed and no longer registers the server
|
|
// (and there is no interface-satisfaction fallback). The handler
|
|
// node and the client call edge both still exist, but the pass can
|
|
// no longer discover a handler — the edge must re-orphan.
|
|
b.g.EvictFile("svc/main.go")
|
|
|
|
resolved := ResolveGRPCStubCalls(b.g)
|
|
assert.Equal(t, 0, resolved)
|
|
assert.Equal(t, grpcStubPlaceholder("UserService", "GetUser"), call.To, "edge must re-orphan to the placeholder")
|
|
assert.Empty(t, call.Origin)
|
|
assert.Empty(t, call.ConfidenceLabel)
|
|
_, hasRes := call.Meta["grpc_resolution"]
|
|
assert.False(t, hasRes, "grpc_resolution meta must be cleared on re-orphan")
|
|
}
|
|
|
|
func TestResolveGRPCStubCalls_AmbiguousUnresolved(t *testing.T) {
|
|
b := newGRPCTestGraph()
|
|
b.addCaller("cli/main.go::run", "cli/main.go", "cli")
|
|
call := b.addStubCall("cli/main.go::run", "UserService", "GetUser", "cli/main.go")
|
|
// Two distinct impl types in the same repo both expose GetUser via
|
|
// the interface signal — ambiguous, must not resolve.
|
|
b.addServerImpl("serverA", "a", "svc", "GetUser")
|
|
b.addServerImpl("serverB", "b", "svc", "GetUser")
|
|
b.addServerInterface("UserService", "a/impl.go::serverA", "a", "svc")
|
|
b.addServerInterface("UserService", "b/impl.go::serverB", "b", "svc")
|
|
|
|
resolved := ResolveGRPCStubCalls(b.g)
|
|
assert.Equal(t, 0, resolved)
|
|
assert.Equal(t, grpcStubPlaceholder("UserService", "GetUser"), call.To)
|
|
}
|
|
|
|
func TestResolveGRPCStubCalls_SameRepoPreference(t *testing.T) {
|
|
b := newGRPCTestGraph()
|
|
// Caller lives in repo "svc" alongside one of two handler impls.
|
|
b.addCaller("svc/client.go::run", "svc/client.go", "svc")
|
|
call := b.addStubCall("svc/client.go::run", "UserService", "GetUser", "svc/client.go")
|
|
local := b.addServerImpl("localServer", "svc", "svc", "GetUser")
|
|
b.addServerInterface("UserService", "svc/impl.go::localServer", "svc", "svc")
|
|
b.addServerImpl("remoteServer", "other", "other", "GetUser")
|
|
b.addServerInterface("UserService", "other/impl.go::remoteServer", "other", "other")
|
|
|
|
ResolveGRPCStubCalls(b.g)
|
|
assert.Equal(t, local["GetUser"], call.To, "same-repo handler must win the tie-break")
|
|
}
|
|
|
|
func TestResolveGRPCStubCalls_CrossRepo(t *testing.T) {
|
|
// Client in repo "cli", handler in repo "svc": the resolved
|
|
// EdgeCalls edge then flows through DetectCrossRepoEdges.
|
|
b := newGRPCTestGraph()
|
|
b.addCaller("cli/main.go::run", "cli/main.go", "cli")
|
|
call := b.addStubCall("cli/main.go::run", "UserService", "GetUser", "cli/main.go")
|
|
methods := b.addServerImpl("userServer", "svc", "svc", "GetUser")
|
|
b.addRegistration("UserService", "userServer", "svc/main.go::main", "svc/main.go", "svc")
|
|
|
|
ResolveGRPCStubCalls(b.g)
|
|
require.Equal(t, methods["GetUser"], call.To)
|
|
|
|
emitted := DetectCrossRepoEdges(b.g)
|
|
assert.Equal(t, 1, emitted, "resolved cross-repo gRPC call must materialise a cross_repo_calls edge")
|
|
cr := firstOutEdgeByKind(b.g, "cli/main.go::run", graph.EdgeCrossRepoCalls)
|
|
require.NotNil(t, cr)
|
|
assert.Equal(t, methods["GetUser"], cr.To)
|
|
}
|