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
This commit is contained in:
2026-10-12 21:31:41 +02:00
parent d1d0889eb3
commit 91811b4fa1
28 changed files with 1394 additions and 503 deletions
+69
View File
@@ -3,7 +3,9 @@ package config
import (
"os"
"path/filepath"
"reflect"
"testing"
"time"
)
func TestLoadOrEmpty_NoFile(t *testing.T) {
@@ -183,3 +185,70 @@ func TestUserConfigPath(t *testing.T) {
t.Errorf("UserConfigPath() = %q", got)
}
}
func TestResolveOrder(t *testing.T) {
cfg := &Config{Units: []Unit{
{Name: "db", Order: 1, Enabled: true, Delay: "9s"},
{Name: "cache", Order: 1, Enabled: true},
{Name: "off", Order: 2, Enabled: false},
{Name: "web", Order: 3, Enabled: true, Delay: "5s"},
{Name: "web-user", Order: 3, Enabled: true, User: "hein"},
{Name: "user-db", Order: 1, Enabled: true, User: "hein"},
}}
if err := cfg.ResolveOrder(); err != nil {
t.Fatal(err)
}
get := func(n string) Unit { return *cfg.FindUnit(n) }
if u := get("db"); len(u.After) != 0 || u.StartDelay != 0 {
t.Errorf("first group must not wait: %+v", u)
}
if u := get("off"); len(u.After) != 0 || u.StartDelay != 0 {
t.Errorf("disabled unit gets no ordering: %+v", u)
}
// skips the disabled order-2 unit, picks the enabled order-1 same-scope units
if u := get("web"); !reflect.DeepEqual(u.After, []string{"cache", "db"}) || u.StartDelay != 5*time.Second {
t.Errorf("web: %+v", u)
}
// user units only order after units of the same user
if u := get("web-user"); !reflect.DeepEqual(u.After, []string{"user-db"}) || u.StartDelay != 0 {
t.Errorf("web-user: %+v", u)
}
// re-resolving after a change resets stale values
cfg.FindUnit("db").Order = 9
if err := cfg.ResolveOrder(); err != nil {
t.Fatal(err)
}
if u := get("web"); !reflect.DeepEqual(u.After, []string{"cache"}) {
t.Errorf("web after reorder: %+v", u)
}
}
func TestResolveOrder_InvalidDelay(t *testing.T) {
for _, d := range []string{"abc", "-2s"} {
cfg := &Config{Units: []Unit{{Name: "a", Delay: d, Enabled: true}}}
if err := cfg.ResolveOrder(); err == nil {
t.Errorf("delay %q should be rejected", d)
}
}
}
func TestLoad_ResolvesOrderAndRejectsBadDelay(t *testing.T) {
dir := t.TempDir()
good := filepath.Join(dir, "good.yaml")
os.WriteFile(good, []byte("units:\n - {name: a, order: 1, enabled: true}\n - {name: b, order: 2, delay: 4s, enabled: true}\n"), 0644)
cfg, err := Load(good)
if err != nil {
t.Fatal(err)
}
if b := cfg.Units[1]; !reflect.DeepEqual(b.After, []string{"a"}) || b.StartDelay != 4*time.Second {
t.Errorf("b = %+v", b)
}
bad := filepath.Join(dir, "bad.yaml")
os.WriteFile(bad, []byte("units:\n - {name: a, delay: nope, enabled: true}\n"), 0644)
if _, err := Load(bad); err == nil {
t.Error("Load must reject invalid delay")
}
}