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
212 lines
7.1 KiB
Go
212 lines
7.1 KiB
Go
package indexer
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
"github.com/zzet/gortex/internal/parser/crashpool"
|
|
"github.com/zzet/gortex/internal/parser/languages"
|
|
)
|
|
|
|
// panicExtractor is a fake extractor that always panics — it stands in
|
|
// for a real grammar/extractor fault on a malformed file.
|
|
type panicExtractor struct{}
|
|
|
|
func (panicExtractor) Language() string { return "boom" }
|
|
func (panicExtractor) Extensions() []string { return []string{".boom"} }
|
|
func (panicExtractor) Extract(string, []byte) (*parser.ExtractionResult, error) {
|
|
panic("runtime error: slice bounds out of range [95:50]")
|
|
}
|
|
|
|
// crashWorkerEnv makes the indexer test binary re-execute itself as a
|
|
// crashpool worker subprocess instead of running the test suite.
|
|
const crashWorkerEnv = "GORTEX_INDEXER_TEST_PARSEWORKER"
|
|
|
|
// TestMain lets crash-isolation tests use the test binary itself as the
|
|
// parser worker subprocess — no built gortex binary required.
|
|
func TestMain(m *testing.M) {
|
|
if os.Getenv(crashWorkerEnv) == "1" {
|
|
_ = crashpool.RunWorker(os.Stdin, os.Stdout)
|
|
os.Exit(0)
|
|
}
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestExtractFile_InProcess(t *testing.T) {
|
|
g := graph.New()
|
|
idx := newTestIndexer(g)
|
|
result, quarantined, err := idx.extractFile(nil, nil, "x.go", "x.go", "go",
|
|
languages.NewGoExtractor(), []byte("package main\n\nfunc Hello() {}\n"))
|
|
require.NoError(t, err)
|
|
require.False(t, quarantined)
|
|
require.NotEmpty(t, result.Nodes)
|
|
}
|
|
|
|
// TestExtractFile_InProcessPanicIsolated guards issue #54: an extractor
|
|
// panic on the in-process (pool == nil) path must be recovered and the
|
|
// file quarantined, never crash the whole indexing run. Covers both the
|
|
// no-budget direct call and the budgeted goroutine path.
|
|
func TestExtractFile_InProcessPanicIsolated(t *testing.T) {
|
|
for _, budget := range []int{0, 5000} {
|
|
name := "no_budget"
|
|
if budget > 0 {
|
|
name = "with_budget"
|
|
}
|
|
t.Run(name, func(t *testing.T) {
|
|
g := graph.New()
|
|
idx := newTestIndexer(g)
|
|
idx.config.MaxExtractMillis = budget
|
|
|
|
result, quarantined, err := idx.extractFile(nil, nil, "boom.boom", "boom.boom",
|
|
"boom", panicExtractor{}, []byte("anything"))
|
|
|
|
require.Error(t, err, "a recovered panic must surface as an error")
|
|
var pe *extractorPanicError
|
|
require.ErrorAs(t, err, &pe, "error must be an extractorPanicError")
|
|
require.True(t, quarantined, "the file must be quarantined, not silently dropped")
|
|
require.Len(t, result.Nodes, 1)
|
|
require.Equal(t, graph.KindFile, result.Nodes[0].Kind)
|
|
require.Equal(t, true, result.Nodes[0].Meta["quarantined"])
|
|
require.Contains(t, result.Nodes[0].Meta["parse_error"], "extractor panic")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestExtractFile_SubprocessPool exercises the full parent→worker→parent
|
|
// path: the file is parsed in a worker subprocess and its nodes come
|
|
// back over the gob pipe.
|
|
func TestExtractFile_SubprocessPool(t *testing.T) {
|
|
pool, err := crashpool.NewPool(crashpool.Config{
|
|
Argv: []string{os.Args[0]},
|
|
Env: []string{crashWorkerEnv + "=1"},
|
|
Workers: 2,
|
|
})
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
g := graph.New()
|
|
idx := newTestIndexer(g)
|
|
q := crashpool.LoadQuarantine("")
|
|
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "main.go")
|
|
src := []byte("package main\n\nfunc Hello() {}\n")
|
|
writeFile(t, path, string(src))
|
|
|
|
result, quarantined, err := idx.extractFile(pool, q, path, "main.go", "go",
|
|
languages.NewGoExtractor(), src)
|
|
require.NoError(t, err)
|
|
require.False(t, quarantined)
|
|
require.NotEmpty(t, result.Nodes)
|
|
}
|
|
|
|
// TestIndex_CrashIsolationFullPass indexes a repo end-to-end with crash
|
|
// isolation enabled — the IndexCtx wiring spawns worker subprocesses,
|
|
// parses every file through them, and completes.
|
|
func TestIndex_CrashIsolationFullPass(t *testing.T) {
|
|
t.Setenv("GORTEX_PARSER_ISOLATION", "1")
|
|
t.Setenv(crashWorkerEnv, "1")
|
|
|
|
dir := setupTestDir(t)
|
|
g := graph.New()
|
|
idx := newTestIndexer(g)
|
|
result, err := idx.Index(dir)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, result.FileCount) // main.go + pkg/util.go (vendor excluded)
|
|
require.Greater(t, result.NodeCount, 0)
|
|
require.Equal(t, 0, result.QuarantinedFiles)
|
|
}
|
|
|
|
func TestCrashIsolationEnabled_EnvOverride(t *testing.T) {
|
|
g := graph.New()
|
|
idx := newTestIndexer(g)
|
|
require.False(t, idx.crashIsolationEnabled())
|
|
|
|
idx.config.CrashIsolation = true
|
|
require.True(t, idx.crashIsolationEnabled())
|
|
|
|
t.Setenv("GORTEX_PARSER_ISOLATION", "0")
|
|
require.False(t, idx.crashIsolationEnabled()) // env overrides config-on
|
|
|
|
t.Setenv("GORTEX_PARSER_ISOLATION", "1")
|
|
idx.config.CrashIsolation = false
|
|
require.True(t, idx.crashIsolationEnabled()) // env overrides config-off
|
|
}
|
|
|
|
func TestQuarantineResult(t *testing.T) {
|
|
r := quarantineResult("bad/file.go", "go", "SIGSEGV in grammar")
|
|
require.Len(t, r.Nodes, 1)
|
|
n := r.Nodes[0]
|
|
require.Equal(t, graph.KindFile, n.Kind)
|
|
require.Equal(t, "bad/file.go", n.ID)
|
|
require.Equal(t, "file.go", n.Name)
|
|
require.Equal(t, true, n.Meta["quarantined"])
|
|
require.Equal(t, "SIGSEGV in grammar", n.Meta["parse_error"])
|
|
}
|
|
|
|
func TestStampParseErrorCount(t *testing.T) {
|
|
nodes := []*graph.Node{
|
|
{ID: "f.go", Kind: graph.KindFile},
|
|
{ID: "f.go::Fn", Kind: graph.KindFunction},
|
|
}
|
|
stampParseErrorCount(nodes, 3)
|
|
require.Equal(t, 3, nodes[0].Meta["parse_errors"])
|
|
require.Equal(t, true, nodes[0].Meta["has_parse_errors"])
|
|
require.Nil(t, nodes[1].Meta)
|
|
|
|
clean := []*graph.Node{{ID: "g.go", Kind: graph.KindFile}}
|
|
stampParseErrorCount(clean, 0) // zero count → no stamp
|
|
require.Nil(t, clean[0].Meta)
|
|
}
|
|
|
|
// TestSharedParsePool_Reused proves the crash-isolation pool is created
|
|
// once and reused — the watcher path must not fork a fresh worker
|
|
// subprocess per file.
|
|
func TestSharedParsePool_Reused(t *testing.T) {
|
|
t.Setenv("GORTEX_PARSER_ISOLATION", "1")
|
|
t.Setenv(crashWorkerEnv, "1")
|
|
|
|
g := graph.New()
|
|
idx := newTestIndexer(g)
|
|
idx.SetRootPath(t.TempDir())
|
|
defer idx.CloseParsePool()
|
|
|
|
p1, _ := idx.sharedParsePool()
|
|
require.NotNil(t, p1)
|
|
p2, _ := idx.sharedParsePool()
|
|
require.Same(t, p1, p2, "sharedParsePool must return the same long-lived pool")
|
|
}
|
|
|
|
// TestIndexFile_CrashIsolationReusesPool re-indexes single files through
|
|
// the crash-isolation path and confirms they all flow through one
|
|
// persistent pool rather than a per-file worker subprocess.
|
|
func TestIndexFile_CrashIsolationReusesPool(t *testing.T) {
|
|
t.Setenv("GORTEX_PARSER_ISOLATION", "1")
|
|
t.Setenv(crashWorkerEnv, "1")
|
|
|
|
dir := t.TempDir()
|
|
writeFile(t, filepath.Join(dir, "a.go"), "package main\n\nfunc A() {}\n")
|
|
writeFile(t, filepath.Join(dir, "b.go"), "package main\n\nfunc B() {}\n")
|
|
|
|
g := graph.New()
|
|
idx := newTestIndexer(g)
|
|
_, err := idx.Index(dir) // cold index also anchors rootPath
|
|
require.NoError(t, err)
|
|
defer idx.CloseParsePool()
|
|
|
|
require.NoError(t, idx.IndexFile(filepath.Join(dir, "a.go")))
|
|
require.NotNil(t, idx.parsePool, "first IndexFile must create the shared pool")
|
|
first := idx.parsePool
|
|
|
|
require.NoError(t, idx.IndexFile(filepath.Join(dir, "b.go")))
|
|
require.Same(t, first, idx.parsePool, "second IndexFile must reuse the shared pool")
|
|
|
|
require.NotEmpty(t, g.FindNodesByName("A"))
|
|
require.NotEmpty(t, g.FindNodesByName("B"))
|
|
}
|