package service import ( "os" "path/filepath" "testing" "github.com/warkanum/unitdore/config" ) func TestBuildExecCommands(t *testing.T) { tests := []struct { name string unit config.Unit wantStart string wantStop string }{ { name: "podman", unit: config.Unit{Name: "app", Runtime: "podman"}, wantStart: "/usr/bin/podman start -a app", wantStop: "/usr/bin/podman stop app", }, { name: "docker", unit: config.Unit{Name: "app", Runtime: "docker"}, wantStart: "/usr/bin/docker start -a app", wantStop: "/usr/bin/docker stop app", }, { name: "custom command", unit: config.Unit{Name: "app", Runtime: "podman", Command: "/bin/custom"}, wantStart: "/bin/custom", wantStop: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { start, stop := buildExecCommands(tt.unit) if start != tt.wantStart { t.Errorf("start = %q, want %q", start, tt.wantStart) } if stop != tt.wantStop { t.Errorf("stop = %q, want %q", stop, tt.wantStop) } }) } } func TestStackMode(t *testing.T) { u := newComposeStack(t) if StackMode(u) != StackModeCompose { t.Errorf("StackMode() = %q, want compose", StackMode(u)) } missingFile := u missingFile.ComposeFiles = []string{"gone.yaml"} if StackMode(missingFile) != StackModePod { t.Error("missing compose file should fall back to pod mode") } missingBin := u missingBin.ComposeBin = "" if StackMode(missingBin) != StackModePod { t.Error("missing compose binary should fall back to pod mode") } missingDir := u missingDir.WorkingDir = "/nonexistent/unitdore-test" if StackMode(missingDir) != StackModePod { t.Error("missing working dir should fall back to pod mode") } } func TestBuildComposeCommands_Quoting(t *testing.T) { u := config.Unit{ Project: "shop", ComposeBin: "/usr/bin/podman-compose", ComposeFiles: []string{"compose.yaml", "my override.yaml"}, } start, stop := buildComposeCommands(u) wantBase := `/usr/bin/podman-compose -p shop -f compose.yaml -f "my override.yaml"` if start != wantBase+" up" { t.Errorf("start = %q", start) } if stop != wantBase+" stop" { t.Errorf("stop = %q", stop) } } // newComposeStack creates a stack unit whose compose binary, working dir and // compose file exist on disk (compose mode). func newComposeStack(t *testing.T) config.Unit { t.Helper() dir := t.TempDir() bin := filepath.Join(dir, "podman-compose") if err := os.WriteFile(bin, []byte("#!/bin/sh\n"), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "compose.yaml"), []byte("services: {}\n"), 0644); err != nil { t.Fatal(err) } return config.Unit{ Name: "shop", Runtime: "podman", Kind: config.KindStack, Project: "shop", WorkingDir: dir, ComposeFiles: []string{"compose.yaml"}, ComposeBin: bin, Enabled: true, } } func TestSplitCommand(t *testing.T) { tests := []struct { in string want []string wantErr bool }{ {in: "/usr/bin/podman start -a app", want: []string{"/usr/bin/podman", "start", "-a", "app"}}, {in: `/bin/x "my file.txt" 'a b'`, want: []string{"/bin/x", "my file.txt", "a b"}}, {in: `/bin/x "say \"hi\""`, want: []string{"/bin/x", `say "hi"`}}, {in: `/bin/x ""`, want: []string{"/bin/x", ""}}, {in: " ", want: nil}, {in: `/bin/x "open`, wantErr: true}, {in: `/bin/x \`, wantErr: true}, } for _, tt := range tests { got, err := SplitCommand(tt.in) if (err != nil) != tt.wantErr { t.Errorf("SplitCommand(%q) err = %v, wantErr %v", tt.in, err, tt.wantErr) continue } if len(got) != len(tt.want) { t.Errorf("SplitCommand(%q) = %q, want %q", tt.in, got, tt.want) continue } for i := range got { if got[i] != tt.want[i] { t.Errorf("SplitCommand(%q)[%d] = %q, want %q", tt.in, i, got[i], tt.want[i]) } } } } func TestQuoteArg_SplitCommand_RoundTrip(t *testing.T) { for _, arg := range []string{"plain", "with space", `a"b`, `back\slash`, "it's"} { got, err := SplitCommand("x " + QuoteArg(arg)) if err != nil || len(got) != 2 || got[1] != arg { t.Errorf("round trip %q = %q, %v", arg, got, err) } } } func TestShellQuote(t *testing.T) { tests := map[string]string{ "plain-1.0/x": "plain-1.0/x", "": "''", "a b": "'a b'", "it's": `'it'\''s'`, "$HOME": "'$HOME'", } for in, want := range tests { if got := ShellQuote(in); got != want { t.Errorf("ShellQuote(%q) = %q, want %q", in, got, want) } } } func TestShellCommand(t *testing.T) { got, err := ShellCommand(`/opt/agent --config "/etc/my agent.toml"`) if err != nil { t.Fatal(err) } if want := `/opt/agent --config '/etc/my agent.toml'`; got != want { t.Errorf("ShellCommand() = %q, want %q", got, want) } } func TestValidate(t *testing.T) { bad := []config.Unit{ {Name: "a", Runtime: config.RuntimeScript}, {Name: "b", Runtime: config.RuntimeExec}, } for _, u := range bad { if Validate(u) == nil { t.Errorf("Validate(%s) should fail", u.Name) } } ok := []config.Unit{ {Name: "a", Runtime: config.RuntimeScript, Command: "/x"}, {Name: "b", Runtime: "podman"}, } for _, u := range ok { if err := Validate(u); err != nil { t.Errorf("Validate(%s) = %v", u.Name, err) } } } func TestBaseName(t *testing.T) { if got := BaseName(config.Unit{Name: "x"}, "p-", "-s"); got != "unitdore-p-x-s" { t.Errorf("BaseName() = %q", got) } } func TestResolve_PodFallbackIsOneshot(t *testing.T) { u := config.Unit{Name: "shop", Runtime: "podman", Kind: config.KindStack, Project: "shop"} spec := Resolve(u) if spec.Type != TypeOneshot || spec.Start != "/usr/bin/podman pod start pod_shop" { t.Errorf("Resolve() = %+v", spec) } }