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
269 lines
7.8 KiB
Go
269 lines
7.8 KiB
Go
package analysis
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/query"
|
|
"pgregory.net/rapid"
|
|
)
|
|
|
|
// --- Mock SourceReader ---
|
|
|
|
type mockSourceReader struct {
|
|
g *graph.Graph
|
|
rootPath string
|
|
}
|
|
|
|
func (m *mockSourceReader) Graph() graph.Store { return m.g }
|
|
func (m *mockSourceReader) ResolveFilePath(relPath string) string {
|
|
if filepath.IsAbs(relPath) {
|
|
return relPath
|
|
}
|
|
if m.rootPath == "" {
|
|
return ""
|
|
}
|
|
return filepath.Join(m.rootPath, relPath)
|
|
}
|
|
|
|
// --- Helpers ---
|
|
|
|
// writeGoFile creates a Go source file in tmpDir at the given relative path
|
|
// with the provided content. Returns the relative path.
|
|
func writeGoFile(t *testing.T, tmpDir, relPath, content string) {
|
|
t.Helper()
|
|
absPath := filepath.Join(tmpDir, relPath)
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(absPath), 0o755))
|
|
require.NoError(t, os.WriteFile(absPath, []byte(content), 0o644))
|
|
}
|
|
|
|
// Feature: gortex-enhancements, Property 7: Scaffold name substitution
|
|
|
|
// --- Generators ---
|
|
|
|
// genValidGoIdent generates a valid Go identifier (uppercase letter + lowercase suffix).
|
|
func genValidGoIdent() *rapid.Generator[string] {
|
|
return rapid.Custom(func(t *rapid.T) string {
|
|
upper := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
|
|
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}
|
|
lower := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
|
|
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}
|
|
|
|
first := rapid.SampledFrom(upper).Draw(t, "first")
|
|
suffixLen := rapid.IntRange(2, 8).Draw(t, "suffixLen")
|
|
var sb strings.Builder
|
|
sb.WriteString(first)
|
|
for i := range suffixLen {
|
|
sb.WriteString(rapid.SampledFrom(lower).Draw(t, fmt.Sprintf("s%d", i)))
|
|
}
|
|
return sb.String()
|
|
})
|
|
}
|
|
|
|
// genDistinctIdentPair generates two distinct Go identifiers.
|
|
func genDistinctIdentPair() *rapid.Generator[[2]string] {
|
|
return rapid.Custom(func(t *rapid.T) [2]string {
|
|
a := genValidGoIdent().Draw(t, "identA")
|
|
b := genValidGoIdent().Draw(t, "identB")
|
|
// Ensure they are distinct
|
|
for b == a {
|
|
b = genValidGoIdent().Draw(t, "identBRetry")
|
|
}
|
|
return [2]string{a, b}
|
|
})
|
|
}
|
|
|
|
// --- Property Tests ---
|
|
|
|
// TestPropertyScaffoldNameSubstitution verifies that for any existing symbol
|
|
// used as an example and for any valid new name, the scaffold output contains
|
|
// the new name in place of the example's name in all generated code, and each
|
|
// ScaffoldEdit has non-empty file_path, a positive insertion_line, and non-empty code.
|
|
func TestPropertyScaffoldNameSubstitution(t *testing.T) {
|
|
rapid.Check(t, func(rt *rapid.T) {
|
|
names := genDistinctIdentPair().Draw(rt, "names")
|
|
exampleName := names[0]
|
|
newName := names[1]
|
|
|
|
// Create a temp directory with a Go source file containing the example function
|
|
tmpDir := t.TempDir()
|
|
relPath := "pkg/example.go"
|
|
sourceCode := fmt.Sprintf(`package pkg
|
|
|
|
func %s(x int) int {
|
|
return x + 1
|
|
}
|
|
`, exampleName)
|
|
writeGoFile(t, tmpDir, relPath, sourceCode)
|
|
|
|
// Build a graph with the example function node
|
|
g := graph.New()
|
|
exampleID := fmt.Sprintf("%s::%s", relPath, exampleName)
|
|
g.AddNode(&graph.Node{
|
|
ID: exampleID,
|
|
Kind: graph.KindFunction,
|
|
Name: exampleName,
|
|
FilePath: relPath,
|
|
StartLine: 3,
|
|
EndLine: 5,
|
|
Language: "go",
|
|
})
|
|
|
|
engine := query.NewEngine(g)
|
|
reader := &mockSourceReader{g: g, rootPath: tmpDir}
|
|
|
|
result, err := GenerateScaffold(engine, reader, exampleID, newName)
|
|
if err != nil {
|
|
rt.Fatalf("GenerateScaffold returned error: %v", err)
|
|
}
|
|
|
|
if len(result.Edits) == 0 {
|
|
rt.Fatal("expected at least one ScaffoldEdit, got none")
|
|
}
|
|
|
|
// Verify each edit has valid fields and the new name is present
|
|
foundNewName := false
|
|
for i, edit := range result.Edits {
|
|
if edit.FilePath == "" {
|
|
rt.Errorf("edit[%d].FilePath is empty", i)
|
|
}
|
|
if edit.InsertionLine <= 0 {
|
|
rt.Errorf("edit[%d].InsertionLine = %d, want > 0", i, edit.InsertionLine)
|
|
}
|
|
if edit.Code == "" {
|
|
rt.Errorf("edit[%d].Code is empty", i)
|
|
}
|
|
if strings.Contains(edit.Code, newName) {
|
|
foundNewName = true
|
|
}
|
|
}
|
|
|
|
if !foundNewName {
|
|
rt.Errorf("no edit contains the new name %q; edits: %v", newName, result.Edits)
|
|
}
|
|
|
|
// The new_symbol edit should NOT contain the old example name as a
|
|
// standalone identifier. A simple strings.Contains would mis-flag
|
|
// the new name when it's a superstring (e.g. example="Bbb",
|
|
// new="Bbba" — "Bbba" trivially contains "Bbb"). Use a word-
|
|
// boundary match so "Bbb" only counts when it stands alone.
|
|
oldRE := regexp.MustCompile(`\b` + regexp.QuoteMeta(exampleName) + `\b`)
|
|
for _, edit := range result.Edits {
|
|
if edit.Reason == "new_symbol" {
|
|
if oldRE.MatchString(edit.Code) {
|
|
rt.Errorf("new_symbol edit still contains old name %q", exampleName)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- Unit Tests for Edge Cases ---
|
|
|
|
// TestScaffoldNoCallersOmitsRegistration verifies that when the example symbol
|
|
// has no callers at depth 1, the registration/wiring section is omitted and a
|
|
// note is included explaining that no wiring pattern was detected.
|
|
func TestScaffoldNoCallersOmitsRegistration(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
relPath := "pkg/lonely.go"
|
|
sourceCode := `package pkg
|
|
|
|
func LonelyFunc(x int) int {
|
|
return x * 2
|
|
}
|
|
`
|
|
writeGoFile(t, tmpDir, relPath, sourceCode)
|
|
|
|
g := graph.New()
|
|
exampleID := "pkg/lonely.go::LonelyFunc"
|
|
g.AddNode(&graph.Node{
|
|
ID: exampleID,
|
|
Kind: graph.KindFunction,
|
|
Name: "LonelyFunc",
|
|
FilePath: relPath,
|
|
StartLine: 3,
|
|
EndLine: 5,
|
|
Language: "go",
|
|
})
|
|
// No callers added — the function is isolated
|
|
|
|
engine := query.NewEngine(g)
|
|
reader := &mockSourceReader{g: g, rootPath: tmpDir}
|
|
|
|
result, err := GenerateScaffold(engine, reader, exampleID, "NewFunc")
|
|
require.NoError(t, err)
|
|
|
|
// Should have no registration edit
|
|
for _, edit := range result.Edits {
|
|
assert.NotEqual(t, "registration", edit.Reason,
|
|
"registration edit should be omitted when there are no callers")
|
|
}
|
|
|
|
// Should have a note about missing callers
|
|
foundNote := false
|
|
for _, note := range result.Notes {
|
|
if strings.Contains(note, "no callers") || strings.Contains(note, "registration") || strings.Contains(note, "wiring") {
|
|
foundNote = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, foundNote, "expected a note about omitted registration; notes: %v", result.Notes)
|
|
}
|
|
|
|
// TestScaffoldNoTestsOmitsTestStub verifies that when the example symbol has
|
|
// no associated test file or test function, the test stub is omitted and a
|
|
// note is included explaining that no test pattern was detected.
|
|
func TestScaffoldNoTestsOmitsTestStub(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
relPath := "pkg/notested.go"
|
|
sourceCode := `package pkg
|
|
|
|
func NoTestedFunc(s string) string {
|
|
return s + "!"
|
|
}
|
|
`
|
|
writeGoFile(t, tmpDir, relPath, sourceCode)
|
|
// Deliberately do NOT create a _test.go file
|
|
|
|
g := graph.New()
|
|
exampleID := "pkg/notested.go::NoTestedFunc"
|
|
g.AddNode(&graph.Node{
|
|
ID: exampleID,
|
|
Kind: graph.KindFunction,
|
|
Name: "NoTestedFunc",
|
|
FilePath: relPath,
|
|
StartLine: 3,
|
|
EndLine: 5,
|
|
Language: "go",
|
|
})
|
|
|
|
engine := query.NewEngine(g)
|
|
reader := &mockSourceReader{g: g, rootPath: tmpDir}
|
|
|
|
result, err := GenerateScaffold(engine, reader, exampleID, "NewFunc")
|
|
require.NoError(t, err)
|
|
|
|
// Should have no test_stub edit
|
|
for _, edit := range result.Edits {
|
|
assert.NotEqual(t, "test_stub", edit.Reason,
|
|
"test_stub edit should be omitted when there is no test file")
|
|
}
|
|
|
|
// Should have a note about missing test pattern
|
|
foundNote := false
|
|
for _, note := range result.Notes {
|
|
if strings.Contains(note, "no test") || strings.Contains(note, "test stub") || strings.Contains(note, "test pattern") {
|
|
foundNote = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, foundNote, "expected a note about omitted test stub; notes: %v", result.Notes)
|
|
}
|