Files
beads/cmd/bd/daemon_debouncer_test.go
Steve Yegge 0fc4da7358 Optimize test suite performance (15-18x speedup)
- Add t.Parallel() to CLI and export/import tests for concurrent execution
- Remove unnecessary 200ms sleep in daemon_autoimport_test (Execute forces sync)
- Reduce filesystem settle wait from 100ms to 50ms on non-Windows
- Optimize debouncer test sleeps (9 reductions, 30-50% faster)

Results:
- cmd/bd: 5+ minutes → 18 seconds
- internal/importer: < 1 second
- Most packages: < 2 seconds

Closes bd-gpe7
2025-11-05 10:26:58 -08:00

193 lines
4.3 KiB
Go

package main
import (
"sync"
"sync/atomic"
"testing"
"time"
)
func TestDebouncer_BatchesMultipleTriggers(t *testing.T) {
var count int32
debouncer := NewDebouncer(50*time.Millisecond, func() {
atomic.AddInt32(&count, 1)
})
t.Cleanup(debouncer.Cancel)
debouncer.Trigger()
debouncer.Trigger()
debouncer.Trigger()
time.Sleep(20 * time.Millisecond)
if got := atomic.LoadInt32(&count); got != 0 {
t.Errorf("action fired too early: got %d, want 0", got)
}
time.Sleep(35 * time.Millisecond)
if got := atomic.LoadInt32(&count); got != 1 {
t.Errorf("action should have fired once: got %d, want 1", got)
}
}
func TestDebouncer_ResetsTimerOnSubsequentTriggers(t *testing.T) {
var count int32
debouncer := NewDebouncer(50*time.Millisecond, func() {
atomic.AddInt32(&count, 1)
})
t.Cleanup(debouncer.Cancel)
debouncer.Trigger()
time.Sleep(20 * time.Millisecond)
debouncer.Trigger()
time.Sleep(20 * time.Millisecond)
if got := atomic.LoadInt32(&count); got != 0 {
t.Errorf("action fired too early after timer reset: got %d, want 0", got)
}
time.Sleep(35 * time.Millisecond)
if got := atomic.LoadInt32(&count); got != 1 {
t.Errorf("action should have fired once after final timer: got %d, want 1", got)
}
}
func TestDebouncer_CancelDuringWait(t *testing.T) {
var count int32
debouncer := NewDebouncer(50*time.Millisecond, func() {
atomic.AddInt32(&count, 1)
})
t.Cleanup(debouncer.Cancel)
debouncer.Trigger()
time.Sleep(10 * time.Millisecond)
debouncer.Cancel()
time.Sleep(50 * time.Millisecond)
if got := atomic.LoadInt32(&count); got != 0 {
t.Errorf("action should not have fired after cancel: got %d, want 0", got)
}
}
func TestDebouncer_CancelWithNoPendingAction(t *testing.T) {
var count int32
debouncer := NewDebouncer(50*time.Millisecond, func() {
atomic.AddInt32(&count, 1)
})
t.Cleanup(debouncer.Cancel)
debouncer.Cancel()
debouncer.Trigger()
time.Sleep(60 * time.Millisecond)
if got := atomic.LoadInt32(&count); got != 1 {
t.Errorf("action should fire normally after cancel with no pending action: got %d, want 1", got)
}
}
func TestDebouncer_ThreadSafety(t *testing.T) {
var count int32
debouncer := NewDebouncer(50*time.Millisecond, func() {
atomic.AddInt32(&count, 1)
})
t.Cleanup(debouncer.Cancel)
var wg sync.WaitGroup
start := make(chan struct{})
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
<-start
debouncer.Trigger()
}()
}
close(start)
wg.Wait()
time.Sleep(70 * time.Millisecond)
got := atomic.LoadInt32(&count)
if got != 1 {
t.Errorf("all concurrent triggers should batch to exactly 1 action: got %d, want 1", got)
}
}
func TestDebouncer_ConcurrentCancelAndTrigger(t *testing.T) {
var count int32
debouncer := NewDebouncer(50*time.Millisecond, func() {
atomic.AddInt32(&count, 1)
})
t.Cleanup(debouncer.Cancel)
var wg sync.WaitGroup
numGoroutines := 50
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
if index%2 == 0 {
debouncer.Trigger()
} else {
debouncer.Cancel()
}
}(i)
}
wg.Wait()
debouncer.Cancel()
time.Sleep(100 * time.Millisecond)
got := atomic.LoadInt32(&count)
if got != 0 && got != 1 {
t.Errorf("unexpected action count with concurrent cancel/trigger: got %d, want 0 or 1", got)
}
}
func TestDebouncer_MultipleSequentialTriggerCycles(t *testing.T) {
var count int32
debouncer := NewDebouncer(30*time.Millisecond, func() {
atomic.AddInt32(&count, 1)
})
t.Cleanup(debouncer.Cancel)
debouncer.Trigger()
time.Sleep(40 * time.Millisecond)
if got := atomic.LoadInt32(&count); got != 1 {
t.Errorf("first cycle: got %d, want 1", got)
}
debouncer.Trigger()
time.Sleep(40 * time.Millisecond)
if got := atomic.LoadInt32(&count); got != 2 {
t.Errorf("second cycle: got %d, want 2", got)
}
debouncer.Trigger()
time.Sleep(40 * time.Millisecond)
if got := atomic.LoadInt32(&count); got != 3 {
t.Errorf("third cycle: got %d, want 3", got)
}
}
func TestDebouncer_CancelImmediatelyAfterTrigger(t *testing.T) {
var count int32
debouncer := NewDebouncer(50*time.Millisecond, func() {
atomic.AddInt32(&count, 1)
})
t.Cleanup(debouncer.Cancel)
debouncer.Trigger()
debouncer.Cancel()
time.Sleep(60 * time.Millisecond)
if got := atomic.LoadInt32(&count); got != 0 {
t.Errorf("action should not fire after immediate cancel: got %d, want 0", got)
}
}