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
104 lines
3.7 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|