Files
unitdore/runtime/runtime_test.go
T
warkanum f0827fcdd7 feat: podman-compose stack units and root/user execution scopes
- syncup: podman first, docker only as fallback (podman wins on name clash;
  existing docker units found in podman switch to podman)
- syncup: group podman-compose containers into one `kind: stack` unit per
  project (project, working_dir, compose_files, compose_bin from labels);
  disable existing member units ("part of stack <project>")
- generator: compose mode (podman-compose up/stop, WorkingDirectory) with
  pod start/stop fallback when compose binary/dir/files are missing
- reconcile: stacks disabled with "stack not found" when neither compose
  files nor pod exist
- root: scan root + rootless users listed in `users:` (podman only)
- non-root: config ~/.config/unitdore/units.yaml, own user units only
- sysuser package: runuser with XDG_RUNTIME_DIR/DBUS env, home/linger lookup
- install: warn when linger is not enabled for user units
- testdata/stack: podman-compose test stack and captured podman ps fixture
- docs: README, generated-units, man page, AGENTS, PLAN
2026-10-12 19:05:51 +02:00

150 lines
4.4 KiB
Go

package runtime
import (
"os"
"reflect"
"testing"
)
func TestGet_Podman(t *testing.T) {
rt := Get("podman")
if rt == nil {
t.Fatal("Get(podman) returned nil")
}
if rt.Name() != "podman" {
t.Errorf("Name() = %q, want %q", rt.Name(), "podman")
}
}
func TestGet_Docker(t *testing.T) {
rt := Get("docker")
if rt == nil {
t.Fatal("Get(docker) returned nil")
}
if rt.Name() != "docker" {
t.Errorf("Name() = %q, want %q", rt.Name(), "docker")
}
}
func TestGet_Unknown(t *testing.T) {
rt := Get("containerd")
if rt != nil {
t.Errorf("Get(containerd) should return nil for unknown runtime, got %+v", rt)
}
}
func TestAvailable_ReturnsRuntimes(t *testing.T) {
runtimes := Available()
if len(runtimes) == 0 {
t.Error("Available() returned empty list")
}
names := map[string]bool{}
for _, rt := range runtimes {
names[rt.Name()] = true
}
if !names["podman"] {
t.Error("Available() should include podman")
}
if !names["docker"] {
t.Error("Available() should include docker")
}
}
// ListRunning and Exists are integration-level — they call external binaries.
// We test that they don't panic when the binary is absent (returns nil/false, no error).
func TestPodman_ListRunning_NoBinary(t *testing.T) {
// This test is meaningful if podman isn't installed; if it is, we just check no error occurs
p := &Podman{}
containers, err := p.ListRunning("")
if err != nil {
t.Errorf("ListRunning() should not error when podman is absent: %v", err)
}
// containers may be nil or populated — both are fine
_ = containers
}
func TestDocker_ListRunning_NoBinary(t *testing.T) {
d := &Docker{}
containers, err := d.ListRunning("")
if err != nil {
t.Errorf("ListRunning() should not error when docker is absent: %v", err)
}
_ = containers
}
func TestAvailable_PodmanFirst(t *testing.T) {
runtimes := Available()
if len(runtimes) < 2 || runtimes[0].Name() != "podman" || runtimes[1].Name() != "docker" {
t.Errorf("Available() order must be podman, docker; got %v", runtimes)
}
}
// Fixture captured from `podman-compose up -d` of testdata/stack/compose.yaml.
func TestParsePodmanPS_ComposeStack(t *testing.T) {
data, err := os.ReadFile("../testdata/stack/podman_ps.json")
if err != nil {
t.Fatalf("reading fixture: %v", err)
}
containers, err := parsePodmanPS(data)
if err != nil {
t.Fatalf("parsePodmanPS() error: %v", err)
}
if len(containers) != 2 {
t.Fatalf("expected 2 containers, got %d", len(containers))
}
for _, c := range containers {
if c.Project() != "unitdore-test" {
t.Errorf("%s: Project() = %q, want unitdore-test", c.Name, c.Project())
}
if c.WorkingDir() == "" {
t.Errorf("%s: WorkingDir() empty", c.Name)
}
if !reflect.DeepEqual(c.ComposeFiles(), []string{"compose.yaml"}) {
t.Errorf("%s: ComposeFiles() = %v", c.Name, c.ComposeFiles())
}
}
}
func TestContainer_NoProject(t *testing.T) {
c := Container{Name: "plain"}
if c.Project() != "" || c.WorkingDir() != "" || c.ComposeFiles() != nil {
t.Errorf("container without labels should have no stack info: %+v", c)
}
}
func TestContainer_ComposeFilesMultiple(t *testing.T) {
c := Container{Labels: map[string]string{LabelComposeConfigFiles: "compose.yaml, ./override.yaml"}}
want := []string{"compose.yaml", "override.yaml"}
if got := c.ComposeFiles(); !reflect.DeepEqual(got, want) {
t.Errorf("ComposeFiles() = %v, want %v", got, want)
}
}
func TestParseDockerPS_Labels(t *testing.T) {
out := []byte(`{"ID":"abc","Names":"web","Image":"nginx","Command":"nginx","State":"running","Labels":"com.docker.compose.project=shop,com.docker.compose.project.config_files=/s/a.yml,/s/b.yml,com.docker.compose.service=web"}`)
cs := parseDockerPS(out)
if len(cs) != 1 {
t.Fatalf("expected 1 container, got %d", len(cs))
}
l := cs[0].Labels
if l["com.docker.compose.project"] != "shop" {
t.Errorf("project label = %q", l["com.docker.compose.project"])
}
if l["com.docker.compose.project.config_files"] != "/s/a.yml,/s/b.yml" {
t.Errorf("config_files label = %q", l["com.docker.compose.project.config_files"])
}
if l["com.docker.compose.service"] != "web" {
t.Errorf("service label = %q", l["com.docker.compose.service"])
}
// Docker compose stacks are not grouped yet (podman-compose only)
if cs[0].Project() != "" {
t.Errorf("docker compose container should not report a podman-compose project")
}
}
func TestPodName(t *testing.T) {
if PodName("shop") != "pod_shop" {
t.Errorf("PodName() = %q", PodName("shop"))
}
}