package zitcli
import (
"context"
"errors"
"testing"
"time"
"craftdesign.group/quarry/internal/ops"
)
// The gate admits its bound and no more: the next caller waits, and a
// caller whose request is cancelled while waiting is told busy rather
// than queued. Busy is an ErrUnavailable, so the surfaces above answer
// 503 with Retry-After.
func TestAdmissionBoundsSubprocesses(t *testing.T) {
a := newAdmission(2)
if !a.acquire(context.Background()) || !a.acquire(context.Background()) {
t.Fatal("the first two callers were refused")
}
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
start := time.Now()
if a.acquire(ctx) {
t.Fatal("a third caller was admitted past the bound")
}
if time.Since(start) > busyWait {
t.Errorf("a cancelled caller waited the full busyWait")
}
a.release()
if !a.acquire(context.Background()) {
t.Fatal("a released slot was not reusable")
}
// The lane counts what happened, for the ops page.
if a.admitted != 3 || a.refused != 1 {
t.Errorf("lane counted admitted=%d refused=%d, want 3 and 1", a.admitted, a.refused)
}
if !errors.Is(ErrBusy, ops.ErrUnavailable) {
t.Errorf("ErrBusy is not an ErrUnavailable")
}
}
// A command minted while the gate is full fails at once with ErrBusy
// instead of spawning.
func TestCommandFailsBusyWhenTheGateIsFull(t *testing.T) {
old := Parallel()
SetParallel(1)
t.Cleanup(func() { SetParallel(old) })
if !gate.acquire(context.Background()) {
t.Fatal("could not fill the gate")
}
defer gate.release()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
defer cancel()
cmd, done := New(t.TempDir()).command(ctx, "version")
defer done()
if _, err := cmd.Output(); !errors.Is(err, ErrBusy) {
t.Fatalf("command past the bound ran or failed otherwise: %v", err)
}
}