- BenchmarkIndexRevisionWithFiles now reports separate timing for options indexing (options_ms) and file indexing (files_ms) - Add BenchmarkIndexFilesOnly to measure file indexing in isolation Run with: go test -bench=BenchmarkIndexFilesOnly -benchtime=1x -timeout=60m ./internal/nixos/... Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
227 lines
6.7 KiB
Go
227 lines
6.7 KiB
Go
package nixos
|
|
|
|
import (
|
|
"context"
|
|
"os/exec"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.t-juice.club/torjus/labmcp/internal/database"
|
|
)
|
|
|
|
// TestNixpkgsRevision is the revision from flake.lock used for testing.
|
|
const TestNixpkgsRevision = "e6eae2ee2110f3d31110d5c222cd395303343b08"
|
|
|
|
// BenchmarkIndexRevision benchmarks indexing a full nixpkgs revision.
|
|
// This is a slow benchmark that requires nix to be installed.
|
|
// Run with: go test -bench=BenchmarkIndexRevision -benchtime=1x -timeout=30m ./internal/nixos/...
|
|
func BenchmarkIndexRevision(b *testing.B) {
|
|
// Check if nix-build is available
|
|
if _, err := exec.LookPath("nix-build"); err != nil {
|
|
b.Skip("nix-build not found, skipping indexer benchmark")
|
|
}
|
|
|
|
// Use in-memory SQLite for the benchmark
|
|
store, err := database.NewSQLiteStore(":memory:")
|
|
if err != nil {
|
|
b.Fatalf("Failed to create store: %v", err)
|
|
}
|
|
defer store.Close()
|
|
|
|
ctx := context.Background()
|
|
if err := store.Initialize(ctx); err != nil {
|
|
b.Fatalf("Failed to initialize store: %v", err)
|
|
}
|
|
|
|
indexer := NewIndexer(store)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
// Delete any existing revision first (for repeated runs)
|
|
if rev, _ := store.GetRevision(ctx, TestNixpkgsRevision); rev != nil {
|
|
store.DeleteRevision(ctx, rev.ID)
|
|
}
|
|
|
|
result, err := indexer.IndexRevision(ctx, TestNixpkgsRevision)
|
|
if err != nil {
|
|
b.Fatalf("IndexRevision failed: %v", err)
|
|
}
|
|
|
|
b.ReportMetric(float64(result.OptionCount), "options")
|
|
b.ReportMetric(float64(result.Duration.Milliseconds()), "ms")
|
|
}
|
|
}
|
|
|
|
// BenchmarkIndexRevisionWithFiles benchmarks indexing with file content storage.
|
|
// This downloads the full nixpkgs tarball and stores allowed file types.
|
|
// Run with: go test -bench=BenchmarkIndexRevisionWithFiles -benchtime=1x -timeout=60m ./internal/nixos/...
|
|
func BenchmarkIndexRevisionWithFiles(b *testing.B) {
|
|
// Check if nix-build is available
|
|
if _, err := exec.LookPath("nix-build"); err != nil {
|
|
b.Skip("nix-build not found, skipping indexer benchmark")
|
|
}
|
|
|
|
// Use in-memory SQLite for the benchmark
|
|
store, err := database.NewSQLiteStore(":memory:")
|
|
if err != nil {
|
|
b.Fatalf("Failed to create store: %v", err)
|
|
}
|
|
defer store.Close()
|
|
|
|
ctx := context.Background()
|
|
if err := store.Initialize(ctx); err != nil {
|
|
b.Fatalf("Failed to initialize store: %v", err)
|
|
}
|
|
|
|
indexer := NewIndexer(store)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
// Delete any existing revision first
|
|
if rev, _ := store.GetRevision(ctx, TestNixpkgsRevision); rev != nil {
|
|
store.DeleteRevision(ctx, rev.ID)
|
|
}
|
|
|
|
result, err := indexer.IndexRevision(ctx, TestNixpkgsRevision)
|
|
if err != nil {
|
|
b.Fatalf("IndexRevision failed: %v", err)
|
|
}
|
|
|
|
fileStart := time.Now()
|
|
fileCount, err := indexer.IndexFiles(ctx, result.Revision.ID, TestNixpkgsRevision)
|
|
if err != nil {
|
|
b.Fatalf("IndexFiles failed: %v", err)
|
|
}
|
|
fileDuration := time.Since(fileStart)
|
|
|
|
b.ReportMetric(float64(result.OptionCount), "options")
|
|
b.ReportMetric(float64(result.Duration.Milliseconds()), "options_ms")
|
|
b.ReportMetric(float64(fileCount), "files")
|
|
b.ReportMetric(float64(fileDuration.Milliseconds()), "files_ms")
|
|
}
|
|
}
|
|
|
|
// BenchmarkIndexFilesOnly benchmarks only the file indexing phase.
|
|
// Assumes options are already indexed. Useful for measuring file indexing in isolation.
|
|
// Run with: go test -bench=BenchmarkIndexFilesOnly -benchtime=1x -timeout=60m ./internal/nixos/...
|
|
func BenchmarkIndexFilesOnly(b *testing.B) {
|
|
if _, err := exec.LookPath("nix-build"); err != nil {
|
|
b.Skip("nix-build not found, skipping indexer benchmark")
|
|
}
|
|
|
|
store, err := database.NewSQLiteStore(":memory:")
|
|
if err != nil {
|
|
b.Fatalf("Failed to create store: %v", err)
|
|
}
|
|
defer store.Close()
|
|
|
|
ctx := context.Background()
|
|
if err := store.Initialize(ctx); err != nil {
|
|
b.Fatalf("Failed to initialize store: %v", err)
|
|
}
|
|
|
|
indexer := NewIndexer(store)
|
|
|
|
// Index options first (outside of benchmark timing)
|
|
result, err := indexer.IndexRevision(ctx, TestNixpkgsRevision)
|
|
if err != nil {
|
|
b.Fatalf("IndexRevision failed: %v", err)
|
|
}
|
|
b.Logf("Pre-indexed %d options", result.OptionCount)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
fileCount, err := indexer.IndexFiles(ctx, result.Revision.ID, TestNixpkgsRevision)
|
|
if err != nil {
|
|
b.Fatalf("IndexFiles failed: %v", err)
|
|
}
|
|
|
|
b.ReportMetric(float64(fileCount), "files")
|
|
}
|
|
}
|
|
|
|
// TestIndexRevision is an integration test for the indexer.
|
|
// Run with: go test -run=TestIndexRevision -timeout=30m ./internal/nixos/...
|
|
func TestIndexRevision(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test in short mode")
|
|
}
|
|
|
|
// Check if nix-build is available
|
|
if _, err := exec.LookPath("nix-build"); err != nil {
|
|
t.Skip("nix-build not found, skipping indexer test")
|
|
}
|
|
|
|
store, err := database.NewSQLiteStore(":memory:")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create store: %v", err)
|
|
}
|
|
defer store.Close()
|
|
|
|
ctx := context.Background()
|
|
if err := store.Initialize(ctx); err != nil {
|
|
t.Fatalf("Failed to initialize store: %v", err)
|
|
}
|
|
|
|
indexer := NewIndexer(store)
|
|
|
|
t.Logf("Indexing nixpkgs revision %s...", TestNixpkgsRevision[:12])
|
|
result, err := indexer.IndexRevision(ctx, TestNixpkgsRevision)
|
|
if err != nil {
|
|
t.Fatalf("IndexRevision failed: %v", err)
|
|
}
|
|
|
|
t.Logf("Indexed %d options in %s", result.OptionCount, result.Duration)
|
|
|
|
// Verify we got a reasonable number of options (NixOS has thousands)
|
|
if result.OptionCount < 1000 {
|
|
t.Errorf("Expected at least 1000 options, got %d", result.OptionCount)
|
|
}
|
|
|
|
// Verify revision was stored
|
|
rev, err := store.GetRevision(ctx, TestNixpkgsRevision)
|
|
if err != nil {
|
|
t.Fatalf("GetRevision failed: %v", err)
|
|
}
|
|
if rev == nil {
|
|
t.Fatal("Revision not found after indexing")
|
|
}
|
|
if rev.OptionCount != result.OptionCount {
|
|
t.Errorf("Stored option count %d != result count %d", rev.OptionCount, result.OptionCount)
|
|
}
|
|
|
|
// Test searching
|
|
options, err := store.SearchOptions(ctx, rev.ID, "nginx", database.SearchFilters{Limit: 10})
|
|
if err != nil {
|
|
t.Fatalf("SearchOptions failed: %v", err)
|
|
}
|
|
if len(options) == 0 {
|
|
t.Error("Expected to find nginx options")
|
|
}
|
|
t.Logf("Found %d nginx options", len(options))
|
|
|
|
// Test getting a specific option
|
|
opt, err := store.GetOption(ctx, rev.ID, "services.nginx.enable")
|
|
if err != nil {
|
|
t.Fatalf("GetOption failed: %v", err)
|
|
}
|
|
if opt == nil {
|
|
t.Error("services.nginx.enable not found")
|
|
} else {
|
|
t.Logf("services.nginx.enable: type=%s", opt.Type)
|
|
if opt.Type != "boolean" {
|
|
t.Errorf("Expected type 'boolean', got %q", opt.Type)
|
|
}
|
|
}
|
|
|
|
// Test getting children
|
|
children, err := store.GetChildren(ctx, rev.ID, "services.nginx")
|
|
if err != nil {
|
|
t.Fatalf("GetChildren failed: %v", err)
|
|
}
|
|
if len(children) == 0 {
|
|
t.Error("Expected services.nginx to have children")
|
|
}
|
|
t.Logf("services.nginx has %d direct children", len(children))
|
|
}
|