package emitter import ( "strings" "testing" "git.cloudinit.dev/coreci/orca/internal/jobspec" ) func TestSystemdEmitter_LifecyclePostStart(t *testing.T) { // lifecycle.post_start → ExecStartPost (one line per command). spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/usr/local/bin/httpd -f"}, Lifecycle: &jobspec.LifecycleBlock{ PostStart: []string{"/usr/bin/sleep 1", "/usr/bin/curl localhost/healthz"}, }, } files, err := SystemdEmitter{}.Render(spec, &Node{Hostname: "n1"}) if err != nil { t.Fatalf("Render: %v", err) } c := files[0].Content if !strings.Contains(c, "ExecStartPost=/usr/bin/sleep 1\n") { t.Errorf("missing ExecStartPost for sleep 1\n%s", c) } if !strings.Contains(c, "ExecStartPost=/usr/bin/curl localhost/healthz\n") { t.Errorf("missing ExecStartPost for curl\n%s", c) } // ExecStart must still be present. if !strings.Contains(c, "ExecStart=/usr/local/bin/httpd -f\n") { t.Errorf("missing ExecStart\n%s", c) } } func TestSystemdEmitter_LifecyclePreStop(t *testing.T) { // lifecycle.pre_stop → ExecStop (one line per command). spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/usr/local/bin/httpd -f"}, Lifecycle: &jobspec.LifecycleBlock{ PreStop: []string{"/usr/local/bin/httpd -graceful", "/usr/bin/sleep 5"}, }, } files, err := SystemdEmitter{}.Render(spec, &Node{Hostname: "n1"}) if err != nil { t.Fatalf("Render: %v", err) } c := files[0].Content if !strings.Contains(c, "ExecStop=/usr/local/bin/httpd -graceful\n") { t.Errorf("missing ExecStop for graceful\n%s", c) } if !strings.Contains(c, "ExecStop=/usr/bin/sleep 5\n") { t.Errorf("missing ExecStop for sleep 5\n%s", c) } } func TestSystemdEmitter_LifecycleBoth(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/httpd"}, Lifecycle: &jobspec.LifecycleBlock{ PostStart: []string{"/bin/after-start"}, PreStop: []string{"/bin/before-stop"}, }, } files, err := SystemdEmitter{}.Render(spec, &Node{}) if err != nil { t.Fatalf("Render: %v", err) } c := files[0].Content // ExecStartPost must appear before ExecStop (post_start runs after // start; pre_stop runs before stop — the order in the unit file // reflects the lifecycle order). startIdx := strings.Index(c, "ExecStart=") postIdx := strings.Index(c, "ExecStartPost=") stopIdx := strings.Index(c, "ExecStop=") if startIdx < 0 || postIdx < 0 || stopIdx < 0 { t.Fatalf("missing one of ExecStart/ExecStartPost/ExecStop\n%s", c) } if !(startIdx < postIdx && postIdx < stopIdx) { t.Errorf("expected order ExecStart < ExecStartPost < ExecStop\n%s", c) } } func TestSystemdEmitter_LifecycleNilOmitsDirectives(t *testing.T) { // No lifecycle block → no ExecStartPost / ExecStop lines. spec := &jobspec.WorkloadSpec{ Kind: "Job", Name: "backup", Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/rsync"}, } files, err := SystemdEmitter{}.Render(spec, &Node{}) if err != nil { t.Fatalf("Render: %v", err) } c := files[0].Content if strings.Contains(c, "ExecStartPost=") { t.Errorf("ExecStartPost should be omitted when no lifecycle\n%s", c) } if strings.Contains(c, "ExecStop=") { t.Errorf("ExecStop should be omitted when no lifecycle\n%s", c) } } func TestSystemdEmitter_LifecycleEmptyListsOmitted(t *testing.T) { // Lifecycle block present but empty lists → no ExecStartPost / ExecStop. spec := &jobspec.WorkloadSpec{ Kind: "Job", Name: "x", Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/x"}, Lifecycle: &jobspec.LifecycleBlock{}, } files, err := SystemdEmitter{}.Render(spec, &Node{}) if err != nil { t.Fatalf("Render: %v", err) } c := files[0].Content if strings.Contains(c, "ExecStartPost=") { t.Errorf("ExecStartPost should be omitted for empty PostStart\n%s", c) } if strings.Contains(c, "ExecStop=") { t.Errorf("ExecStop should be omitted for empty PreStop\n%s", c) } } func TestSystemdEmitter_LifecycleOnlyPostStart(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Job", Name: "x", Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/x"}, Lifecycle: &jobspec.LifecycleBlock{ PostStart: []string{"/bin/notify-up"}, }, } files, err := SystemdEmitter{}.Render(spec, &Node{}) if err != nil { t.Fatalf("Render: %v", err) } c := files[0].Content if !strings.Contains(c, "ExecStartPost=/bin/notify-up\n") { t.Errorf("missing ExecStartPost\n%s", c) } if strings.Contains(c, "ExecStop=") { t.Errorf("ExecStop should be omitted when only PostStart set\n%s", c) } } func TestSystemdEmitter_LifecycleOnlyPreStop(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Job", Name: "x", Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/x"}, Lifecycle: &jobspec.LifecycleBlock{ PreStop: []string{"/bin/notify-down"}, }, } files, err := SystemdEmitter{}.Render(spec, &Node{}) if err != nil { t.Fatalf("Render: %v", err) } c := files[0].Content if !strings.Contains(c, "ExecStop=/bin/notify-down\n") { t.Errorf("missing ExecStop\n%s", c) } if strings.Contains(c, "ExecStartPost=") { t.Errorf("ExecStartPost should be omitted when only PreStop set\n%s", c) } }