internal/agent/reconcile/quota_test.go
Ref: Size: 4.0 KiB History
package reconcile
import (
"testing"
"github.com/a73x/eitri/internal/pb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// withRes overrides a desired VM's resource request.
func withRes(vcpus, memMB, diskGB int64) func(*pb.VMSpec) {
return func(v *pb.VMSpec) { v.Vcpus = vcpus; v.MemMb = memMB; v.DiskGb = diskGB }
}
func TestQuotaUnderCapBoots(t *testing.T) {
f := setup(t)
f.eng.MaxVCPUs = 4
rep := f.step(snap(1, vm("vm1", withRes(2, 512, 5))))
assert.Equal(t, []string{"vm1"}, f.prov.booted)
assert.Equal(t, "ready", findVM(rep, "vm1").Phase)
}
func TestQuotaOverCapRefusedNamesDimension(t *testing.T) {
f := setup(t)
f.eng.MaxVCPUs = 2
rep := f.step(snap(1, vm("vm1", withRes(4, 512, 5))))
assert.Empty(t, f.prov.booted, "an over-cap VM must not boot")
assert.Empty(t, f.prov.prepared, "an over-cap VM must not even prepare a disk")
av := findVM(rep, "vm1")
require.NotNil(t, av)
assert.Equal(t, "failed", av.Phase)
assert.Contains(t, av.GetLastError(), "capacity limit")
assert.Contains(t, av.GetLastError(), "vcpus", "error names the binding dimension")
}
func TestQuotaEnforcesMemAndDiskIndependently(t *testing.T) {
f := setup(t)
f.eng.MaxMemMB = 1024
rep := f.step(snap(1, vm("vm1", withRes(1, 4096, 5))))
assert.Empty(t, f.prov.booted)
assert.Contains(t, findVM(rep, "vm1").GetLastError(), "memory")
g := setup(t)
g.eng.MaxDiskGB = 10
rep = g.step(snap(1, vm("vm1", withRes(1, 512, 50))))
assert.Empty(t, g.prov.booted)
assert.Contains(t, findVM(rep, "vm1").GetLastError(), "disk")
}
func TestQuotaZeroMeansUnlimited(t *testing.T) {
f := setup(t) // caps default to 0
rep := f.step(snap(1, vm("vm1", withRes(999, 999999, 99999))))
assert.Equal(t, []string{"vm1"}, f.prov.booted, "no caps configured = unlimited")
assert.Equal(t, "ready", findVM(rep, "vm1").Phase)
}
func TestQuotaRefusalIsNonTerminal(t *testing.T) {
f := setup(t)
f.eng.MaxVCPUs = 2
// vm1 (2 vcpus) fills the cap exactly and boots.
f.step(snap(1, vm("vm1", withRes(2, 512, 5))))
require.Equal(t, []string{"vm1"}, f.prov.booted)
// vm2 (2 vcpus) would push the host to 4 > 2 — blocked. Repeat well past
// MaxCreateAttempts (3): quota refusal must NOT consume the retry budget.
for range 5 {
f.step(snap(2, vm("vm1", withRes(2, 512, 5)), vm("vm2", withRes(2, 512, 5))))
}
assert.Equal(t, []string{"vm1"}, f.prov.booted, "vm2 still blocked, vm1 untouched")
// Room frees (operator raises the cap). A blocked-forever VM whose budget
// had been burned would be terminal-failed; a non-terminal one boots now.
f.eng.MaxVCPUs = 10
f.step(snap(3, vm("vm1", withRes(2, 512, 5)), vm("vm2", withRes(2, 512, 5))))
assert.Equal(t, []string{"vm1", "vm2"}, f.prov.booted, "vm2 boots once room frees")
}
// TestQuotaFreedByQuarantineEventuallyBootsTheWaitingVM pins that quarantined
// VMs do not count against the host cap — and that the guarantee is EVENTUAL,
// not same-tick. Each VM reconciles independently, so vm2's admission may run
// before vm1's quarantine has released its compute; the refusal is
// non-terminal and the loop is level-triggered, so a later tick boots it.
func TestQuotaFreedByQuarantineEventuallyBootsTheWaitingVM(t *testing.T) {
f := setup(t)
f.eng.MaxVCPUs = 3
// vm1 (2 vcpus) boots and holds 2 of the 3-vcpu cap.
f.step(snap(1, vm("vm1", withRes(2, 512, 5))))
require.Equal(t, []string{"vm1"}, f.prov.booted)
// Tombstone vm1 (→ quarantined, compute released) and desire vm2 (2 vcpus).
// Counting the quarantined vm1 would give 4 > 3 and block vm2 forever.
// Two ticks suffice deterministically: whatever the map order, tick 1 reaps
// vm1 and releases its compute, so tick 2 always admits vm2. The third is
// slack, not a flake bound.
var rep *pb.Report
for range 3 {
rep = f.step(snap(2,
tombstoned(vm("vm1", withRes(2, 512, 5))),
vm("vm2", withRes(2, 512, 5))))
if findVM(rep, "vm2").GetPhase() == "ready" {
break
}
}
assert.Contains(t, f.prov.booted, "vm2", "quarantined vm1 must not count against the cap")
assert.Equal(t, "ready", findVM(rep, "vm2").GetPhase())
}