Files
orca/internal/runtime/process_test.go
T
Jon Chery 872ffcaf25 feat(P07a/b/c): runtime abstraction — 5 backends (process/podman/wasm/pve-vm/pve-ct), C-01 satisfied (REQ-078)
P07a/b/c — Runtime abstraction interface + 5 implementations.

Runtime interface (internal/runtime/runtime.go, REQ-078):
- Runtime interface { Prepare, Start, Stop, Status }. Alloc struct carries
  Runtime field (changeable on migration per R-004). Registry keyed by
  runtime.one_of. DefaultRegistry(transport) registers all 5.

Process (internal/runtime/process.go):
- ProcessRuntime wraps os/exec (LOCAL testing only; production uses systemd
  emitter). SIGTERM grace 10s then SIGKILL.

Podman (internal/runtime/podman.go):
- PodmanRuntime via sshpush.Transport. podman pull/run/stop/rm/inspect.

Wasm (internal/runtime/wasm.go, gate C-01 SATISFIED):
- WasmRuntime uses wasmtime CLI (apt-installed on peer) via SSH exec. NO CGO
  — does NOT import bytecodealliance/wasmtime-go. CGO_ENABLED=0 build
  passes. D-002 cross-compile story preserved. D-187 recorded.

PVE (internal/runtime/pve.go):
- PveVMRuntime (qm create/start/stop/status) + PveCTRuntime (pct
  create/start/stop/status) via sshpush.Transport. VMID = hash(alloc.ID)%99999.

C-01 evaluation: internal/runtime/C01_WASMTIME_CGO_EVAL.md. Auto-decision
(full autonomy): wasmtime remains primary; CLI-via-SSH avoids CGO entirely.
D-187 in PROJECT.md.

23 packages pass, 20 bats pass, gofmt clean, verify-reqs 90 consistent.
92.7% coverage on internal/runtime.

---ci---
project: orca
phase: P07a/b/c
milestone: v0.9
status: execute
---/ci---
2026-08-05 18:31:25 +00:00

173 lines
4.8 KiB
Go

