Files
beads/internal/rpc/bench_test.go
Steve Yegge 97e5df1180 Fix bd-1: Prevent test database pollution
Tests were connecting to test daemon but daemon routed to production DB via
findDatabaseForCwd(). Fixed by ensuring tests use isolated .beads directories
and change working directory to tmpDir.

Changes:
- bench_test.go: Added .beads subdir, chdir, and client.dbPath to setupBenchServer
- bench_test.go: Set dbPath for goroutine clients in BenchmarkConcurrentAgents
- comments_test.go: Refactored to use setupTestServer
- version_test.go: Fixed 4 tests to use setupTestServerIsolated with proper isolation
- rpc_test.go: Added setupTestServerIsolated() helper for custom test setup

Verified: RPC test suite runs with no database pollution (151→151 issues)
Amp-Thread-ID: https://ampcode.com/threads/T-348b7ba8-4292-4ed3-b143-0ad07d226c21
Co-authored-by: Amp <amp@ampcode.com>
2025-10-22 00:43:42 -07:00

340 lines
8.0 KiB
Go

//go:build bench
package rpc
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"
"time"
sqlitestorage "github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types"
)
// BenchmarkDirectCreate benchmarks direct SQLite create operations
func BenchmarkDirectCreate(b *testing.B) {
tmpDir, err := os.MkdirTemp("", "bd-bench-direct-*")
if err != nil {
b.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
dbPath := filepath.Join(tmpDir, "test.db")
store, err := sqlitestorage.New(dbPath)
if err != nil {
b.Fatalf("Failed to create store: %v", err)
}
defer store.Close()
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
issue := &types.Issue{
Title: fmt.Sprintf("Benchmark Issue %d", i),
Description: "Benchmark description",
IssueType: "task",
Priority: 2,
Status: types.StatusOpen,
}
if err := store.CreateIssue(ctx, issue, "benchmark"); err != nil {
b.Fatalf("Failed to create issue: %v", err)
}
}
}
// BenchmarkDaemonCreate benchmarks RPC create operations
func BenchmarkDaemonCreate(b *testing.B) {
_, client, cleanup, _ := setupBenchServer(b)
defer cleanup()
b.ResetTimer()
for i := 0; i < b.N; i++ {
args := &CreateArgs{
Title: fmt.Sprintf("Benchmark Issue %d", i),
Description: "Benchmark description",
IssueType: "task",
Priority: 2,
}
if _, err := client.Create(args); err != nil {
b.Fatalf("Failed to create issue: %v", err)
}
}
}
// BenchmarkDirectUpdate benchmarks direct SQLite update operations
func BenchmarkDirectUpdate(b *testing.B) {
tmpDir, err := os.MkdirTemp("", "bd-bench-direct-update-*")
if err != nil {
b.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
dbPath := filepath.Join(tmpDir, "test.db")
store, err := sqlitestorage.New(dbPath)
if err != nil {
b.Fatalf("Failed to create store: %v", err)
}
defer store.Close()
ctx := context.Background()
issue := &types.Issue{
Title: "Test Issue",
Description: "Test description",
IssueType: "task",
Priority: 2,
Status: types.StatusOpen,
}
if err := store.CreateIssue(ctx, issue, "benchmark"); err != nil {
b.Fatalf("Failed to create issue: %v", err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
updates := map[string]interface{}{
"title": fmt.Sprintf("Updated Issue %d", i),
}
if err := store.UpdateIssue(ctx, issue.ID, updates, "benchmark"); err != nil {
b.Fatalf("Failed to update issue: %v", err)
}
}
}
// BenchmarkDaemonUpdate benchmarks RPC update operations
func BenchmarkDaemonUpdate(b *testing.B) {
_, client, cleanup, _ := setupBenchServer(b)
defer cleanup()
createArgs := &CreateArgs{
Title: "Test Issue",
Description: "Test description",
IssueType: "task",
Priority: 2,
}
resp, err := client.Create(createArgs)
if err != nil {
b.Fatalf("Failed to create issue: %v", err)
}
var issue types.Issue
if err := json.Unmarshal(resp.Data, &issue); err != nil {
b.Fatalf("Failed to unmarshal issue: %v", err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
newTitle := fmt.Sprintf("Updated Issue %d", i)
args := &UpdateArgs{
ID: issue.ID,
Title: &newTitle,
}
if _, err := client.Update(args); err != nil {
b.Fatalf("Failed to update issue: %v", err)
}
}
}
// BenchmarkDirectList benchmarks direct SQLite list operations
func BenchmarkDirectList(b *testing.B) {
tmpDir, err := os.MkdirTemp("", "bd-bench-direct-list-*")
if err != nil {
b.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
dbPath := filepath.Join(tmpDir, "test.db")
store, err := sqlitestorage.New(dbPath)
if err != nil {
b.Fatalf("Failed to create store: %v", err)
}
defer store.Close()
ctx := context.Background()
for i := 0; i < 100; i++ {
issue := &types.Issue{
Title: fmt.Sprintf("Issue %d", i),
Description: "Test description",
IssueType: "task",
Priority: 2,
Status: types.StatusOpen,
}
if err := store.CreateIssue(ctx, issue, "benchmark"); err != nil {
b.Fatalf("Failed to create issue: %v", err)
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
filter := types.IssueFilter{Limit: 50}
if _, err := store.SearchIssues(ctx, "", filter); err != nil {
b.Fatalf("Failed to list issues: %v", err)
}
}
}
// BenchmarkDaemonList benchmarks RPC list operations
func BenchmarkDaemonList(b *testing.B) {
_, client, cleanup, _ := setupBenchServer(b)
defer cleanup()
for i := 0; i < 100; i++ {
args := &CreateArgs{
Title: fmt.Sprintf("Issue %d", i),
Description: "Test description",
IssueType: "task",
Priority: 2,
}
if _, err := client.Create(args); err != nil {
b.Fatalf("Failed to create issue: %v", err)
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
args := &ListArgs{Limit: 50}
if _, err := client.List(args); err != nil {
b.Fatalf("Failed to list issues: %v", err)
}
}
}
// BenchmarkDaemonLatency measures round-trip latency
func BenchmarkDaemonLatency(b *testing.B) {
_, client, cleanup, _ := setupBenchServer(b)
defer cleanup()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := client.Ping(); err != nil {
b.Fatalf("Ping failed: %v", err)
}
}
}
// BenchmarkConcurrentAgents benchmarks concurrent agent throughput
func BenchmarkConcurrentAgents(b *testing.B) {
server, _, cleanup, dbPath := setupBenchServer(b)
defer cleanup()
numAgents := 4
opsPerAgent := b.N / numAgents
b.ResetTimer()
done := make(chan bool, numAgents)
for i := 0; i < numAgents; i++ {
go func() {
client, err := TryConnect(server.socketPath)
if err != nil {
b.Errorf("Failed to connect: %v", err)
done <- false
return
}
defer client.Close()
// Set dbPath so client validates it's connected to the right daemon
client.dbPath = dbPath
for j := 0; j < opsPerAgent; j++ {
args := &CreateArgs{
Title: fmt.Sprintf("Issue %d", j),
IssueType: "task",
Priority: 2,
}
if _, err := client.Create(args); err != nil {
b.Errorf("Failed to create issue: %v", err)
done <- false
return
}
}
done <- true
}()
}
for i := 0; i < numAgents; i++ {
<-done
}
}
func setupBenchServer(b *testing.B) (*Server, *Client, func(), string) {
tmpDir, err := os.MkdirTemp("", "bd-rpc-bench-*")
if err != nil {
b.Fatalf("Failed to create temp dir: %v", err)
}
// Create .beads subdirectory so findDatabaseForCwd finds THIS database, not project's
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
os.RemoveAll(tmpDir)
b.Fatalf("Failed to create .beads dir: %v", err)
}
dbPath := filepath.Join(beadsDir, "test.db")
socketPath := filepath.Join(beadsDir, "bd.sock")
store, err := sqlitestorage.New(dbPath)
if err != nil {
os.RemoveAll(tmpDir)
b.Fatalf("Failed to create store: %v", err)
}
server := NewServer(socketPath, store)
ctx, cancel := context.WithCancel(context.Background())
go func() {
if err := server.Start(ctx); err != nil && err.Error() != "accept unix "+socketPath+": use of closed network connection" {
b.Logf("Server error: %v", err)
}
}()
time.Sleep(100 * time.Millisecond)
// Change to tmpDir so client's os.Getwd() finds the test database
originalWd, err := os.Getwd()
if err != nil {
cancel()
server.Stop()
store.Close()
os.RemoveAll(tmpDir)
b.Fatalf("Failed to get working directory: %v", err)
}
if err := os.Chdir(tmpDir); err != nil {
cancel()
server.Stop()
store.Close()
os.RemoveAll(tmpDir)
b.Fatalf("Failed to change directory: %v", err)
}
client, err := TryConnect(socketPath)
if err != nil {
cancel()
server.Stop()
store.Close()
os.Chdir(originalWd)
os.RemoveAll(tmpDir)
b.Fatalf("Failed to connect client: %v", err)
}
// Set the client's dbPath to the test database so it doesn't route to the wrong DB
client.dbPath = dbPath
cleanup := func() {
client.Close()
cancel()
server.Stop()
store.Close()
os.Chdir(originalWd) // Restore original working directory
os.RemoveAll(tmpDir)
}
return server, client, cleanup, dbPath
}