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
227 lines
6.3 KiB
Go
227 lines
6.3 KiB
Go
package dataflow
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
func newGraphWithNodes(ids ...string) *graph.Graph {
|
|
g := graph.New()
|
|
for _, id := range ids {
|
|
g.AddNode(&graph.Node{
|
|
ID: id,
|
|
Kind: graph.KindFunction,
|
|
Name: id,
|
|
FilePath: "test.go",
|
|
})
|
|
}
|
|
return g
|
|
}
|
|
|
|
func addEdge(g *graph.Graph, kind graph.EdgeKind, from, to string) {
|
|
g.AddEdge(&graph.Edge{
|
|
From: from,
|
|
To: to,
|
|
Kind: kind,
|
|
FilePath: "test.go",
|
|
Origin: graph.OriginASTResolved,
|
|
})
|
|
}
|
|
|
|
func TestEngine_FlowBetween_Direct(t *testing.T) {
|
|
g := newGraphWithNodes("A", "B")
|
|
addEdge(g, graph.EdgeValueFlow, "A", "B")
|
|
e := New(g)
|
|
paths := e.FlowBetween("A", "B", 0, 0)
|
|
if len(paths) != 1 {
|
|
t.Fatalf("expected 1 path, got %d", len(paths))
|
|
}
|
|
if paths[0].Length() != 1 || paths[0].IDs[0] != "A" || paths[0].IDs[1] != "B" {
|
|
t.Errorf("unexpected path: %+v", paths[0])
|
|
}
|
|
if paths[0].Confidence <= 0 || paths[0].Confidence > 1 {
|
|
t.Errorf("confidence out of range: %v", paths[0].Confidence)
|
|
}
|
|
}
|
|
|
|
func TestEngine_FlowBetween_MultiHop(t *testing.T) {
|
|
g := newGraphWithNodes("A", "B", "C", "D")
|
|
addEdge(g, graph.EdgeValueFlow, "A", "B")
|
|
addEdge(g, graph.EdgeArgOf, "B", "C")
|
|
addEdge(g, graph.EdgeReturnsTo, "C", "D")
|
|
e := New(g)
|
|
paths := e.FlowBetween("A", "D", 0, 0)
|
|
if len(paths) != 1 {
|
|
t.Fatalf("expected 1 path, got %d: %+v", len(paths), paths)
|
|
}
|
|
if paths[0].Length() != 3 {
|
|
t.Errorf("unexpected length: %d", paths[0].Length())
|
|
}
|
|
}
|
|
|
|
func TestEngine_FlowBetween_NoPath(t *testing.T) {
|
|
g := newGraphWithNodes("A", "B", "C")
|
|
addEdge(g, graph.EdgeValueFlow, "A", "B")
|
|
e := New(g)
|
|
paths := e.FlowBetween("A", "C", 0, 0)
|
|
if len(paths) != 0 {
|
|
t.Errorf("expected no paths, got %+v", paths)
|
|
}
|
|
}
|
|
|
|
func TestEngine_FlowBetween_DepthBound(t *testing.T) {
|
|
// 5-hop chain: A → B → C → D → E → F
|
|
g := newGraphWithNodes("A", "B", "C", "D", "E", "F")
|
|
addEdge(g, graph.EdgeValueFlow, "A", "B")
|
|
addEdge(g, graph.EdgeValueFlow, "B", "C")
|
|
addEdge(g, graph.EdgeValueFlow, "C", "D")
|
|
addEdge(g, graph.EdgeValueFlow, "D", "E")
|
|
addEdge(g, graph.EdgeValueFlow, "E", "F")
|
|
e := New(g)
|
|
paths := e.FlowBetween("A", "F", 3, 0)
|
|
if len(paths) != 0 {
|
|
t.Errorf("max_depth=3 should not reach F at hop 5, got %+v", paths)
|
|
}
|
|
paths = e.FlowBetween("A", "F", 5, 0)
|
|
if len(paths) != 1 {
|
|
t.Errorf("max_depth=5 should reach F, got %+v", paths)
|
|
}
|
|
}
|
|
|
|
func TestEngine_FlowBetween_MultiplePaths(t *testing.T) {
|
|
// Diamond: A → B → D
|
|
// A → C → D
|
|
g := newGraphWithNodes("A", "B", "C", "D")
|
|
addEdge(g, graph.EdgeValueFlow, "A", "B")
|
|
addEdge(g, graph.EdgeValueFlow, "B", "D")
|
|
addEdge(g, graph.EdgeValueFlow, "A", "C")
|
|
addEdge(g, graph.EdgeValueFlow, "C", "D")
|
|
e := New(g)
|
|
paths := e.FlowBetween("A", "D", 0, 0)
|
|
if len(paths) != 2 {
|
|
t.Fatalf("expected 2 paths through diamond, got %d: %+v", len(paths), paths)
|
|
}
|
|
}
|
|
|
|
func TestEngine_FlowBetween_IgnoresNonDataflow(t *testing.T) {
|
|
g := newGraphWithNodes("A", "B")
|
|
// Calls is not a dataflow edge; the BFS must skip it.
|
|
addEdge(g, graph.EdgeCalls, "A", "B")
|
|
e := New(g)
|
|
if got := e.FlowBetween("A", "B", 0, 0); len(got) != 0 {
|
|
t.Errorf("expected no paths on call-only graph, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestEngine_FlowBetween_SelfPath(t *testing.T) {
|
|
g := newGraphWithNodes("A")
|
|
e := New(g)
|
|
paths := e.FlowBetween("A", "A", 0, 0)
|
|
if len(paths) != 1 || len(paths[0].IDs) != 1 {
|
|
t.Errorf("expected one trivial self-path, got %+v", paths)
|
|
}
|
|
}
|
|
|
|
func TestEngine_TaintPaths_PatternMatch(t *testing.T) {
|
|
g := graph.New()
|
|
addNamedFunc := func(id, name string) {
|
|
g.AddNode(&graph.Node{
|
|
ID: id,
|
|
Kind: graph.KindFunction,
|
|
Name: name,
|
|
FilePath: "test.go",
|
|
})
|
|
}
|
|
addNamedFunc("pkg.GetEnv", "GetEnv")
|
|
addNamedFunc("pkg.Mid", "mid")
|
|
addNamedFunc("pkg.DBQuery", "DBQuery")
|
|
addEdge(g, graph.EdgeValueFlow, "pkg.GetEnv", "pkg.Mid")
|
|
addEdge(g, graph.EdgeArgOf, "pkg.Mid", "pkg.DBQuery")
|
|
|
|
e := New(g)
|
|
src := ParsePattern("name:Env")
|
|
sink := ParsePattern("name:Query")
|
|
findings := e.TaintPaths(src, sink, 0, 0)
|
|
if len(findings) != 1 {
|
|
t.Fatalf("expected 1 finding, got %d: %+v", len(findings), findings)
|
|
}
|
|
f := findings[0]
|
|
if f.Source.ID != "pkg.GetEnv" || f.Sink.ID != "pkg.DBQuery" {
|
|
t.Errorf("unexpected pair: %s → %s", f.Source.ID, f.Sink.ID)
|
|
}
|
|
if len(f.Paths) == 0 {
|
|
t.Errorf("path missing on finding")
|
|
}
|
|
}
|
|
|
|
func TestEngine_TaintPaths_RanksByLength(t *testing.T) {
|
|
g := graph.New()
|
|
for _, name := range []string{"src1", "src2", "mid", "sink"} {
|
|
g.AddNode(&graph.Node{
|
|
ID: name, Kind: graph.KindFunction, Name: name, FilePath: "t.go",
|
|
})
|
|
}
|
|
// src1 → sink (1 hop)
|
|
addEdge(g, graph.EdgeValueFlow, "src1", "sink")
|
|
// src2 → mid → sink (2 hops)
|
|
addEdge(g, graph.EdgeValueFlow, "src2", "mid")
|
|
addEdge(g, graph.EdgeValueFlow, "mid", "sink")
|
|
|
|
e := New(g)
|
|
src := ParsePattern("name:src")
|
|
sink := ParsePattern("exact:sink")
|
|
findings := e.TaintPaths(src, sink, 0, 0)
|
|
if len(findings) != 2 {
|
|
t.Fatalf("expected 2 findings, got %d", len(findings))
|
|
}
|
|
if findings[0].Source.ID != "src1" {
|
|
t.Errorf("expected src1 first (1 hop), got %s", findings[0].Source.ID)
|
|
}
|
|
}
|
|
|
|
func TestParsePattern_Combined(t *testing.T) {
|
|
p := ParsePattern("name:foo kind:method")
|
|
if p.Empty() {
|
|
t.Fatal("pattern should match")
|
|
}
|
|
matchNode := func(name string, kind graph.NodeKind) bool {
|
|
return p.matches(&graph.Node{Name: name, Kind: kind})
|
|
}
|
|
if !matchNode("foobar", graph.KindMethod) {
|
|
t.Error("foobar method should match")
|
|
}
|
|
if matchNode("foobar", graph.KindFunction) {
|
|
t.Error("function should not match kind:method clause")
|
|
}
|
|
if matchNode("baz", graph.KindMethod) {
|
|
t.Error("baz should not match name:foo clause")
|
|
}
|
|
}
|
|
|
|
func TestParsePattern_Exact(t *testing.T) {
|
|
p := ParsePattern("exact:Foo")
|
|
matchNode := func(name string) bool {
|
|
return p.matches(&graph.Node{Name: name, Kind: graph.KindFunction})
|
|
}
|
|
if !matchNode("Foo") {
|
|
t.Error("exact match must hit Foo")
|
|
}
|
|
if matchNode("FooBar") {
|
|
t.Error("exact match must not hit FooBar")
|
|
}
|
|
}
|
|
|
|
func TestParsePattern_Path(t *testing.T) {
|
|
p := ParsePattern("path:cmd/ name:main")
|
|
matchNode := func(name, path string) bool {
|
|
return p.matches(&graph.Node{Name: name, FilePath: path, Kind: graph.KindFunction})
|
|
}
|
|
if !matchNode("main", "cmd/server/main.go") {
|
|
t.Error("cmd/server/main.go::main should match")
|
|
}
|
|
if matchNode("main", "internal/server/main.go") {
|
|
t.Error("internal path should not match cmd/ prefix")
|
|
}
|
|
}
|