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) } } }