Files
werkdock/internal/cli/images_test.go
6d6060bb07 RFC 0005 milestone 2: instance lifecycle — run -d, ps, stop, logs, rm, exec (#4)
An instance is a directory under instances/<id>/ (config.json, state.json,
stdout, stderr, run/, volumes/) written by package instance, the service
behind the verbs. run -d and start fork this binary as a supervisor
(werkdock supervise ID) that records the pids and the exit code; stop,
kill, wait, logs, ps, rm, inspect read the files. exec is a second bwrap
over the same instance tree. The command is pid 1 of its sandbox
(bwrap --as-pid-1) so stop's SIGTERM reaches it, as in docker; /run and
the anonymous volumes are instance directories, kept across stop/start
and shared with exec. Version 0.5.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #4
2026-09-10 20:55:59 +02:00

104 lines
3.7 KiB
Go

package cli
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"time"
"werkdock/internal/store"
)
func imagesStore(t *testing.T) store.Store {
t.Helper()
home := t.TempDir()
t.Setenv("WERKDOCK_HOME", home)
st := store.Store{Root: home}
for rel, meta := range map[string]string{
"alpine/3.20": `{"name":"alpine:3.20","createdAt":"2026-04-16T23:53:26Z","architecture":"amd64","os":"linux"}`,
"alpine/latest": `{"name":"alpine","createdAt":"2026-04-16T23:53:26Z","architecture":"amd64","os":"linux"}`,
"tool/latest": `{"name":"tool","createdAt":"2026-09-01T10:00:00Z"}`,
} {
dir := filepath.Join(home, "images", rel)
if err := os.MkdirAll(filepath.Join(dir, "rootfs", "bin"), 0o755); err != nil {
t.Fatal(err)
}
os.WriteFile(filepath.Join(dir, "rootfs", "bin", "sh"), bytes.Repeat([]byte("x"), 1500), 0o755)
os.Link(filepath.Join(dir, "rootfs", "bin", "sh"), filepath.Join(dir, "rootfs", "bin", "ash"))
os.WriteFile(filepath.Join(dir, "image.json"), []byte(meta+"\n"), 0o644)
}
return st
}
func TestImagesRowsAndSize(t *testing.T) {
st := imagesStore(t)
row := imageRow{st: st, ref: store.Reference{Repository: "alpine", Tag: "3.20"}}
rows := []interface{}{&row}
if got := row.Size(); got != "1.5kB" {
t.Errorf("size counts a hardlink once: got %q", got)
}
latest := imageRow{st: st, ref: store.Reference{Repository: "alpine", Tag: "latest"}}
if got, want := row.ID(), latest.ID(); got != want {
t.Errorf("two tags of one image differ in ID: %q vs %q", got, want)
}
var out bytes.Buffer
if err := printFormatted(&out, "table", imageColumns, rows); err != nil {
t.Fatal(err)
}
lines := strings.Split(strings.TrimSpace(out.String()), "\n")
if len(lines) != 2 || !strings.HasPrefix(lines[0], "REPOSITORY TAG IMAGE ID") || !strings.HasPrefix(lines[1], "alpine 3.20 ") || !strings.HasSuffix(lines[1], "1.5kB") {
t.Errorf("table:\n%s", out.String())
}
out.Reset()
if err := printFormatted(&out, `table {{.Repository}}\t{{.CreatedAt}}`, imageColumns, rows); err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(out.String(), "REPOSITORY CREATED AT\nalpine 2026-04-1") {
t.Errorf("custom table:\n%s", out.String())
}
out.Reset()
if err := printFormatted(&out, "{{.Repository}}:{{.Tag}} {{.ID}}", imageColumns, rows); err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(out.String(), "alpine:3.20 ") || len(strings.TrimSpace(out.String())) != len("alpine:3.20 ")+12 {
t.Errorf("template: %q", out.String())
}
if err := printFormatted(&out, "{{.Nope}}", imageColumns, rows); err == nil {
t.Error("unknown field accepted")
}
}
func TestImagesCommandFilterAndQuiet(t *testing.T) {
imagesStore(t)
for _, tc := range []struct {
args []string
want int
}{
{nil, 0}, {[]string{"alpine"}, 0}, {[]string{"alpine:3.20"}, 0}, {[]string{"nosuch"}, 0},
{[]string{"-q"}, 0}, {[]string{"a", "b"}, exitCLIError}, {[]string{"--format", "{{.Nope}}"}, exitCLIError},
} {
if code := imagesCmd(tc.args); code != tc.want {
t.Errorf("images %q: exited %d, want %d", tc.args, code, tc.want)
}
}
}
func TestHumanDurationAndSize(t *testing.T) {
for d, want := range map[time.Duration]string{
30 * time.Second: "30 seconds", 90 * time.Second: "About a minute", 5 * time.Minute: "5 minutes",
90 * time.Minute: "About an hour", 30 * time.Hour: "30 hours", 3 * 24 * time.Hour: "3 days",
20 * 24 * time.Hour: "2 weeks", 100 * 24 * time.Hour: "3 months", 800 * 24 * time.Hour: "2 years",
} {
if got := humanDuration(d); got != want {
t.Errorf("%v: got %q, want %q", d, got, want)
}
}
for n, want := range map[int64]string{500: "500B", 7800000: "7.8MB", 4110000: "4.11MB", 1500: "1.5kB", 2500000000: "2.5GB"} {
if got := humanSize(n); got != want {
t.Errorf("%d: got %q, want %q", n, got, want)
}
}
}