package reconcile import ( "errors" "sync" "testing" "time" ) // TestQueue_SameGuestSerialized asserts that jobs for one vmid run strictly // one-at-a-time in submit order — the core §10 guarantee that keeps Proxmox from // seeing concurrent conflicting ops on a guest. func TestQueue_SameGuestSerialized(t *testing.T) { q := NewQueue() defer q.Close() const n = 50 var mu sync.Mutex var order []int inside := 0 maxConcurrent := 0 chans := make([]<-chan error, n) for i := 0; i < n; i++ { i := i chans[i] = q.Submit(100, func() error { mu.Lock() inside++ if inside > maxConcurrent { maxConcurrent = inside } order = append(order, i) mu.Unlock() time.Sleep(time.Millisecond) // widen any overlap window mu.Lock() inside-- mu.Unlock() return nil }) } for _, ch := range chans { if err := <-ch; err != nil { t.Fatalf("unexpected job error: %v", err) } } if maxConcurrent != 1 { t.Errorf("same-guest jobs overlapped: maxConcurrent=%d, want 1", maxConcurrent) } for i := 0; i < n; i++ { if order[i] != i { t.Fatalf("same-guest jobs ran out of submit order: got %v", order) break } } } // TestQueue_DifferentGuestsParallel asserts independent vmids proceed concurrently: // two jobs on different lanes that each wait for the other before finishing must BOTH // complete (they'd deadlock under a global lock / single worker). func TestQueue_DifferentGuestsParallel(t *testing.T) { q := NewQueue() defer q.Close() aReady := make(chan struct{}) bReady := make(chan struct{}) chA := q.Submit(1, func() error { close(aReady) select { case <-bReady: return nil case <-time.After(2 * time.Second): return errors.New("guest 1 timed out waiting for guest 2 (not parallel)") } }) chB := q.Submit(2, func() error { close(bReady) select { case <-aReady: return nil case <-time.After(2 * time.Second): return errors.New("guest 2 timed out waiting for guest 1 (not parallel)") } }) if err := <-chA; err != nil { t.Error(err) } if err := <-chB; err != nil { t.Error(err) } } // TestQueue_PropagatesJobError confirms a job's error reaches its result channel. func TestQueue_PropagatesJobError(t *testing.T) { q := NewQueue() defer q.Close() want := errors.New("boom") if got := <-q.Submit(7, func() error { return want }); got != want { t.Errorf("Submit result = %v, want %v", got, want) } } // TestQueue_DrainsPendingOnClose confirms jobs already queued before Close still run. func TestQueue_DrainsPendingOnClose(t *testing.T) { q := NewQueue() release := make(chan struct{}) var ran sync.WaitGroup ran.Add(2) // First job blocks until released, pinning the lane so the second sits pending. ch1 := q.Submit(5, func() error { <-release; ran.Done(); return nil }) ch2 := q.Submit(5, func() error { ran.Done(); return nil }) q.Close() // close while job1 is queued/running and job2 is pending close(release) if err := <-ch1; err != nil { t.Errorf("job1 err: %v", err) } if err := <-ch2; err != nil { t.Errorf("pending job2 should still run after Close, got: %v", err) } ran.Wait() } // TestQueue_SubmitAfterClose returns ErrQueueClosed. func TestQueue_SubmitAfterClose(t *testing.T) { q := NewQueue() q.Close() if got := <-q.Submit(1, func() error { return nil }); got != ErrQueueClosed { t.Errorf("Submit after Close = %v, want ErrQueueClosed", got) } }