zzet--gortex
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
190 行
5.8 KiB
Go
190 行
5.8 KiB
Go
package indexer
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/zzet/gortex/internal/clones"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// cloneRepoSource is a single Go file holding two near-duplicate
|
|
// functions (processItems / processRecords — identical logic, every
|
|
// identifier renamed: a textbook Type-2 clone) plus an unrelated
|
|
// function of comparable size that must NOT be flagged as a clone.
|
|
const cloneRepoSource = `package main
|
|
|
|
func processItems(items []Item) int {
|
|
total := 0
|
|
for i := 0; i < len(items); i++ {
|
|
if items[i].Active {
|
|
total += items[i].Weight * factor
|
|
} else {
|
|
total -= items[i].Penalty
|
|
}
|
|
}
|
|
if total < 0 {
|
|
total = 0
|
|
}
|
|
return total
|
|
}
|
|
|
|
func processRecords(records []Record) int {
|
|
sum := 0
|
|
for idx := 0; idx < len(records); idx++ {
|
|
if records[idx].Enabled {
|
|
sum += records[idx].Score * multiplier
|
|
} else {
|
|
sum -= records[idx].Fine
|
|
}
|
|
}
|
|
if sum < 0 {
|
|
sum = 0
|
|
}
|
|
return sum
|
|
}
|
|
|
|
func openAndScan(conn *Conn, statement string) error {
|
|
rows, err := conn.Query(statement)
|
|
if err != nil {
|
|
return wrap(err, "query failed")
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var name string
|
|
if scanErr := rows.Scan(&name); scanErr != nil {
|
|
return scanErr
|
|
}
|
|
}
|
|
return rows.Err()
|
|
}
|
|
`
|
|
|
|
func similarToEdges(g graph.Store) []*graph.Edge {
|
|
var out []*graph.Edge
|
|
for _, e := range g.AllEdges() {
|
|
if e.Kind == graph.EdgeSimilarTo {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// TestIndex_EmitsSimilarToEdges runs the full single-repo indexer over
|
|
// a file with a Type-2 clone pair and asserts the inline IndexCtx
|
|
// global pass stamped signatures and materialised symmetric
|
|
// EdgeSimilarTo edges between exactly the two clones.
|
|
func TestIndex_EmitsSimilarToEdges(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, filepath.Join(dir, "main.go"), cloneRepoSource)
|
|
|
|
g := graph.New()
|
|
idx := newTestIndexer(g)
|
|
_, err := idx.Index(dir)
|
|
require.NoError(t, err)
|
|
|
|
itemsID := "main.go::processItems"
|
|
recordsID := "main.go::processRecords"
|
|
otherID := "main.go::openAndScan"
|
|
|
|
// Signatures stamped at parse time.
|
|
for _, id := range []string{itemsID, recordsID, otherID} {
|
|
n := g.GetNode(id)
|
|
require.NotNil(t, n, "node %s should exist", id)
|
|
sig, _ := n.Meta[cloneSigMetaKey].(string)
|
|
assert.NotEmpty(t, sig, "node %s should carry a clone_sig", id)
|
|
}
|
|
|
|
edges := similarToEdges(g)
|
|
require.Len(t, edges, 2, "expected exactly one symmetric clone pair (2 directed edges)")
|
|
|
|
// Both directions present, between the two clones only.
|
|
dirs := map[[2]string]bool{}
|
|
for _, e := range edges {
|
|
dirs[[2]string{e.From, e.To}] = true
|
|
assert.Equal(t, graph.OriginASTInferred, e.Origin)
|
|
sim, ok := e.Meta["similarity"].(float64)
|
|
assert.True(t, ok, "edge should carry similarity meta")
|
|
assert.GreaterOrEqual(t, sim, clones.DefaultThreshold)
|
|
assert.Equal(t, sim, e.Confidence)
|
|
}
|
|
assert.True(t, dirs[[2]string{itemsID, recordsID}], "missing processItems→processRecords")
|
|
assert.True(t, dirs[[2]string{recordsID, itemsID}], "missing processRecords→processItems")
|
|
|
|
// The unrelated function must not appear on any clone edge.
|
|
for _, e := range edges {
|
|
assert.NotEqual(t, otherID, e.From, "unrelated fn must not be a clone")
|
|
assert.NotEqual(t, otherID, e.To, "unrelated fn must not be a clone")
|
|
}
|
|
}
|
|
|
|
// TestIndex_CloneEdgesSurviveReindex verifies that re-indexing the file
|
|
// (eviction + re-add + global recompute) leaves the clone edge set
|
|
// consistent rather than duplicated or stale.
|
|
func TestIndex_CloneEdgesSurviveReindex(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "main.go")
|
|
writeFile(t, path, cloneRepoSource)
|
|
|
|
g := graph.New()
|
|
idx := newTestIndexer(g)
|
|
_, err := idx.Index(dir)
|
|
require.NoError(t, err)
|
|
require.Len(t, similarToEdges(g), 2)
|
|
|
|
// Re-index the same file: EvictFile drops the old edges in both
|
|
// directions, the resolve pass recomputes them.
|
|
require.NoError(t, idx.IndexFile(path))
|
|
assert.Len(t, similarToEdges(g), 2, "reindex must not duplicate or drop clone edges")
|
|
}
|
|
|
|
// TestDetectClonesAndEmitEdges is a focused unit test over the global
|
|
// pass: it builds a graph by hand, stamps signatures, and asserts edge
|
|
// emission + idempotency without going through the parser.
|
|
func TestDetectClonesAndEmitEdges(t *testing.T) {
|
|
g := graph.New()
|
|
|
|
bodyA := cloneRepoSource // any substantial body works for the unit test
|
|
sigA, ok := clones.ComputeSignature(bodyA)
|
|
require.True(t, ok)
|
|
enc := clones.EncodeSignature(sigA)
|
|
|
|
// Two nodes carrying the identical signature → guaranteed clone.
|
|
g.AddNode(&graph.Node{
|
|
ID: "a.go::Fn", Kind: graph.KindFunction, Name: "Fn",
|
|
FilePath: "a.go", StartLine: 1, Language: "go",
|
|
Meta: map[string]any{cloneSigMetaKey: enc},
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "b.go::Gn", Kind: graph.KindMethod, Name: "Gn",
|
|
FilePath: "b.go", StartLine: 1, Language: "go",
|
|
Meta: map[string]any{cloneSigMetaKey: enc},
|
|
})
|
|
// A node with no signature must be ignored.
|
|
g.AddNode(&graph.Node{
|
|
ID: "c.go::Hn", Kind: graph.KindFunction, Name: "Hn",
|
|
FilePath: "c.go", StartLine: 1, Language: "go",
|
|
})
|
|
|
|
stats := detectClonesAndEmitEdges(g, "", 0)
|
|
assert.Equal(t, 1, stats.Pairs)
|
|
assert.Equal(t, 2, stats.Edges)
|
|
|
|
// Idempotent: a second run dedupes via graph.AddEdge.
|
|
detectClonesAndEmitEdges(g, "", 0)
|
|
assert.Len(t, similarToEdges(g), 2, "second pass must not duplicate edges")
|
|
}
|
|
|
|
func TestBodyText(t *testing.T) {
|
|
lines := []string{"line1", "line2", "line3", "line4"}
|
|
assert.Equal(t, "line2\nline3", bodyText(lines, 2, 3))
|
|
assert.Equal(t, "line1\nline2\nline3\nline4", bodyText(lines, 1, 4))
|
|
assert.Equal(t, "", bodyText(lines, 0, 2), "zero start line is degenerate")
|
|
assert.Equal(t, "", bodyText(lines, 3, 2), "end before start is degenerate")
|
|
assert.Equal(t, "", bodyText(lines, 99, 100), "out-of-bounds start")
|
|
assert.Equal(t, "line4", bodyText(lines, 4, 99), "end clamps to len")
|
|
}
|