Pulse/internal/deploy/reservation_test.go
2026-03-18 16:06:30 +00:00

133 lines
3.6 KiB
Go

package deploy
import (
"testing"
"time"
)
func TestReserveAndRelease(t *testing.T) {
rm := NewReservationManager()
if err := rm.Reserve("j1", "org-1", 3, 30*time.Minute); err != nil {
t.Fatalf("Reserve: %v", err)
}
if got := rm.ReservedForOrg("org-1"); got != 3 {
t.Errorf("expected 3 reserved, got %d", got)
}
rm.Release("j1")
if got := rm.ReservedForOrg("org-1"); got != 0 {
t.Errorf("expected 0 after release, got %d", got)
}
}
func TestReserveDuplicate(t *testing.T) {
rm := NewReservationManager()
_ = rm.Reserve("j1", "org-1", 2, 30*time.Minute)
if err := rm.Reserve("j1", "org-1", 1, 30*time.Minute); err == nil {
t.Fatal("expected error for duplicate reservation")
}
}
func TestReserveInvalidSlots(t *testing.T) {
rm := NewReservationManager()
if err := rm.Reserve("j1", "org-1", 0, 30*time.Minute); err == nil {
t.Fatal("expected error for zero slots")
}
if err := rm.Reserve("j2", "org-1", -1, 30*time.Minute); err == nil {
t.Fatal("expected error for negative slots")
}
}
func TestMultiOrgIsolation(t *testing.T) {
rm := NewReservationManager()
_ = rm.Reserve("j1", "org-1", 3, 30*time.Minute)
_ = rm.Reserve("j2", "org-2", 5, 30*time.Minute)
if got := rm.ReservedForOrg("org-1"); got != 3 {
t.Errorf("expected 3 for org-1, got %d", got)
}
if got := rm.ReservedForOrg("org-2"); got != 5 {
t.Errorf("expected 5 for org-2, got %d", got)
}
if got := rm.ReservedForOrg("org-3"); got != 0 {
t.Errorf("expected 0 for unknown org, got %d", got)
}
}
func TestMultipleReservationsSameOrg(t *testing.T) {
rm := NewReservationManager()
_ = rm.Reserve("j1", "org-1", 2, 30*time.Minute)
_ = rm.Reserve("j2", "org-1", 3, 30*time.Minute)
if got := rm.ReservedForOrg("org-1"); got != 5 {
t.Errorf("expected 5 total, got %d", got)
}
rm.Release("j1")
if got := rm.ReservedForOrg("org-1"); got != 3 {
t.Errorf("expected 3 after partial release, got %d", got)
}
}
func TestReserveInvalidTTL(t *testing.T) {
rm := NewReservationManager()
if err := rm.Reserve("j1", "org-1", 3, 0); err == nil {
t.Fatal("expected error for zero TTL")
}
if err := rm.Reserve("j2", "org-1", 3, -1*time.Second); err == nil {
t.Fatal("expected error for negative TTL")
}
}
func TestTTLExpiry(t *testing.T) {
rm := NewReservationManager()
// Directly inject an already-expired reservation to test expiry counting.
rm.mu.Lock()
rm.reservations["j1"] = &Reservation{
JobID: "j1", OrgID: "org-1", Slots: 3,
CreatedAt: time.Now().UTC().Add(-2 * time.Minute),
ExpiresAt: time.Now().UTC().Add(-1 * time.Minute),
}
rm.mu.Unlock()
// Expired reservations should not be counted.
if got := rm.ReservedForOrg("org-1"); got != 0 {
t.Errorf("expected 0 for expired reservation, got %d", got)
}
}
func TestCleanExpired(t *testing.T) {
rm := NewReservationManager()
// Directly inject an already-expired reservation.
rm.mu.Lock()
rm.reservations["j1"] = &Reservation{
JobID: "j1", OrgID: "org-1", Slots: 2,
CreatedAt: time.Now().UTC().Add(-2 * time.Minute),
ExpiresAt: time.Now().UTC().Add(-1 * time.Minute),
}
rm.mu.Unlock()
_ = rm.Reserve("j2", "org-1", 3, 30*time.Minute) // still valid
rm.CleanExpired()
// j1 should be cleaned up, only j2 remains.
if got := rm.ReservedForOrg("org-1"); got != 3 {
t.Errorf("expected 3 after cleanup, got %d", got)
}
// Verify j1 was actually removed (can re-reserve that ID).
if err := rm.Reserve("j1", "org-1", 1, 30*time.Minute); err != nil {
t.Errorf("expected to re-reserve cleaned-up job, got: %v", err)
}
}
func TestReleaseNonexistent(t *testing.T) {
rm := NewReservationManager()
// Should not panic.
rm.Release("nonexistent")
}