Files
orca/internal/emitter/systemd_test.go
T
Jon Chery 60b0357eb6 feat(P0c): Job/Service/DaemonSet schemas + emitter interface + systemd stub (REQ-074)
P0c — Kind-specific schema validators + Layer 4 emitter interface.

Schemas (internal/spec/schema/schema.go, REQ-074):
- Validator interface with JobValidator, ServiceValidator, DaemonSetValidator.
  JobValidator: count=1, no service block, optional schedule/timeout.
  ServiceValidator: ports required, count>=1, restart+update+runtime required.
  DaemonSetValidator: schedule mode required, no ports (D-175), no count.
  ValidatorFor(kind) dispatcher. 96.2% coverage.

Emitter interface (internal/emitter/emitter.go, REQ-074, I-B-002):
- File{Path,Content,Mode}, Emitter interface { Render(spec,node) []File },
  Registry keyed by kind:runtime, Register + Render lookup. 100% coverage.

Systemd stub (internal/emitter/systemd.go):
- SystemdEmitter for process runtime. Renders minimal [Service] unit at
  /etc/systemd/system/orca-v1-alloc-<name>.service (orca-v1- prefix per
  dual-write window REQ-090 — no overlap with v0.8 daemon's orca-<job>).

Flock test fix: TestFlock_concurrentBlocks rewritten to use non-blocking
tryFlockEx (LOCK_NB) instead of a leaked blocking goroutine. Eliminates
the temp-dir cleanup race.

20 packages pass, 20 bats pass, gofmt clean, verify-reqs 90 consistent.

---ci---
project: orca
phase: P0c
milestone: v0.9
status: execute
---/ci---
2026-08-05 17:17:02 +00:00

157 lines
4.9 KiB
Go

package emitter
import (
"strings"
"testing"
"git.cloudinit.dev/coreci/orca/internal/jobspec"
)
func TestSystemdEmitter_RenderJob(t *testing.T) {
spec := &jobspec.WorkloadSpec{
Kind: "Job",
Name: "backup",
Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/usr/bin/rsync -a /src /dst"},
}
node := &Node{Hostname: "node-1", Runtime: []string{"process"}}
files, err := SystemdEmitter{}.Render(spec, node)
if err != nil {
t.Fatalf("Render: %v", err)
}
if len(files) != 1 {
t.Fatalf("got %d files, want 1", len(files))
}
f := files[0]
wantPath := "/etc/systemd/system/orca-v1-backup.service"
if f.Path != wantPath {
t.Errorf("Path = %q, want %q", f.Path, wantPath)
}
wantContent := "[Service]\nExecStart=/usr/bin/rsync -a /src /dst\n"
if f.Content != wantContent {
t.Errorf("Content = %q, want %q", f.Content, wantContent)
}
if f.Mode != "0644" {
t.Errorf("Mode = %q, want 0644", f.Mode)
}
}
func TestSystemdEmitter_RenderService(t *testing.T) {
// The full service emitter (Traefik route + health checks) lands in
// P02; here we only prove the systemd side renders for a Service
// kind with a process runtime.
spec := &jobspec.WorkloadSpec{
Kind: "Service",
Name: "web",
Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/usr/local/bin/httpd -f"},
}
node := &Node{Hostname: "node-1", Runtime: []string{"process"}}
files, err := SystemdEmitter{}.Render(spec, node)
if err != nil {
t.Fatalf("Render: %v", err)
}
if len(files) != 1 {
t.Fatalf("got %d files, want 1", len(files))
}
if files[0].Path != "/etc/systemd/system/orca-v1-web.service" {
t.Errorf("Path = %q, want /etc/systemd/system/orca-v1-web.service", files[0].Path)
}
if !strings.Contains(files[0].Content, "ExecStart=/usr/local/bin/httpd -f") {
t.Errorf("Content = %q, want it to contain the ExecStart line", files[0].Content)
}
}
func TestSystemdEmitter_EmptyCommandError(t *testing.T) {
spec := &jobspec.WorkloadSpec{
Kind: "Job",
Name: "x",
Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: ""},
}
_, err := SystemdEmitter{}.Render(spec, &Node{})
if err == nil {
t.Fatal("expected error for empty command, got nil")
}
if !strings.Contains(err.Error(), "command is empty") {
t.Errorf("error = %q, want 'command is empty'", err.Error())
}
}
func TestSystemdEmitter_WhitespaceCommandError(t *testing.T) {
spec := &jobspec.WorkloadSpec{
Kind: "Job",
Name: "x",
Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: " "},
}
_, err := SystemdEmitter{}.Render(spec, &Node{})
if err == nil {
t.Fatal("expected error for whitespace-only command, got nil")
}
if !strings.Contains(err.Error(), "command is empty") {
t.Errorf("error = %q, want 'command is empty'", err.Error())
}
}
func TestSystemdEmitter_NilSpec(t *testing.T) {
v := SystemdEmitter{}
if _, err := v.Render(nil, &Node{}); err == nil {
t.Fatal("expected error for nil spec")
}
}
func TestSystemdEmitter_EmptyName(t *testing.T) {
spec := &jobspec.WorkloadSpec{
Kind: "Job",
Name: " ",
Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/x"},
}
_, err := SystemdEmitter{}.Render(spec, &Node{})
if err == nil {
t.Fatal("expected error for empty name")
}
if !strings.Contains(err.Error(), "name is empty") {
t.Errorf("error = %q, want 'name is empty'", err.Error())
}
}
func TestSystemdEmitter_NilRuntime(t *testing.T) {
spec := &jobspec.WorkloadSpec{Kind: "Job", Name: "x"}
_, err := SystemdEmitter{}.Render(spec, &Node{})
if err == nil {
t.Fatal("expected error for nil runtime")
}
if !strings.Contains(err.Error(), "runtime block is nil") {
t.Errorf("error = %q, want 'runtime block is nil'", err.Error())
}
}
func TestSystemdEmitter_UnitNamePrefix(t *testing.T) {
// The orca-v1- prefix is load-bearing for the dual-write window
// (REQ-090, I-C-006): the v0.8 daemon writes `orca-<job>.service`
// and the v0.9 SSH-push path writes `orca-v1-<spec.Name>.service`,
// so the two never collide. This test guards against accidental
// removal of the prefix.
spec := &jobspec.WorkloadSpec{
Kind: "Job",
Name: "dual-write-safety",
Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/true"},
}
files, err := SystemdEmitter{}.Render(spec, &Node{Hostname: "n"})
if err != nil {
t.Fatalf("Render: %v", err)
}
if !strings.HasPrefix(files[0].Path, "/etc/systemd/system/orca-v1-") {
t.Errorf("Path = %q, want it to start with /etc/systemd/system/orca-v1- (REQ-090)", files[0].Path)
}
if !strings.HasSuffix(files[0].Path, ".service") {
t.Errorf("Path = %q, want it to end with .service", files[0].Path)
}
// Explicitly assert the full expected unit name to lock the contract.
want := "/etc/systemd/system/orca-v1-dual-write-safety.service"
if files[0].Path != want {
t.Errorf("Path = %q, want %q", files[0].Path, want)
}
// Sanity: the prefix is exactly "orca-v1-", not "orca-v0" or "orca".
if unitNamePrefix != "orca-v1-" {
t.Errorf("unitNamePrefix = %q, want orca-v1-", unitNamePrefix)
}
}