package main import ( "os" "path/filepath" "testing" "github.com/zzet/gortex/internal/graph" ) // TestRunRepo_LocalFixture is the end-to-end smoke test: runs the // full bench against the in-tree nestjs fixture and asserts the // shape of the resulting row. Uses a private cache dir so it doesn't // touch the developer's real ~/.cache/gortex/bench tree. func TestRunRepo_LocalFixture(t *testing.T) { fixturePath, err := filepath.Abs(filepath.Join("..", "fixtures", "di", "nestjs")) if err != nil { t.Fatal(err) } if _, err := os.Stat(fixturePath); err != nil { t.Skipf("fixture not available: %v", err) } // Run with a tiny query set to keep the test fast. queries := []string{"AppModule", "ConfigService", "auth handler"} row := runRepo(repoSpec{ Slug: "nestjs-fixture", Path: fixturePath, Local: true, }, queries, budgets{ // Generous budgets so a slow CI doesn't flake — the real // validation lives in main.go's strict-mode path. ImpactP95Ms: 50.0, SearchP95Ms: 100.0, }) if row.Error != "" { t.Fatalf("runRepo error: %s", row.Error) } if row.Files == 0 { t.Errorf("expected indexed files, got 0") } if row.Nodes == 0 { t.Errorf("expected graph nodes, got 0") } if row.ColdIndexMs <= 0 { t.Errorf("cold-index time should be > 0, got %.3f", row.ColdIndexMs) } if row.SearchP95Ms <= 0 { t.Errorf("search p95 should be > 0, got %.3f", row.SearchP95Ms) } if row.IncrementalMs <= 0 { t.Errorf("incremental should be > 0, got %.3f", row.IncrementalMs) } if row.DBBytes <= 0 { t.Errorf("DB size should be > 0, got %d", row.DBBytes) } if row.BudgetViolations > 0 { t.Errorf("generous budgets shouldn't violate, got %d", row.BudgetViolations) } } func TestPickImpactSeeds_RespectsLimit(t *testing.T) { g := graph.New() for i := range 5 { g.AddNode(&graph.Node{ ID: funcID(i), Name: funcName(i), Kind: graph.KindFunction, }) } seeds := pickImpactSeeds(g, 3) if len(seeds) != 3 { t.Errorf("len(seeds)=%d, want 3 (n < pool)", len(seeds)) } // Pool smaller than n: return all. seeds = pickImpactSeeds(g, 10) if len(seeds) != 5 { t.Errorf("len(seeds)=%d, want 5 (n > pool)", len(seeds)) } } func TestPickImpactSeeds_FunctionsAndMethodsOnly(t *testing.T) { g := graph.New() g.AddNode(&graph.Node{ID: "f.go::F", Name: "F", Kind: graph.KindFunction}) g.AddNode(&graph.Node{ID: "f.go::M", Name: "M", Kind: graph.KindMethod}) g.AddNode(&graph.Node{ID: "f.go::T", Name: "T", Kind: graph.KindType}) g.AddNode(&graph.Node{ID: "f.go::V", Name: "V", Kind: graph.KindVariable}) seeds := pickImpactSeeds(g, 10) if len(seeds) != 2 { t.Errorf("got %d seeds %v, want 2 (function + method only)", len(seeds), seeds) } } func TestPickIncrementalFiles_RespectsLimit(t *testing.T) { g := graph.New() for i := range 8 { g.AddNode(&graph.Node{ ID: fileID(i), Kind: graph.KindFile, FilePath: fileName(i), }) } files := pickIncrementalFiles(g, 3) if len(files) != 3 { t.Errorf("len(files)=%d, want 3", len(files)) } for _, f := range files { if f == "" { t.Errorf("empty file path returned") } } } func TestTouchFile_MissingFileSilent(t *testing.T) { if err := touchFile(filepath.Join(t.TempDir(), "missing.go")); err != nil { t.Errorf("missing file should not error, got %v", err) } } func TestTouchFile_BumpsMtime(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "x.go") if err := os.WriteFile(path, []byte("// x"), 0o644); err != nil { t.Fatal(err) } st1, _ := os.Stat(path) // Sleep to ensure mtime granularity is exceeded. old := st1.ModTime() if err := touchFile(path); err != nil { t.Fatal(err) } st2, _ := os.Stat(path) if !st2.ModTime().After(old) && !st2.ModTime().Equal(old) { t.Errorf("mtime regressed: %v → %v", old, st2.ModTime()) } } func TestEstimateDBSize_ScalesWithGraph(t *testing.T) { small := graph.New() small.AddNode(&graph.Node{ID: "s1", Name: "s1", Kind: graph.KindFunction}) smallSize := estimateDBSize(small) large := graph.New() for i := range 100 { large.AddNode(&graph.Node{ID: funcID(i), Name: funcName(i), Kind: graph.KindFunction}) } largeSize := estimateDBSize(large) if smallSize <= 0 || largeSize <= 0 { t.Errorf("DB size estimates should be positive, got small=%d large=%d", smallSize, largeSize) } if largeSize <= smallSize { t.Errorf("larger graph should yield larger estimate, got small=%d large=%d", smallSize, largeSize) } } func TestDefaultRepoSet_IncludeLinuxFlag(t *testing.T) { without := defaultRepoSet(false) with := defaultRepoSet(true) if len(without) != 3 || len(with) != 4 { t.Errorf("expected 3 without linux + 4 with, got %d / %d", len(without), len(with)) } // linux must be in the includeLinux=true set, NOT in the false set. hasLinux := func(rs []repoSpec) bool { for _, r := range rs { if r.Slug == "linux" { return true } } return false } if hasLinux(without) { t.Error("defaultRepoSet(false) should not include linux") } if !hasLinux(with) { t.Error("defaultRepoSet(true) should include linux") } } // --- helpers -------------------------------------------------------- func funcID(i int) string { return "f.go::F" + itoa(i) } func funcName(i int) string { return "F" + itoa(i) } func fileID(i int) string { return "file" + itoa(i) + ".go" } func fileName(i int) string { return "file" + itoa(i) + ".go" } func itoa(i int) string { if i == 0 { return "0" } var out []byte for i > 0 { out = append([]byte{byte('0' + i%10)}, out...) i /= 10 } return string(out) }