872ffcaf25
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---
173 lines
4.8 KiB
Go
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
|
|
}
|