Files
unitdore/openrc/openrc_test.go
T
warkanum 91811b4fa1 feat: order/delay in service files, enable cmd, --all on unit commands
- install/uninstall/start/stop/enable take <name> or --all; *all kept as shortcuts
- add enable (enable at boot + start); startall no longer enables
- order/delay resolved by config.ResolveOrder and written into systemd,
  OpenRC and runit service files (After=/sleep), so they apply at boot
- install --all stops and disables units before removing their files
- update regenerates all installed, enabled units and removes disabled ones
- edit creates the config dir; docs and man page updated
- tests: fake Init for cmd, ResolveOrder, backend ordering output
2026-10-12 21:31:41 +02:00

254 lines
6.9 KiB
Go

package openrc
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/service"
)
// setup points the backend at a temp init dir with recorded commands.
func setup(t *testing.T) *[]string {
t.Helper()
oldDir, oldRun, oldOut, oldRoot := initDir, runCmd, outCmd, service.IsRoot
t.Cleanup(func() { initDir, runCmd, outCmd, service.IsRoot = oldDir, oldRun, oldOut, oldRoot })
initDir = t.TempDir()
service.IsRoot = func() bool { return true }
var calls []string
runCmd = func(name string, args ...string) error {
calls = append(calls, name+" "+strings.Join(args, " "))
return nil
}
return &calls
}
func mustContain(t *testing.T, content string, wants ...string) {
t.Helper()
for _, w := range wants {
if !strings.Contains(content, w) {
t.Errorf("missing %q in:\n%s", w, content)
}
}
}
func TestGenerate_Container(t *testing.T) {
setup(t)
u := config.Unit{Name: "nginx", Runtime: "podman", Enabled: true}
got, err := Backend{}.Generate(u, "", "")
if err != nil {
t.Fatal(err)
}
mustContain(t, got,
"#!/sbin/openrc-run",
`description="Unitdore: nginx (podman)"`,
"command_background=yes",
`pidfile="/run/${RC_SVCNAME}.pid"`,
`command="/usr/bin/podman"`,
`command_args="start -a nginx"`,
"need net",
"stop_pre() {\n\t/usr/bin/podman stop nginx || true\n}",
)
if strings.Contains(got, "supervise-daemon") || strings.Contains(got, "need docker") {
t.Errorf("unexpected supervisor or docker dep:\n%s", got)
}
}
func TestGenerate_DockerRestart(t *testing.T) {
setup(t)
u := config.Unit{Name: "redis", Runtime: "docker", Restart: true, RestartSec: 5, RestartRetries: 3}
got, err := Backend{}.Generate(u, "", "")
if err != nil {
t.Fatal(err)
}
mustContain(t, got, "supervisor=supervise-daemon", "respawn_delay=5", "respawn_max=3", "need docker")
if strings.Contains(got, "command_background") {
t.Errorf("supervised unit must not background:\n%s", got)
}
}
func TestGenerate_ScriptCustomShellAndDir(t *testing.T) {
setup(t)
u := config.Unit{
Name: "job", Runtime: config.RuntimeScript, Shell: "/bin/sh",
Script: "/opt/my jobs/run.sh", WorkingDir: "/opt",
}
got, err := Backend{}.Generate(u, "", "")
if err != nil {
t.Fatal(err)
}
mustContain(t, got,
"(sh script)",
`command="/bin/sh"`,
`command_args="'/opt/my jobs/run.sh'"`,
`directory="/opt"`,
)
}
func TestGenerate_OneshotStack(t *testing.T) {
setup(t)
u := config.Unit{Name: "shop", Runtime: "podman", Kind: config.KindStack, Project: "shop"}
got, err := Backend{}.Generate(u, "", "")
if err != nil {
t.Fatal(err)
}
mustContain(t, got,
"start() {",
"/usr/bin/podman pod start pod_shop",
"stop() {",
"/usr/bin/podman pod stop pod_shop",
)
if strings.Contains(got, "command=") {
t.Errorf("oneshot must not set command:\n%s", got)
}
}
func TestGenerate_RootlessUser(t *testing.T) {
setup(t)
u := config.Unit{Name: "app", Runtime: "podman", User: "root"}
got, err := Backend{}.Generate(u, "", "")
if err != nil {
t.Fatal(err)
}
mustContain(t, got,
"export HOME=/root",
"export XDG_RUNTIME_DIR=/run/user/0",
`command_user="root"`,
"checkpath -d -m 0700 -o root /run/user/0",
"su -m -s /bin/sh -c '/usr/bin/podman stop app' root",
)
}
func TestGenerate_Invalid(t *testing.T) {
setup(t)
if _, err := (Backend{}).Generate(config.Unit{Name: "x", Runtime: config.RuntimeExec}, "", ""); err == nil {
t.Error("expected validation error")
}
if _, err := (Backend{}).Generate(config.Unit{Name: "x", Runtime: "podman", Command: `/bin/x "open`}, "", ""); err == nil {
t.Error("expected quote error")
}
}
func TestInstallUninstall(t *testing.T) {
calls := setup(t)
b := Backend{}
u := config.Unit{Name: "nginx", Runtime: "podman"}
if b.IsInstalled(u, "", "") {
t.Fatal("should not be installed yet")
}
if err := b.Install(u, "p-", ""); err != nil {
t.Fatal(err)
}
path := filepath.Join(initDir, "unitdore-p-nginx")
fi, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if fi.Mode().Perm()&0111 == 0 {
t.Errorf("init script must be executable, mode %v", fi.Mode())
}
if !b.IsInstalled(u, "p-", "") {
t.Error("should be installed")
}
if err := b.Uninstall(u, "p-", ""); err != nil {
t.Fatal(err)
}
if b.IsInstalled(u, "p-", "") {
t.Error("should be uninstalled")
}
if got := strings.Join(*calls, "|"); got != "rc-update del unitdore-p-nginx default" {
t.Errorf("calls = %q", got)
}
}
func TestControlCommands(t *testing.T) {
calls := setup(t)
b := Backend{}
u := config.Unit{Name: "nginx", Runtime: "podman"}
_ = b.Start(u, "", "")
_ = b.Stop(u, "", "")
_ = b.Enable(u, "", "")
_ = b.Disable(u, "", "")
want := []string{
"rc-service unitdore-nginx start",
"rc-service unitdore-nginx stop",
"rc-update add unitdore-nginx default",
"rc-service unitdore-nginx start",
"rc-service unitdore-nginx stop",
"rc-update del unitdore-nginx default",
}
if got := strings.Join(*calls, "|"); got != strings.Join(want, "|") {
t.Errorf("calls =\n%q\nwant\n%q", *calls, want)
}
}
func TestRequiresRoot(t *testing.T) {
setup(t)
service.IsRoot = func() bool { return false }
u := config.Unit{Name: "nginx", Runtime: "podman"}
b := Backend{}
for name, err := range map[string]error{
"install": b.Install(u, "", ""), "uninstall": b.Uninstall(u, "", ""),
"start": b.Start(u, "", ""), "stop": b.Stop(u, "", ""),
"enable": b.Enable(u, "", ""), "disable": b.Disable(u, "", ""),
} {
if err == nil {
t.Errorf("%s should require root", name)
}
}
}
func TestParseStatus(t *testing.T) {
tests := map[string]string{
" * status: started": "active",
" * status: stopped": "inactive",
" * status: crashed": "failed",
" * status: starting": "activating",
" * status: stopping": "deactivating",
"": "unknown",
"rc-service: not found": "unknown",
}
for in, want := range tests {
if got := parseStatus(in); got != want {
t.Errorf("parseStatus(%q) = %q, want %q", in, got, want)
}
}
}
func TestGenerate_OrderAndDelay(t *testing.T) {
setup(t)
u := config.Unit{Name: "web", Runtime: "podman", Enabled: true, After: []string{"db", "cache"}, StartDelay: 5 * time.Second}
got, err := Backend{}.Generate(u, "p-", "")
if err != nil {
t.Fatal(err)
}
mustContain(t, got, "need net\n\tafter unitdore-p-db unitdore-p-cache\n}", "start_pre() {\n\tsleep 5\n}")
}
func TestGenerate_OneshotDelay(t *testing.T) {
setup(t)
u := config.Unit{Name: "job", Runtime: "exec", Executable: "/bin/true", ServiceType: "oneshot", Enabled: true, StartDelay: time.Second}
got, err := Backend{}.Generate(u, "", "")
if err != nil {
t.Fatal(err)
}
mustContain(t, got, "start_pre() {\n\tsleep 1\n}")
}
func TestGenerate_NoOrderNoExtras(t *testing.T) {
setup(t)
got, err := Backend{}.Generate(config.Unit{Name: "a", Runtime: "podman", Enabled: true}, "", "")
if err != nil {
t.Fatal(err)
}
if strings.Contains(got, "after ") || strings.Contains(got, "start_pre") {
t.Errorf("unexpected ordering output:\n%s", got)
}
}