60b0357eb6
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---
164 lines
4.4 KiB
Go
164 lines
4.4 KiB
Go
package emitter
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/jobspec"
|
|
)
|
|
|
|
// mockEmitter is a test-only Emitter that returns a fixed File slice
|
|
// (or an error) so the Registry tests do not depend on the
|
|
// SystemdEmitter. Implements Emitter via value receiver.
|
|
type mockEmitter struct {
|
|
files []File
|
|
err error
|
|
}
|
|
|
|
func (m mockEmitter) Render(spec *jobspec.WorkloadSpec, node *Node) ([]File, error) {
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
out := make([]File, len(m.files))
|
|
copy(out, m.files)
|
|
return out, nil
|
|
}
|
|
|
|
func TestRegistry_RegisterAndRender(t *testing.T) {
|
|
r := NewRegistry()
|
|
want := []File{{Path: "/tmp/a", Content: "alpha", Mode: "0644"}}
|
|
r.Register("job:process", mockEmitter{files: want})
|
|
spec := &jobspec.WorkloadSpec{
|
|
Kind: "Job",
|
|
Name: "demo",
|
|
Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/true"},
|
|
}
|
|
node := &Node{Hostname: "node-1", Runtime: []string{"process"}}
|
|
got, err := r.Render(spec, node)
|
|
if err != nil {
|
|
t.Fatalf("Render: %v", err)
|
|
}
|
|
if len(got) != 1 {
|
|
t.Fatalf("got %d files, want 1", len(got))
|
|
}
|
|
if got[0] != want[0] {
|
|
t.Errorf("file = %+v, want %+v", got[0], want[0])
|
|
}
|
|
}
|
|
|
|
func TestRegistry_UnknownKindRuntime(t *testing.T) {
|
|
r := NewRegistry()
|
|
spec := &jobspec.WorkloadSpec{
|
|
Kind: "Service",
|
|
Name: "web",
|
|
Runtime: &jobspec.RuntimeBlock{OneOf: "wasm"},
|
|
}
|
|
_, err := r.Render(spec, &Node{})
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown kind:runtime, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "no emitter registered") {
|
|
t.Errorf("error = %q, want 'no emitter registered'", err.Error())
|
|
}
|
|
if !strings.Contains(err.Error(), "service:wasm") {
|
|
t.Errorf("error = %q, want it to mention 'service:wasm'", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestRegistry_MultipleEmittersCorrectSelected(t *testing.T) {
|
|
r := NewRegistry()
|
|
jobFiles := []File{{Path: "/tmp/job", Content: "job", Mode: "0644"}}
|
|
svcFiles := []File{{Path: "/tmp/svc", Content: "svc", Mode: "0644"}}
|
|
dsFiles := []File{{Path: "/tmp/ds", Content: "ds", Mode: "0644"}}
|
|
r.Register("job:process", mockEmitter{files: jobFiles})
|
|
r.Register("service:process", mockEmitter{files: svcFiles})
|
|
r.Register("daemonset:process", mockEmitter{files: dsFiles})
|
|
|
|
cases := []struct {
|
|
kind string
|
|
runtime string
|
|
wantPath string
|
|
}{
|
|
{"Job", "process", "/tmp/job"},
|
|
{"Service", "process", "/tmp/svc"},
|
|
{"DaemonSet", "process", "/tmp/ds"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.kind+":"+tc.runtime, func(t *testing.T) {
|
|
spec := &jobspec.WorkloadSpec{
|
|
Kind: tc.kind,
|
|
Name: "x",
|
|
Runtime: &jobspec.RuntimeBlock{OneOf: tc.runtime, Command: "/bin/x"},
|
|
}
|
|
got, err := r.Render(spec, &Node{Hostname: "n"})
|
|
if err != nil {
|
|
t.Fatalf("Render: %v", err)
|
|
}
|
|
if len(got) != 1 {
|
|
t.Fatalf("got %d files, want 1", len(got))
|
|
}
|
|
if got[0].Path != tc.wantPath {
|
|
t.Errorf("path = %q, want %q", got[0].Path, tc.wantPath)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegistry_NilSpec(t *testing.T) {
|
|
r := NewRegistry()
|
|
_, err := r.Render(nil, &Node{})
|
|
if err == nil {
|
|
t.Fatal("expected error for nil spec")
|
|
}
|
|
if !strings.Contains(err.Error(), "spec is nil") {
|
|
t.Errorf("error = %q, want 'spec is nil'", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestRegistry_EmptyKind(t *testing.T) {
|
|
r := NewRegistry()
|
|
spec := &jobspec.WorkloadSpec{Runtime: &jobspec.RuntimeBlock{OneOf: "process"}}
|
|
_, err := r.Render(spec, &Node{})
|
|
if err == nil {
|
|
t.Fatal("expected error for empty kind")
|
|
}
|
|
if !strings.Contains(err.Error(), "kind is empty") {
|
|
t.Errorf("error = %q, want 'kind is empty'", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestRegistry_NilRuntime(t *testing.T) {
|
|
r := NewRegistry()
|
|
spec := &jobspec.WorkloadSpec{Kind: "Job", Name: "x"}
|
|
_, err := r.Render(spec, &Node{})
|
|
if err == nil {
|
|
t.Fatal("expected error for nil runtime")
|
|
}
|
|
if !strings.Contains(err.Error(), "runtime is nil") {
|
|
t.Errorf("error = %q, want 'runtime is nil'", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestRegistry_EmitterErrorPropagates(t *testing.T) {
|
|
r := NewRegistry()
|
|
wantErr := errors.New("boom")
|
|
r.Register("job:process", mockEmitter{err: wantErr})
|
|
spec := &jobspec.WorkloadSpec{
|
|
Kind: "Job",
|
|
Name: "x",
|
|
Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/x"},
|
|
}
|
|
_, err := r.Render(spec, &Node{})
|
|
if !errors.Is(err, wantErr) {
|
|
t.Errorf("err = %v, want %v", err, wantErr)
|
|
}
|
|
}
|
|
|
|
// Compile-time assertion that mockEmitter and SystemdEmitter implement
|
|
// Emitter.
|
|
var (
|
|
_ Emitter = mockEmitter{}
|
|
_ Emitter = SystemdEmitter{}
|
|
)
|