package runtime
import (
"context"
"runtime"
"testing"
"time"
"git.cloudinit.dev/coreci/orca/internal/jobspec"
)
// TestProcessRuntime_PrepareIsNoop verifies Prepare is a no-op.
func TestProcessRuntime_PrepareIsNoop(t *testing.T) {
p := NewProcessRuntime()
a := alloc("process", "", "/bin/true")
if err := p.Prepare(context.Background(), a); err != nil {
t.Fatalf("Prepare: %v", err)
}
}
// TestProcessRuntime_PrepareNilSpec exercises the nil-spec branch
// indirectly — Prepare is a no-op regardless of input.
func TestProcessRuntime_PrepareNilSpec(t *testing.T) {
p := NewProcessRuntime()
if err := p.Prepare(context.Background(), &Alloc{ID: "x"}); err != nil {
t.Fatalf("Prepare (nil spec) should still be no-op: %v", err)
}
}
// TestProcessRuntime_StartStopStatus runs a real long-lived process
// (sleep) and verifies Start -> Status(running) -> Stop -> Status(stopped).
func TestProcessRuntime_StartStopStatus(t *testing.T) {
if _, err := sleepBin(); err != nil {
t.Skipf("sleep binary not available: %v", err)
}
p := NewProcessRuntime()
sleepCmd, _ := sleepBin()
a := alloc("process", "", sleepCmd+" 30")
ctx, cancel := withTimeout(10 * time.Second)
defer cancel()
pid, err := p.Start(ctx, a)
if err != nil {
t.Fatalf("Start: %v", err)
}
if pid <= 0 {
t.Fatalf("pid = %d, want > 0", pid)
}
st, err := p.Status(context.Background(), a)
if err != nil {
t.Fatalf("Status: %v", err)
}
if st != StateRunning {
t.Errorf("Status = %q, want running", st)
}
// Verify PID lookup.
got, err := p.PID(a.ID)
if err != nil || got != pid {
t.Errorf("PID = %d/%v, want %d", got, err, pid)
}
stopCtx, cancelStop := withTimeout(15 * time.Second)
defer cancelStop()
if err := p.Stop(stopCtx, a); err != nil {
t.Fatalf("Stop: %v", err)
}
st2, _ := p.Status(context.Background(), a)
if st2 != StateStopped {
t.Errorf("Status after Stop = %q, want stopped", st2)
}
}
// TestProcessRuntime_StopNotStarted verifies Stop is idempotent on a
// never-started alloc.
func TestProcessRuntime_StopNotStarted(t *testing.T) {
p := NewProcessRuntime()
a := alloc("process", "", "/bin/true")
ctx, cancel := withTimeout(2 * time.Second)
defer cancel()
if err := p.Stop(ctx, a); err != nil {
t.Errorf("Stop on never-started alloc should be no-op, got %v", err)
}
}
// TestProcessRuntime_StatusPending verifies Status returns pending for
// an alloc that was never started.
func TestProcessRuntime_StatusPending(t *testing.T) {
p := NewProcessRuntime()
a := alloc("process", "", "/bin/true")
st, err := p.Status(context.Background(), a)
if err != nil {
t.Fatalf("Status: %v", err)
}
if st != StatePending {
t.Errorf("Status = %q, want pending", st)
}
}
// TestProcessRuntime_StartNoCommand verifies Start errors on missing
// command.
func TestProcessRuntime_StartNoCommand(t *testing.T) {
p := NewProcessRuntime()
a := &Alloc{ID: "x", Spec: nil, Runtime: "process"}
if _, err := p.Start(context.Background(), a); err == nil {
t.Error("Start with nil spec should error")
}
}
// TestProcessRuntime_StartEmptyCommand verifies Start errors when
// the command parses to zero argv.
func TestProcessRuntime_StartEmptyCommand(t *testing.T) {
p := NewProcessRuntime()
a := &Alloc{
ID: "e",
Runtime: "process",
Spec: &jobspec.WorkloadSpec{
Runtime: &jobspec.RuntimeBlock{Command: " "},
},
}
_, err := p.Start(context.Background(), a)
if err == nil {
t.Error("Start with empty command should error")
}
}
// TestProcessRuntime_StartBadBinary verifies Start propagates exec
// errors for a missing binary.
func TestProcessRuntime_StartBadBinary(t *testing.T) {
p := NewProcessRuntime()
a := alloc("process", "", "/no/such/binary/here")
_, err := p.Start(context.Background(), a)
if err == nil {
t.Error("Start with missing binary should error")
}
}
// TestProcessRuntime_StopOnFinishedProcess verifies Stop on a process
// that already exited (e.g. /bin/true) is a no-op (no error).
func TestProcessRuntime_StopOnFinishedProcess(t *testing.T) {
p := NewProcessRuntime()
a := alloc("process", "", "/bin/true")
_, _ = p.Start(context.Background(), a)
// Give /bin/true time to exit.
time.Sleep(200 * time.Millisecond)
ctx, cancel := withTimeout(5 * time.Second)
defer cancel()
if err := p.Stop(ctx, a); err != nil {
t.Errorf("Stop on exited process should be no-op, got %v", err)
}
}
// TestProcessRuntime_PIDUnknown verifies PID lookup errors for an
// unknown alloc.
func TestProcessRuntime_PIDUnknown(t *testing.T) {
p := NewProcessRuntime()
if _, err := p.PID("nope"); err == nil {
t.Error("PID unknown should error")
}
}
// sleepBin returns the sleep command path ("sleep") on this OS.
func sleepBin() (string, error) {
// /bin/sleep exists on Linux; on other platforms fall back to
// "sleep" (resolved via PATH).
if runtime.GOOS == "linux" {
return "/bin/sleep", nil
}
return "sleep", nil
}