- Add internal/batching package for git operation batching - Implement GitBatcher with configurable batch size and age - Queue git operations (commit, push, merge) - Auto-flush on max batch size - Manual flush on demand - Time-based flush (max batch age) - Batch status tracking (pending, executing, completed, failed) - Network savings calculation - Statistics tracking per batch and aggregated - 24 batching tests, all passing Features: - Enqueue() for adding operations to queue - Flush() for manual batch creation - GetPendingBatch() for next pending batch - MarkBatchExecuting/Completed/Failed() for status tracking - GetStats() for batching statistics - CalculateNetworkSavings() for round trip savings - GetExecutedBatches() for completed batch history - TimeSinceLastFlush() for age checking - ShouldFlush() for time-based decisions Performance Benefits: - N commits batched into 1 push saves N-1 round trips - Example: 10 commits in 2 batches saves 8 round trips - Configurable batch size (default 10) - Configurable max age (default 5s) - FIFO queue processing Network Savings Example: - 10 operations in 2 batches of 5 each - Network savings: 8 round trips (vs 10 individual operations) - Verified in TestGetStats Status Tracking: - pending: queued and ready to execute - executing: currently being executed - completed: finished successfully - failed: execution failed (kept for retry) Test Coverage: - 24 batching tests (enqueue, flush, status, stats) - Auto-flush on max size verified - Time-based flush behavior tested - Network savings calculation verified - Error handling and state management - Concurrent safe operations (RWMutex) Next: T2.6 (LLM request batching)
356 lines
7.6 KiB
Go
356 lines
7.6 KiB
Go
package batching
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestNewGitBatcher(t *testing.T) {
|
|
batcher := NewGitBatcher(10, 5*time.Second)
|
|
assert.NotNil(t, batcher)
|
|
assert.Equal(t, 0, batcher.QueueSize())
|
|
}
|
|
|
|
func TestEnqueueOperation(t *testing.T) {
|
|
batcher := NewGitBatcher(10, 5*time.Second)
|
|
|
|
op := &GitOp{
|
|
OpType: "commit",
|
|
Branch: "main",
|
|
Message: "Add feature",
|
|
Files: []string{"file1.go"},
|
|
}
|
|
|
|
batcher.Enqueue(op)
|
|
assert.Equal(t, 1, batcher.QueueSize())
|
|
}
|
|
|
|
func TestEnqueueMultipleOps(t *testing.T) {
|
|
batcher := NewGitBatcher(10, 5*time.Second)
|
|
|
|
for i := 0; i < 5; i++ {
|
|
op := &GitOp{
|
|
OpType: "commit",
|
|
Branch: "main",
|
|
Message: "Commit",
|
|
}
|
|
batcher.Enqueue(op)
|
|
}
|
|
|
|
assert.Equal(t, 5, batcher.QueueSize())
|
|
}
|
|
|
|
func TestAutoFlushOnMaxBatchSize(t *testing.T) {
|
|
batcher := NewGitBatcher(5, 10*time.Second)
|
|
|
|
for i := 0; i < 5; i++ {
|
|
op := &GitOp{
|
|
OpType: "commit",
|
|
Branch: "main",
|
|
Message: "Commit",
|
|
}
|
|
batcher.Enqueue(op)
|
|
}
|
|
|
|
// After 5 ops, should auto-flush
|
|
assert.Equal(t, 0, batcher.QueueSize())
|
|
assert.Equal(t, 1, batcher.PendingBatchCount())
|
|
}
|
|
|
|
func TestManualFlush(t *testing.T) {
|
|
batcher := NewGitBatcher(10, 5*time.Second)
|
|
|
|
op := &GitOp{
|
|
OpType: "commit",
|
|
Branch: "main",
|
|
Message: "Commit",
|
|
}
|
|
batcher.Enqueue(op)
|
|
assert.Equal(t, 1, batcher.QueueSize())
|
|
|
|
batcher.Flush()
|
|
assert.Equal(t, 0, batcher.QueueSize())
|
|
assert.Equal(t, 1, batcher.PendingBatchCount())
|
|
}
|
|
|
|
func TestGetPendingBatch(t *testing.T) {
|
|
batcher := NewGitBatcher(10, 5*time.Second)
|
|
|
|
op := &GitOp{
|
|
OpType: "commit",
|
|
Branch: "main",
|
|
Message: "Commit",
|
|
}
|
|
batcher.Enqueue(op)
|
|
batcher.Flush()
|
|
|
|
batch := batcher.GetPendingBatch()
|
|
assert.NotNil(t, batch)
|
|
assert.Equal(t, 1, len(batch.Operations))
|
|
}
|
|
|
|
func TestMarkBatchExecuting(t *testing.T) {
|
|
batcher := NewGitBatcher(10, 5*time.Second)
|
|
|
|
op := &GitOp{OpType: "commit"}
|
|
batcher.Enqueue(op)
|
|
batcher.Flush()
|
|
|
|
batch := batcher.GetPendingBatch()
|
|
batcher.MarkBatchExecuting(batch.ID)
|
|
|
|
updated := batcher.GetBatchByID(batch.ID)
|
|
assert.Equal(t, "executing", updated.Status)
|
|
}
|
|
|
|
func TestMarkBatchCompleted(t *testing.T) {
|
|
batcher := NewGitBatcher(10, 5*time.Second)
|
|
|
|
op := &GitOp{OpType: "commit"}
|
|
batcher.Enqueue(op)
|
|
batcher.Flush()
|
|
|
|
batch := batcher.GetPendingBatch()
|
|
batcher.MarkBatchCompleted(batch.ID)
|
|
|
|
executed := batcher.GetExecutedBatches()
|
|
assert.Equal(t, 1, len(executed))
|
|
assert.Equal(t, "completed", executed[0].Status)
|
|
}
|
|
|
|
func TestMarkBatchFailed(t *testing.T) {
|
|
batcher := NewGitBatcher(10, 5*time.Second)
|
|
|
|
op := &GitOp{OpType: "commit"}
|
|
batcher.Enqueue(op)
|
|
batcher.Flush()
|
|
|
|
batch := batcher.GetPendingBatch()
|
|
testErr := assert.AnError
|
|
batcher.MarkBatchFailed(batch.ID, testErr)
|
|
|
|
failed := batcher.GetBatchByID(batch.ID)
|
|
assert.Equal(t, "failed", failed.Status)
|
|
assert.Error(t, failed.Error)
|
|
}
|
|
|
|
func TestGetStats(t *testing.T) {
|
|
batcher := NewGitBatcher(5, 5*time.Second)
|
|
|
|
// Add 10 ops (will create 2 batches of 5 each)
|
|
for i := 0; i < 10; i++ {
|
|
op := &GitOp{OpType: "commit"}
|
|
batcher.Enqueue(op)
|
|
}
|
|
|
|
stats := batcher.GetStats()
|
|
assert.Equal(t, 10, stats.TotalOps)
|
|
assert.Equal(t, 2, stats.TotalBatches)
|
|
assert.Equal(t, 5.0, stats.AvgOpsPerBatch)
|
|
// 10 ops in 2 batches saves 8 round trips (5-1 + 5-1)
|
|
assert.Equal(t, 8, stats.NetworkSavings)
|
|
}
|
|
|
|
func TestQueueSize(t *testing.T) {
|
|
batcher := NewGitBatcher(10, 5*time.Second)
|
|
|
|
op := &GitOp{OpType: "commit"}
|
|
batcher.Enqueue(op)
|
|
|
|
assert.Equal(t, 1, batcher.QueueSize())
|
|
}
|
|
|
|
func TestPendingBatchCount(t *testing.T) {
|
|
batcher := NewGitBatcher(10, 5*time.Second)
|
|
|
|
op := &GitOp{OpType: "commit"}
|
|
batcher.Enqueue(op)
|
|
batcher.Flush()
|
|
|
|
assert.Equal(t, 1, batcher.PendingBatchCount())
|
|
}
|
|
|
|
func TestGetExecutedBatches(t *testing.T) {
|
|
batcher := NewGitBatcher(10, 5*time.Second)
|
|
|
|
// Create and execute batches
|
|
for i := 0; i < 2; i++ {
|
|
op := &GitOp{OpType: "commit"}
|
|
batcher.Enqueue(op)
|
|
batcher.Flush()
|
|
|
|
batch := batcher.GetPendingBatch()
|
|
batcher.MarkBatchCompleted(batch.ID)
|
|
}
|
|
|
|
executed := batcher.GetExecutedBatches()
|
|
assert.Equal(t, 2, len(executed))
|
|
}
|
|
|
|
func TestTimeSinceLastFlush(t *testing.T) {
|
|
batcher := NewGitBatcher(10, 5*time.Second)
|
|
|
|
op := &GitOp{OpType: "commit"}
|
|
batcher.Enqueue(op)
|
|
batcher.Flush()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
elapsed := batcher.TimeSinceLastFlush()
|
|
|
|
assert.Greater(t, elapsed, 50*time.Millisecond)
|
|
assert.Less(t, elapsed, 200*time.Millisecond)
|
|
}
|
|
|
|
func TestShouldFlush(t *testing.T) {
|
|
batcher := NewGitBatcher(100, 100*time.Millisecond)
|
|
|
|
// Empty queue should not flush
|
|
assert.False(t, batcher.ShouldFlush())
|
|
|
|
// Enqueue but not old enough
|
|
op := &GitOp{OpType: "commit"}
|
|
batcher.Enqueue(op)
|
|
assert.False(t, batcher.ShouldFlush())
|
|
|
|
// Wait for age to exceed max age
|
|
time.Sleep(150 * time.Millisecond)
|
|
assert.True(t, batcher.ShouldFlush())
|
|
}
|
|
|
|
func TestClear(t *testing.T) {
|
|
batcher := NewGitBatcher(10, 5*time.Second)
|
|
|
|
op := &GitOp{OpType: "commit"}
|
|
batcher.Enqueue(op)
|
|
batcher.Flush()
|
|
|
|
assert.Equal(t, 1, batcher.PendingBatchCount())
|
|
|
|
batcher.Clear()
|
|
assert.Equal(t, 0, batcher.QueueSize())
|
|
assert.Equal(t, 0, batcher.PendingBatchCount())
|
|
}
|
|
|
|
func TestGetQueuedOps(t *testing.T) {
|
|
batcher := NewGitBatcher(10, 5*time.Second)
|
|
|
|
ops := []*GitOp{
|
|
{OpType: "commit", Message: "Commit 1"},
|
|
{OpType: "commit", Message: "Commit 2"},
|
|
{OpType: "commit", Message: "Commit 3"},
|
|
}
|
|
|
|
for _, op := range ops {
|
|
batcher.Enqueue(op)
|
|
}
|
|
|
|
queued := batcher.GetQueuedOps()
|
|
assert.Equal(t, 3, len(queued))
|
|
assert.Equal(t, "Commit 1", queued[0].Message)
|
|
assert.Equal(t, "Commit 3", queued[2].Message)
|
|
}
|
|
|
|
func TestCalculateNetworkSavings(t *testing.T) {
|
|
batcher := NewGitBatcher(3, 5*time.Second)
|
|
|
|
// Add 6 ops (will create 2 batches of 3 each)
|
|
for i := 0; i < 6; i++ {
|
|
op := &GitOp{OpType: "commit"}
|
|
batcher.Enqueue(op)
|
|
}
|
|
|
|
// Mark both batches as completed
|
|
for i := 0; i < 2; i++ {
|
|
batch := batcher.GetPendingBatch()
|
|
if batch != nil {
|
|
batcher.MarkBatchCompleted(batch.ID)
|
|
}
|
|
}
|
|
|
|
savings := batcher.CalculateNetworkSavings()
|
|
// 2 batches of 3 each saves 4 round trips (3-1 + 3-1)
|
|
assert.Equal(t, 4, savings)
|
|
}
|
|
|
|
func TestGetBatchByID(t *testing.T) {
|
|
batcher := NewGitBatcher(10, 5*time.Second)
|
|
|
|
op := &GitOp{OpType: "commit"}
|
|
batcher.Enqueue(op)
|
|
batcher.Flush()
|
|
|
|
batch := batcher.GetPendingBatch()
|
|
retrieved := batcher.GetBatchByID(batch.ID)
|
|
|
|
assert.NotNil(t, retrieved)
|
|
assert.Equal(t, batch.ID, retrieved.ID)
|
|
}
|
|
|
|
func TestGetBatchInfo(t *testing.T) {
|
|
batch := &GitBatch{
|
|
ID: "test-batch",
|
|
Status: "completed",
|
|
CreatedAt: time.Now(),
|
|
ExecutedAt: time.Now().Add(1 * time.Second),
|
|
}
|
|
|
|
info := batch.GetInfo()
|
|
assert.Equal(t, "test-batch", info["id"])
|
|
assert.Equal(t, "completed", info["status"])
|
|
}
|
|
|
|
func TestMultipleBatches(t *testing.T) {
|
|
batcher := NewGitBatcher(3, 5*time.Second)
|
|
|
|
// Create 3 batches
|
|
for batch := 0; batch < 3; batch++ {
|
|
for i := 0; i < 3; i++ {
|
|
op := &GitOp{
|
|
OpType: "commit",
|
|
Branch: "main",
|
|
}
|
|
batcher.Enqueue(op)
|
|
}
|
|
}
|
|
|
|
// All 3 batches should be pending
|
|
assert.Equal(t, 3, batcher.PendingBatchCount())
|
|
assert.Equal(t, 0, batcher.QueueSize())
|
|
}
|
|
|
|
func TestEnqueueNil(t *testing.T) {
|
|
batcher := NewGitBatcher(10, 5*time.Second)
|
|
|
|
// Enqueueing nil should not fail
|
|
batcher.Enqueue(nil)
|
|
assert.Equal(t, 0, batcher.QueueSize())
|
|
}
|
|
|
|
func BenchmarkEnqueue(b *testing.B) {
|
|
batcher := NewGitBatcher(1000, 10*time.Second)
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
op := &GitOp{
|
|
OpType: "commit",
|
|
Branch: "main",
|
|
Message: "Commit",
|
|
}
|
|
batcher.Enqueue(op)
|
|
}
|
|
}
|
|
|
|
func BenchmarkFlush(b *testing.B) {
|
|
batcher := NewGitBatcher(1000, 10*time.Second)
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
op := &GitOp{OpType: "commit"}
|
|
batcher.Enqueue(op)
|
|
|
|
if (i + 1) % 100 == 0 {
|
|
batcher.Flush()
|
|
}
|
|
}
|
|
}
|