| package bridge |
|
|
| import ( |
| "context" |
| "fmt" |
| "sync" |
| "sync/atomic" |
| "testing" |
| "time" |
| ) |
|
|
| |
|
|
| func TestTabExecutor_MultiTabSimulation(t *testing.T) { |
| |
| te := NewTabExecutor(4) |
| results := make(map[string][]int) |
| var mu sync.Mutex |
|
|
| var wg sync.WaitGroup |
| tabs := []string{"tab1", "tab2", "tab3"} |
|
|
| for _, tab := range tabs { |
| for step := 0; step < 5; step++ { |
| wg.Add(1) |
| tab, step := tab, step |
| go func() { |
| defer wg.Done() |
| err := te.Execute(context.Background(), tab, func(ctx context.Context) error { |
| time.Sleep(time.Duration(step) * time.Millisecond) |
| mu.Lock() |
| results[tab] = append(results[tab], step) |
| mu.Unlock() |
| return nil |
| }) |
| if err != nil { |
| t.Errorf("tab %s step %d: %v", tab, step, err) |
| } |
| }() |
| } |
| } |
| wg.Wait() |
|
|
| for _, tab := range tabs { |
| mu.Lock() |
| steps := results[tab] |
| mu.Unlock() |
| if len(steps) != 5 { |
| t.Errorf("tab %s: expected 5 steps, got %d", tab, len(steps)) |
| } |
| } |
| } |
|
|
| func TestTabExecutor_ErrorIsolation(t *testing.T) { |
| te := NewTabExecutor(4) |
|
|
| |
| err1 := te.Execute(context.Background(), "tab1", func(ctx context.Context) error { |
| return fmt.Errorf("tab1 error") |
| }) |
|
|
| |
| err2 := te.Execute(context.Background(), "tab2", func(ctx context.Context) error { |
| return nil |
| }) |
|
|
| if err1 == nil { |
| t.Error("expected error from tab1") |
| } |
| if err2 != nil { |
| t.Errorf("tab2 should succeed regardless of tab1: %v", err2) |
| } |
| } |
|
|
| func TestTabExecutor_PanicIsolation(t *testing.T) { |
| te := NewTabExecutor(4) |
|
|
| |
| err1 := te.Execute(context.Background(), "tab1", func(ctx context.Context) error { |
| panic("tab1 crashed") |
| }) |
|
|
| |
| err2 := te.Execute(context.Background(), "tab2", func(ctx context.Context) error { |
| return nil |
| }) |
|
|
| if err1 == nil { |
| t.Error("expected error from tab1 panic") |
| } |
| if err2 != nil { |
| t.Errorf("tab2 should succeed regardless of tab1 panic: %v", err2) |
| } |
| } |
|
|
| |
|
|
| func TestTabExecutor_StressHighConcurrency(t *testing.T) { |
| te := NewTabExecutor(4) |
| var completed int32 |
| var wg sync.WaitGroup |
|
|
| for i := 0; i < 50; i++ { |
| wg.Add(1) |
| tabID := fmt.Sprintf("tab%d", i%10) |
| go func() { |
| defer wg.Done() |
| err := te.Execute(context.Background(), tabID, func(ctx context.Context) error { |
| time.Sleep(time.Millisecond) |
| atomic.AddInt32(&completed, 1) |
| return nil |
| }) |
| if err != nil { |
| t.Errorf("unexpected error: %v", err) |
| } |
| }() |
| } |
| wg.Wait() |
|
|
| if n := atomic.LoadInt32(&completed); n != 50 { |
| t.Errorf("expected 50 completions, got %d", n) |
| } |
| } |
|
|
| func TestTabExecutor_StressRapidCreateRemove(t *testing.T) { |
| te := NewTabExecutor(4) |
| var wg sync.WaitGroup |
|
|
| for i := 0; i < 20; i++ { |
| wg.Add(1) |
| go func(i int) { |
| defer wg.Done() |
| tabID := fmt.Sprintf("tab_%d", i) |
| _ = te.Execute(context.Background(), tabID, func(ctx context.Context) error { |
| time.Sleep(time.Millisecond) |
| return nil |
| }) |
| te.RemoveTab(tabID) |
| }(i) |
| } |
| wg.Wait() |
| } |
|
|
| func TestTabExecutor_StressSameTabConcurrent(t *testing.T) { |
| te := NewTabExecutor(8) |
| var counter int32 |
| var wg sync.WaitGroup |
|
|
| |
| for i := 0; i < 30; i++ { |
| wg.Add(1) |
| go func() { |
| defer wg.Done() |
| _ = te.Execute(context.Background(), "single_tab", func(ctx context.Context) error { |
| atomic.AddInt32(&counter, 1) |
| return nil |
| }) |
| }() |
| } |
| wg.Wait() |
|
|
| if n := atomic.LoadInt32(&counter); n != 30 { |
| t.Errorf("expected 30 executions, got %d", n) |
| } |
| } |
|
|
| |
|
|
| func TestTabManager_ExecuteWithoutExecutor(t *testing.T) { |
| tm := &TabManager{ |
| tabs: make(map[string]*TabEntry), |
| snapshots: make(map[string]*RefCache), |
| executor: nil, |
| } |
| var executed bool |
| err := tm.Execute(context.Background(), "tab1", func(ctx context.Context) error { |
| executed = true |
| return nil |
| }) |
| if err != nil { |
| t.Fatalf("unexpected error: %v", err) |
| } |
| if !executed { |
| t.Error("task should execute directly when executor is nil") |
| } |
| } |
|
|
| func TestTabManager_ExecuteWithExecutor(t *testing.T) { |
| tm := &TabManager{ |
| tabs: make(map[string]*TabEntry), |
| snapshots: make(map[string]*RefCache), |
| executor: NewTabExecutor(2), |
| } |
| var executed bool |
| err := tm.Execute(context.Background(), "tab1", func(ctx context.Context) error { |
| executed = true |
| return nil |
| }) |
| if err != nil { |
| t.Fatalf("unexpected error: %v", err) |
| } |
| if !executed { |
| t.Error("task should be executed via executor") |
| } |
| } |
|
|
| func TestTabManager_ExecutorAccessor(t *testing.T) { |
| te := NewTabExecutor(3) |
| tm := &TabManager{ |
| tabs: make(map[string]*TabEntry), |
| snapshots: make(map[string]*RefCache), |
| executor: te, |
| } |
| if tm.Executor() != te { |
| t.Error("Executor() should return the configured TabExecutor") |
| } |
| } |
|
|
| func TestTabManager_ExecutorNilAccessor(t *testing.T) { |
| tm := &TabManager{ |
| tabs: make(map[string]*TabEntry), |
| snapshots: make(map[string]*RefCache), |
| } |
| if tm.Executor() != nil { |
| t.Error("Executor() should return nil when not configured") |
| } |
| } |
|
|
| func TestTabExecutor_ConcurrentRemoveAndExecute(t *testing.T) { |
| |
| |
| te := NewTabExecutor(4) |
|
|
| var wg sync.WaitGroup |
| for i := 0; i < 50; i++ { |
| wg.Add(2) |
| tabID := fmt.Sprintf("race_tab_%d", i) |
|
|
| go func() { |
| defer wg.Done() |
| _ = te.Execute(context.Background(), tabID, func(ctx context.Context) error { |
| time.Sleep(time.Millisecond) |
| return nil |
| }) |
| }() |
|
|
| go func() { |
| defer wg.Done() |
| time.Sleep(500 * time.Microsecond) |
| te.RemoveTab(tabID) |
| }() |
| } |
| wg.Wait() |
| } |
|
|
| func TestTabExecutor_RemoveTabDuringActiveExecution(t *testing.T) { |
| |
| te := NewTabExecutor(2) |
| taskStarted := make(chan struct{}) |
| taskDone := make(chan struct{}) |
| var taskCompleted bool |
|
|
| go func() { |
| _ = te.Execute(context.Background(), "active_tab", func(ctx context.Context) error { |
| close(taskStarted) |
| time.Sleep(50 * time.Millisecond) |
| taskCompleted = true |
| return nil |
| }) |
| close(taskDone) |
| }() |
|
|
| <-taskStarted |
| |
| te.RemoveTab("active_tab") |
|
|
| |
| if !taskCompleted { |
| t.Error("RemoveTab returned before active task completed") |
| } |
|
|
| <-taskDone |
| } |
|
|
| func TestTabExecutor_StatsUnderLoad(t *testing.T) { |
| te := NewTabExecutor(2) |
| started := make(chan struct{}, 2) |
|
|
| |
| var wg sync.WaitGroup |
| for i := 0; i < 2; i++ { |
| wg.Add(1) |
| go func(i int) { |
| defer wg.Done() |
| _ = te.Execute(context.Background(), fmt.Sprintf("stats_tab_%d", i), func(ctx context.Context) error { |
| started <- struct{}{} |
| time.Sleep(100 * time.Millisecond) |
| return nil |
| }) |
| }(i) |
| } |
|
|
| |
| <-started |
| <-started |
|
|
| stats := te.Stats() |
| if stats.SemaphoreUsed != 2 { |
| t.Errorf("expected 2 semaphore slots used, got %d", stats.SemaphoreUsed) |
| } |
| if stats.SemaphoreFree != 0 { |
| t.Errorf("expected 0 semaphore slots free, got %d", stats.SemaphoreFree) |
| } |
| if stats.ActiveTabs != 2 { |
| t.Errorf("expected 2 active tabs, got %d", stats.ActiveTabs) |
| } |
|
|
| wg.Wait() |
| } |
|
|