- 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
255 lines
6.9 KiB
Go
255 lines
6.9 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestLoadOrEmpty_NoFile(t *testing.T) {
|
|
cfg, err := LoadOrEmpty("/tmp/unitdore-nonexistent-test.yaml")
|
|
if err != nil {
|
|
t.Fatalf("expected no error for missing file, got: %v", err)
|
|
}
|
|
if len(cfg.Units) != 0 {
|
|
t.Errorf("expected empty units, got %d", len(cfg.Units))
|
|
}
|
|
}
|
|
|
|
func TestSaveAndLoad(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "units.yaml")
|
|
|
|
cfg := &Config{
|
|
Units: []Unit{
|
|
{Name: "nginx", Runtime: "podman", Order: 1, Enabled: true},
|
|
{Name: "myapp", Runtime: "docker", User: "hein", Order: 2, Enabled: true},
|
|
},
|
|
}
|
|
|
|
if err := cfg.Save(path); err != nil {
|
|
t.Fatalf("Save failed: %v", err)
|
|
}
|
|
|
|
loaded, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load failed: %v", err)
|
|
}
|
|
|
|
if len(loaded.Units) != 2 {
|
|
t.Errorf("expected 2 units, got %d", len(loaded.Units))
|
|
}
|
|
if loaded.Units[0].Name != "nginx" {
|
|
t.Errorf("expected nginx, got %s", loaded.Units[0].Name)
|
|
}
|
|
if loaded.Units[1].User != "hein" {
|
|
t.Errorf("expected user hein, got %s", loaded.Units[1].User)
|
|
}
|
|
}
|
|
|
|
func TestFindUnit(t *testing.T) {
|
|
cfg := &Config{
|
|
Units: []Unit{
|
|
{Name: "nginx", Runtime: "podman"},
|
|
{Name: "myapp", Runtime: "docker"},
|
|
},
|
|
}
|
|
|
|
u := cfg.FindUnit("nginx")
|
|
if u == nil {
|
|
t.Fatal("expected to find nginx, got nil")
|
|
}
|
|
if u.Runtime != "podman" {
|
|
t.Errorf("expected podman, got %s", u.Runtime)
|
|
}
|
|
|
|
missing := cfg.FindUnit("nothere")
|
|
if missing != nil {
|
|
t.Errorf("expected nil for missing unit, got %+v", missing)
|
|
}
|
|
}
|
|
|
|
func TestAddUnit(t *testing.T) {
|
|
cfg := &Config{}
|
|
|
|
added := cfg.AddUnit(Unit{Name: "nginx", Runtime: "podman", Enabled: true})
|
|
if !added {
|
|
t.Error("expected AddUnit to return true for new unit")
|
|
}
|
|
if len(cfg.Units) != 1 {
|
|
t.Errorf("expected 1 unit, got %d", len(cfg.Units))
|
|
}
|
|
|
|
// Adding a duplicate should return false and not grow the list
|
|
added = cfg.AddUnit(Unit{Name: "nginx", Runtime: "docker"})
|
|
if added {
|
|
t.Error("expected AddUnit to return false for duplicate")
|
|
}
|
|
if len(cfg.Units) != 1 {
|
|
t.Errorf("expected still 1 unit, got %d", len(cfg.Units))
|
|
}
|
|
}
|
|
|
|
func TestLoad_InvalidYAML(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "bad.yaml")
|
|
// Use a tab character in a mapping key — this is genuinely invalid YAML
|
|
os.WriteFile(path, []byte("units:\n - name: ok\n\t broken: tab-indent"), 0644)
|
|
|
|
_, err := Load(path)
|
|
if err == nil {
|
|
t.Error("expected error for invalid YAML, got nil")
|
|
}
|
|
}
|
|
|
|
func TestSave_CreatesParentDir(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "subdir", "nested", "units.yaml")
|
|
|
|
cfg := &Config{Units: []Unit{{Name: "test", Runtime: "podman", Enabled: true}}}
|
|
if err := cfg.Save(path); err != nil {
|
|
t.Fatalf("Save failed to create parent dirs: %v", err)
|
|
}
|
|
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Errorf("expected file to exist: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDisabledReason_Roundtrip(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "units.yaml")
|
|
|
|
cfg := &Config{
|
|
Units: []Unit{
|
|
{
|
|
Name: "ghost",
|
|
Runtime: "podman",
|
|
Enabled: false,
|
|
DisabledReason: "container not found",
|
|
},
|
|
},
|
|
}
|
|
|
|
if err := cfg.Save(path); err != nil {
|
|
t.Fatalf("Save failed: %v", err)
|
|
}
|
|
|
|
loaded, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load failed: %v", err)
|
|
}
|
|
|
|
if loaded.Units[0].DisabledReason != "container not found" {
|
|
t.Errorf("expected disabled_reason to survive roundtrip, got: %q", loaded.Units[0].DisabledReason)
|
|
}
|
|
}
|
|
|
|
func TestSaveAndLoad_Stack(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "units.yaml")
|
|
cfg := &Config{
|
|
Users: []string{"hein"},
|
|
Units: []Unit{{
|
|
Name: "shop", Runtime: "podman", Kind: KindStack, Project: "shop",
|
|
WorkingDir: "/srv/shop", ComposeFiles: []string{"compose.yaml"},
|
|
ComposeBin: "/usr/bin/podman-compose", User: "hein", Enabled: true,
|
|
}},
|
|
}
|
|
if err := cfg.Save(path); err != nil {
|
|
t.Fatalf("Save failed: %v", err)
|
|
}
|
|
loaded, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load failed: %v", err)
|
|
}
|
|
u := loaded.Units[0]
|
|
if !u.IsStack() || u.Project != "shop" || u.WorkingDir != "/srv/shop" ||
|
|
len(u.ComposeFiles) != 1 || u.ComposeBin != "/usr/bin/podman-compose" {
|
|
t.Errorf("stack fields not round-tripped: %+v", u)
|
|
}
|
|
if len(loaded.Users) != 1 || loaded.Users[0] != "hein" {
|
|
t.Errorf("users not round-tripped: %v", loaded.Users)
|
|
}
|
|
}
|
|
|
|
func TestIsStack_DefaultKind(t *testing.T) {
|
|
if (Unit{Name: "x"}).IsStack() {
|
|
t.Error("unit without kind must be a container")
|
|
}
|
|
}
|
|
|
|
func TestUserConfigPath(t *testing.T) {
|
|
if got := UserConfigPath("/home/hein"); got != "/home/hein/.config/unitdore/units.yaml" {
|
|
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")
|
|
}
|
|
}
|