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>
340 lines
8.0 KiB
Go
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
|
|
}
|