From f18a7e262655d43a674fa4e094384d36bab9d581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torjus=20H=C3=A5kestad?= Date: Tue, 3 Feb 2026 17:47:07 +0100 Subject: [PATCH] test: add indexer benchmark and integration test - BenchmarkIndexRevision: benchmark full nixpkgs indexing - BenchmarkIndexRevisionWithFiles: benchmark with file content storage - TestIndexRevision: integration test for indexer - Uses nixpkgs revision from flake.lock (e6eae2ee...) - Skips if nix-build not available or in short mode Run with: go test -bench=BenchmarkIndexRevision -benchtime=1x -timeout=30m ./internal/nixos/... Co-Authored-By: Claude Opus 4.5 --- internal/nixos/indexer_test.go | 182 +++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 internal/nixos/indexer_test.go diff --git a/internal/nixos/indexer_test.go b/internal/nixos/indexer_test.go new file mode 100644 index 0000000..76cb49b --- /dev/null +++ b/internal/nixos/indexer_test.go @@ -0,0 +1,182 @@ +package nixos + +import ( + "context" + "os/exec" + "testing" + + "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) + } + + fileCount, err := indexer.IndexFiles(ctx, result.Revision.ID, TestNixpkgsRevision) + if err != nil { + b.Fatalf("IndexFiles failed: %v", err) + } + + b.ReportMetric(float64(result.OptionCount), "options") + 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)) +}