- 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
150 lines
4.4 KiB
Go
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"))
|
|
}
|
|
}
|