Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions go/internal/runtime/contract_microvm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//go:build microvm && unix

package runtime

// The microVM leg of the shared ContainerRuntime contract suite (record §U5):
// runs runContractSuite against a real MicroVMRuntime on live hardware, gated on
// microvmtest.Require(t) (skip-on-absent-KVM, hard-fail under
// COMPASS_REQUIRE_MICROVM=1). It supplies the microVM caps encoding all 6
// conceded divergences (record 580-593) as ON flags, so every divergence row
// runs here and a silent widening fails. It ALSO records the Q-budget numbers
// (record 832-834): boot latency (Start wall-clock) and per-process PSS, emitted
// via t.Logf as informational spike output per §(g) — NOT a boot gate.
//
// The Create→Start→Exec→Stop→Remove happy path and the ExecStreaming-kill path
// that used to live in microvm_lifecycle_microvm_test.go are now shared contract
// rows and were folded out of that file; e2eConfig and
// TestMicroVMStartFailureLeavesNoState (a microVM-backend-only negative with no
// podman analog) stay there.

import (
"errors"
"testing"
"time"

"github.com/RigelBuild/compass/go/internal/microvmtest"
)

// TestContractSuite_MicroVM drives the shared contract rows against a live
// MicroVMRuntime through the ContainerRuntime interface. The factory builds a
// runtime from the resolved test env; sessions are created with a single
// /workspace virtio-fs share and uid 1000. All divergence caps are ON, so the
// microVM-specific rows (output cap, non-numeric user, empty MountLabel, ignored
// Command/CapAdd, graceful power-off, portable kill error) all run.
func TestContractSuite_MicroVM(t *testing.T) {
env := microvmtest.Require(t)

caps := backendCaps{
name: "microvm",
makeSpec: func(t *testing.T, name string) ContainerSpec {
t.Helper()
return ContainerSpec{
Name: name,
UID: 1000,
Mounts: []Mount{{HostPath: t.TempDir(), ContainerPath: "/workspace"}},
}
},
// All 6 conceded divergences hold on the microVM backend (record
// 580-593): each ON flag runs its row so a silent widening fails.
refusesRootExec: true,
numericUIDOnly: true,
emptyMountLabel: true,
ignoresCommandAndCapAdd: true,
capsOutput: true,
gracefulStopPowersOff: true,
portableKillError: true,
assertDuplicateName: func(t *testing.T, err error) {
t.Helper()
var dup *DuplicateNameError
if !errors.As(err, &dup) {
t.Fatalf("duplicate-name Create error = %v (%T), want *DuplicateNameError", err, err)
}
},
}

runContractSuite(t, func(t *testing.T) ContainerRuntime {
t.Helper()
return NewMicroVMRuntime(e2eConfig(t, env))
}, caps)
}

// TestMicroVMQBudget records the boot-latency and per-process PSS numbers that
// feed the Q-budget (record 832-834, §(g), launch.go:450-455). It is
// INFORMATIONAL spike output, NOT a boot gate: it times Start (the full
// Launch→Health-OK→Provision window) and reads the session VM's PSS
// (proportional set size in kB, launch.go:456), emitting both via t.Logf. PSS is
// best-effort — a sandboxed helper (passt sets PR_SET_DUMPABLE=0) leaves no
// readable entry, which is reported, never failed.
func TestMicroVMQBudget(t *testing.T) {
env := microvmtest.Require(t)
m := NewMicroVMRuntime(e2eConfig(t, env))

workspace := t.TempDir()
id, err := m.Create(t.Context(), ContainerSpec{
Name: "qbudget-agent",
UID: 1000,
Mounts: []Mount{{HostPath: workspace, ContainerPath: "/workspace"}},
})
if err != nil {
t.Fatalf("Create: %v", err)
}
t.Cleanup(func() {
if err := m.Remove(t.Context(), id); err != nil {
t.Errorf("Remove (cleanup): %v", err)
}
})

start := time.Now()
if err := m.Start(t.Context(), id); err != nil {
t.Fatalf("Start: %v", err)
}
bootLatency := time.Since(start)
t.Logf("Q-budget: boot latency (Start Launch→Health-OK→Provision) = %s", bootLatency)

session, err := m.session(id)
if err != nil {
t.Fatalf("session after Start: %v", err)
}
pss, pssErr := session.vm.PSS()
if pssErr != nil {
t.Logf("Q-budget: reading PSS (best-effort): %v", pssErr)
}
for _, name := range []string{"cloud-hypervisor", "virtiofsd", "passt"} {
if kb, present := pss[name]; present {
t.Logf("Q-budget: PSS %s = %d kB", name, kb)
} else {
t.Logf("Q-budget: PSS %s unavailable (sandboxed or exited)", name)
}
}
}
69 changes: 69 additions & 0 deletions go/internal/runtime/contract_podman_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//go:build podman

package runtime

// The podman leg of the shared ContainerRuntime contract suite (record §U5):
// runs runContractSuite against a real rootless-podman PodmanCLI, gated on
// podmanUsable() (skip-not-fail where podman is absent, the existing suite's
// pattern, lifecycle_test.go:54-60). It supplies the podman caps: the
// divergences that are microVM-specific (output cap, numeric-uid-only, empty
// MountLabel, ignored Command/CapAdd, graceful power-off) are OFF here, so those
// rows self-skip; the deliberate-kill row asserts the byte-identical
// *exec.ExitError path so this leg proves the podman byte-path stays
// unregressed (divergence 6, OQ-G/U3b).

import (
"errors"
"testing"
)

// TestContractSuite_Podman drives the shared contract rows against rootless
// podman through the ContainerRuntime interface. It builds the agent image once
// (buildImage) and hands runContractSuite a factory minting a fresh PodmanCLI,
// with containers created from that image running `sleep infinity` as uid 1000
// — the production keep-alive-plus-exec shape (lifecycle_test.go).
func TestContractSuite_Podman(t *testing.T) {
if !podmanUsable() {
t.Skip("rootless podman not usable in this environment")
}
buildImage(t, t.TempDir())

caps := backendCaps{
name: "podman",
makeSpec: func(t *testing.T, name string) ContainerSpec {
t.Helper()
return ContainerSpec{
Image: imageTag,
Name: name,
Command: []string{"sleep", "infinity"},
UID: 1000,
}
},
// Divergences 1-4 are microVM-specific: podman's capture is unbounded,
// it resolves image user names, MountLabel is the engine's real label,
// and Command is the keep-alive entrypoint. All OFF → those rows skip.
refusesRootExec: false,
numericUIDOnly: false,
emptyMountLabel: false,
ignoresCommandAndCapAdd: false,
capsOutput: false,
// A `sleep infinity` PID 1 ignores SIGTERM, so Stop burns the full grace
// on podman — the graceful-power-off row would prove nothing here.
gracefulStopPowersOff: false,
// Divergence 6: the podman deliberate-kill error is the byte-identical
// *exec.ExitError (checked in deliberateKill), never the portable type.
portableKillError: false,
assertDuplicateName: func(t *testing.T, err error) {
t.Helper()
var cmdErr *CommandError
if !errors.As(err, &cmdErr) {
t.Fatalf("duplicate-name Create error = %v (%T), want *CommandError (the engine's name-collision refusal)", err, err)
}
},
}

runContractSuite(t, func(t *testing.T) ContainerRuntime {
t.Helper()
return NewPodmanCLI()
}, caps)
}
Loading
Loading