a73x

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())
}