From a1924cf1e6b47d7ee03355fe5258c5f1c1a7ccc8 Mon Sep 17 00:00:00 2001 From: christophe-duc Date: Wed, 7 Jan 2026 14:09:14 -0400 Subject: [PATCH] Phase5 and Phase6 completed --- .github/workflows/ci.yml | 32 ++ pkg/commands/integration_test.go | 360 +++++++++++++++++ pkg/commands/podman_host_test.go | 216 ++++++++++ pkg/commands/podman_test.go | 533 ++++++++++++++++++++++++ pkg/commands/runtime_mock.go | 309 ++++++++++++++ pkg/commands/runtime_test.go | 593 +++++++++++++++++++++++++++ pkg/commands/runtime_types_test.go | 629 +++++++++++++++++++++++++++++ scripts/integration-test.sh | 52 +++ test/podman-compose.yml | 25 ++ 9 files changed, 2749 insertions(+) create mode 100644 pkg/commands/integration_test.go create mode 100644 pkg/commands/podman_host_test.go create mode 100644 pkg/commands/podman_test.go create mode 100644 pkg/commands/runtime_mock.go create mode 100644 pkg/commands/runtime_test.go create mode 100644 pkg/commands/runtime_types_test.go create mode 100755 scripts/integration-test.sh create mode 100644 test/podman-compose.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 309b2486..2406ab0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,38 @@ jobs: - name: Test code run: | bash ./test.sh + + integration: + name: integration-tests + runs-on: ubuntu-latest + env: + GOFLAGS: -mod=vendor + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version: 1.24.x + - name: Cache build + uses: actions/cache@v4 + with: + path: ~/.cache/go-build + key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-integration + restore-keys: | + ${{runner.os}}-go- + - name: Install Podman + run: | + sudo apt-get update + sudo apt-get install -y podman + - name: Start Podman socket + run: | + systemctl --user start podman.socket + # Verify socket is available + podman info + - name: Run integration tests + run: | + go test -tags=integration -v ./pkg/commands/... build: runs-on: ubuntu-latest env: diff --git a/pkg/commands/integration_test.go b/pkg/commands/integration_test.go new file mode 100644 index 00000000..e9dc4901 --- /dev/null +++ b/pkg/commands/integration_test.go @@ -0,0 +1,360 @@ +//go:build integration + +package commands + +import ( + "context" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Integration tests require a running Podman instance. +// Run with: go test -tags=integration ./pkg/commands/... + +// getSocketRuntime creates a SocketRuntime for integration testing. +// It skips the test if no Podman socket is available. +func getSocketRuntime(t *testing.T) *SocketRuntime { + t.Helper() + + socketPath := detectSocketPath() + if socketPath == "" { + t.Skip("No Podman socket available") + } + + runtime, err := NewSocketRuntime(socketPath) + if err != nil { + t.Skipf("Failed to connect to Podman socket: %v", err) + } + + return runtime +} + +// TestIntegrationSocketRuntimeMode verifies the runtime mode +func TestIntegrationSocketRuntimeMode(t *testing.T) { + runtime := getSocketRuntime(t) + defer runtime.Close() + + assert.Equal(t, "socket", runtime.Mode()) +} + +// TestIntegrationListContainers tests listing containers from a real Podman instance +func TestIntegrationListContainers(t *testing.T) { + runtime := getSocketRuntime(t) + defer runtime.Close() + + ctx := context.Background() + containers, err := runtime.ListContainers(ctx) + require.NoError(t, err) + + // We should get a list (may be empty if no containers running) + assert.NotNil(t, containers) + + // If there are containers, verify their structure + for _, c := range containers { + assert.NotEmpty(t, c.ID) + assert.NotEmpty(t, c.State) + } +} + +// TestIntegrationListImages tests listing images from a real Podman instance +func TestIntegrationListImages(t *testing.T) { + runtime := getSocketRuntime(t) + defer runtime.Close() + + ctx := context.Background() + images, err := runtime.ListImages(ctx) + require.NoError(t, err) + + // We should get a list (usually at least one image exists) + assert.NotNil(t, images) + + // If there are images, verify their structure + for _, img := range images { + assert.NotEmpty(t, img.ID) + assert.Greater(t, img.Size, int64(0)) + } +} + +// TestIntegrationListVolumes tests listing volumes from a real Podman instance +func TestIntegrationListVolumes(t *testing.T) { + runtime := getSocketRuntime(t) + defer runtime.Close() + + ctx := context.Background() + volumes, err := runtime.ListVolumes(ctx) + require.NoError(t, err) + + // We should get a list (may be empty) + assert.NotNil(t, volumes) + + // If there are volumes, verify their structure + for _, vol := range volumes { + assert.NotEmpty(t, vol.Name) + assert.NotEmpty(t, vol.Driver) + } +} + +// TestIntegrationListNetworks tests listing networks from a real Podman instance +func TestIntegrationListNetworks(t *testing.T) { + runtime := getSocketRuntime(t) + defer runtime.Close() + + ctx := context.Background() + networks, err := runtime.ListNetworks(ctx) + require.NoError(t, err) + + // We should always have at least the default network + assert.NotNil(t, networks) + + // Find the default bridge network + foundBridge := false + for _, nw := range networks { + assert.NotEmpty(t, nw.Name) + if nw.Name == "podman" || nw.Name == "bridge" { + foundBridge = true + } + } + // Podman usually has a default network + if len(networks) > 0 { + assert.True(t, foundBridge || len(networks) >= 1, "Expected at least one network") + } +} + +// TestIntegrationContainerLifecycle tests a complete container lifecycle +// This test creates, starts, inspects, and removes a test container +func TestIntegrationContainerLifecycle(t *testing.T) { + if os.Getenv("RUN_LIFECYCLE_TESTS") != "1" { + t.Skip("Skipping lifecycle tests (set RUN_LIFECYCLE_TESTS=1 to run)") + } + + runtime := getSocketRuntime(t) + defer runtime.Close() + + ctx := context.Background() + + // First, ensure we have an alpine image + images, err := runtime.ListImages(ctx) + require.NoError(t, err) + + hasAlpine := false + for _, img := range images { + for _, tag := range img.RepoTags { + if tag == "alpine:latest" || tag == "docker.io/library/alpine:latest" { + hasAlpine = true + break + } + } + } + + if !hasAlpine { + t.Skip("alpine:latest image not found, skipping lifecycle test") + } + + // List containers before the test + containersBefore, err := runtime.ListContainers(ctx) + require.NoError(t, err) + + t.Logf("Found %d containers before test", len(containersBefore)) + + // The actual container creation/lifecycle would require podman CLI + // since the bindings API doesn't have a simple create method + // This test verifies the list operation works correctly +} + +// TestIntegrationImageInspect tests inspecting an image +func TestIntegrationImageInspect(t *testing.T) { + runtime := getSocketRuntime(t) + defer runtime.Close() + + ctx := context.Background() + + // List images first + images, err := runtime.ListImages(ctx) + require.NoError(t, err) + + if len(images) == 0 { + t.Skip("No images available for inspection") + } + + // Inspect the first image + details, err := runtime.InspectImage(ctx, images[0].ID) + require.NoError(t, err) + + assert.NotNil(t, details) + assert.Equal(t, images[0].ID, details.ID) + assert.NotEmpty(t, details.Architecture) + assert.NotEmpty(t, details.Os) +} + +// TestIntegrationImageHistory tests getting image history +func TestIntegrationImageHistory(t *testing.T) { + runtime := getSocketRuntime(t) + defer runtime.Close() + + ctx := context.Background() + + // List images first + images, err := runtime.ListImages(ctx) + require.NoError(t, err) + + if len(images) == 0 { + t.Skip("No images available for history") + } + + // Get history of the first image + history, err := runtime.ImageHistory(ctx, images[0].ID) + require.NoError(t, err) + + assert.NotNil(t, history) + // Images should have at least one layer + assert.GreaterOrEqual(t, len(history), 1) +} + +// TestIntegrationContainerInspect tests inspecting a container if one exists +func TestIntegrationContainerInspect(t *testing.T) { + runtime := getSocketRuntime(t) + defer runtime.Close() + + ctx := context.Background() + + // List containers + containers, err := runtime.ListContainers(ctx) + require.NoError(t, err) + + if len(containers) == 0 { + t.Skip("No containers available for inspection") + } + + // Inspect the first container + details, err := runtime.InspectContainer(ctx, containers[0].ID) + require.NoError(t, err) + + assert.NotNil(t, details) + assert.Equal(t, containers[0].ID, details.ID) + assert.NotNil(t, details.State) +} + +// TestIntegrationContainerStats tests getting container stats if a running container exists +func TestIntegrationContainerStats(t *testing.T) { + runtime := getSocketRuntime(t) + defer runtime.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // List running containers + containers, err := runtime.ListContainers(ctx) + require.NoError(t, err) + + // Find a running container + var runningContainer *ContainerSummary + for i := range containers { + if containers[i].State == "running" { + runningContainer = &containers[i] + break + } + } + + if runningContainer == nil { + t.Skip("No running containers available for stats") + } + + // Get one-shot stats (not streaming) + statsChan, errChan := runtime.ContainerStats(ctx, runningContainer.ID, false) + + select { + case stats, ok := <-statsChan: + if ok { + assert.Equal(t, runningContainer.ID, stats.ID) + } + case err := <-errChan: + if err != nil { + t.Logf("Stats error (may be expected): %v", err) + } + case <-ctx.Done(): + t.Log("Stats timed out (may be expected for one-shot)") + } +} + +// TestIntegrationContainerTop tests getting process info if a running container exists +func TestIntegrationContainerTop(t *testing.T) { + runtime := getSocketRuntime(t) + defer runtime.Close() + + ctx := context.Background() + + // List running containers + containers, err := runtime.ListContainers(ctx) + require.NoError(t, err) + + // Find a running container + var runningContainer *ContainerSummary + for i := range containers { + if containers[i].State == "running" { + runningContainer = &containers[i] + break + } + } + + if runningContainer == nil { + t.Skip("No running containers available for top") + } + + // Get top info + headers, processes, err := runtime.ContainerTop(ctx, runningContainer.ID) + require.NoError(t, err) + + assert.NotEmpty(t, headers) + assert.NotNil(t, processes) +} + +// TestIntegrationEvents tests the event stream briefly +func TestIntegrationEvents(t *testing.T) { + if os.Getenv("RUN_EVENT_TESTS") != "1" { + t.Skip("Skipping event tests (set RUN_EVENT_TESTS=1 to run)") + } + + runtime := getSocketRuntime(t) + defer runtime.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + eventChan, errChan := runtime.Events(ctx) + + // Just verify the channels are created and we can listen + select { + case event := <-eventChan: + t.Logf("Received event: type=%s action=%s", event.Type, event.Action) + case err := <-errChan: + if err != nil { + t.Logf("Event error: %v", err) + } + case <-ctx.Done(): + t.Log("No events received (expected if no container activity)") + } +} + +// TestIntegrationRuntimeClose tests that Close works without error +func TestIntegrationRuntimeClose(t *testing.T) { + runtime := getSocketRuntime(t) + + err := runtime.Close() + assert.NoError(t, err) +} + +// TestIntegrationDetectSocketPath tests socket path detection +func TestIntegrationDetectSocketPath(t *testing.T) { + path := detectSocketPath() + + if path == "" { + t.Log("No socket path detected") + } else { + t.Logf("Detected socket path: %s", path) + assert.NotEmpty(t, path) + } +} diff --git a/pkg/commands/podman_host_test.go b/pkg/commands/podman_host_test.go new file mode 100644 index 00000000..bc97f725 --- /dev/null +++ b/pkg/commands/podman_host_test.go @@ -0,0 +1,216 @@ +//go:build !windows + +package commands + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDetectSocketPath_ContainerHost(t *testing.T) { + // Save original value + originalContainerHost := os.Getenv("CONTAINER_HOST") + originalDockerHost := os.Getenv("DOCKER_HOST") + defer func() { + os.Setenv("CONTAINER_HOST", originalContainerHost) + os.Setenv("DOCKER_HOST", originalDockerHost) + }() + + // Clear both env vars first + os.Unsetenv("CONTAINER_HOST") + os.Unsetenv("DOCKER_HOST") + + // Test CONTAINER_HOST takes priority + os.Setenv("CONTAINER_HOST", "unix:///custom/podman.sock") + result := detectSocketPath() + assert.Equal(t, "unix:///custom/podman.sock", result) +} + +func TestDetectSocketPath_DockerHostFallback(t *testing.T) { + // Save original values + originalContainerHost := os.Getenv("CONTAINER_HOST") + originalDockerHost := os.Getenv("DOCKER_HOST") + defer func() { + os.Setenv("CONTAINER_HOST", originalContainerHost) + os.Setenv("DOCKER_HOST", originalDockerHost) + }() + + // Clear CONTAINER_HOST, set DOCKER_HOST + os.Unsetenv("CONTAINER_HOST") + os.Setenv("DOCKER_HOST", "unix:///var/run/docker.sock") + + result := detectSocketPath() + assert.Equal(t, "unix:///var/run/docker.sock", result) +} + +func TestDetectSocketPath_ContainerHostPriority(t *testing.T) { + // Save original values + originalContainerHost := os.Getenv("CONTAINER_HOST") + originalDockerHost := os.Getenv("DOCKER_HOST") + defer func() { + os.Setenv("CONTAINER_HOST", originalContainerHost) + os.Setenv("DOCKER_HOST", originalDockerHost) + }() + + // Set both - CONTAINER_HOST should take priority + os.Setenv("CONTAINER_HOST", "unix:///podman.sock") + os.Setenv("DOCKER_HOST", "unix:///docker.sock") + + result := detectSocketPath() + assert.Equal(t, "unix:///podman.sock", result) +} + +func TestDetectSocketPath_SSHHost(t *testing.T) { + // Save original value + originalContainerHost := os.Getenv("CONTAINER_HOST") + defer func() { + os.Setenv("CONTAINER_HOST", originalContainerHost) + }() + + // Test SSH format + os.Setenv("CONTAINER_HOST", "ssh://user@remote.host:22/run/podman/podman.sock") + result := detectSocketPath() + assert.Equal(t, "ssh://user@remote.host:22/run/podman/podman.sock", result) +} + +func TestDetectSocketPath_EmptyEnvVars(t *testing.T) { + // Save original values + originalContainerHost := os.Getenv("CONTAINER_HOST") + originalDockerHost := os.Getenv("DOCKER_HOST") + defer func() { + os.Setenv("CONTAINER_HOST", originalContainerHost) + os.Setenv("DOCKER_HOST", originalDockerHost) + }() + + // Clear all env vars + os.Unsetenv("CONTAINER_HOST") + os.Unsetenv("DOCKER_HOST") + + // The result depends on whether socket files exist on the system + // We can at least verify it doesn't panic and returns a string + result := detectSocketPath() + // Result will be either a socket path or empty string + assert.IsType(t, "", result) +} + +func TestDetectSocketPath_RootlessPath(t *testing.T) { + // Save original values + originalContainerHost := os.Getenv("CONTAINER_HOST") + originalDockerHost := os.Getenv("DOCKER_HOST") + defer func() { + os.Setenv("CONTAINER_HOST", originalContainerHost) + os.Setenv("DOCKER_HOST", originalDockerHost) + }() + + // Clear env vars to test socket detection + os.Unsetenv("CONTAINER_HOST") + os.Unsetenv("DOCKER_HOST") + + // Get current UID + uid := os.Getuid() + expectedRootlessPath := fmt.Sprintf("unix:///run/user/%d/podman/podman.sock", uid) + + result := detectSocketPath() + + // If rootless socket exists, it should return that path + // Otherwise, it will try other paths + if result == expectedRootlessPath { + assert.Equal(t, expectedRootlessPath, result) + } + // This is a valid test outcome - the socket may or may not exist +} + +func TestDetectSocketPath_VariousFormats(t *testing.T) { + // Save original value + originalContainerHost := os.Getenv("CONTAINER_HOST") + defer func() { + os.Setenv("CONTAINER_HOST", originalContainerHost) + }() + + testCases := []struct { + name string + envValue string + expected string + }{ + { + name: "Unix socket with unix:// prefix", + envValue: "unix:///run/podman/podman.sock", + expected: "unix:///run/podman/podman.sock", + }, + { + name: "SSH connection", + envValue: "ssh://root@192.168.1.100:22/run/podman/podman.sock", + expected: "ssh://root@192.168.1.100:22/run/podman/podman.sock", + }, + { + name: "TCP connection", + envValue: "tcp://localhost:8080", + expected: "tcp://localhost:8080", + }, + { + name: "Path without prefix", + envValue: "/run/podman/podman.sock", + expected: "/run/podman/podman.sock", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + os.Setenv("CONTAINER_HOST", tc.envValue) + result := detectSocketPath() + assert.Equal(t, tc.expected, result) + }) + } +} + +// Test that the function handles edge cases +func TestDetectSocketPath_EdgeCases(t *testing.T) { + // Save original values + originalContainerHost := os.Getenv("CONTAINER_HOST") + originalDockerHost := os.Getenv("DOCKER_HOST") + defer func() { + os.Setenv("CONTAINER_HOST", originalContainerHost) + os.Setenv("DOCKER_HOST", originalDockerHost) + }() + + testCases := []struct { + name string + containerHost string + dockerHost string + expected string + }{ + { + name: "Empty CONTAINER_HOST with spaces", + containerHost: " ", + dockerHost: "", + expected: " ", // Whitespace is treated as a value + }, + { + name: "Very long path", + containerHost: "unix:///this/is/a/very/long/path/to/a/socket/file/that/might/exist/somewhere/podman.sock", + dockerHost: "", + expected: "unix:///this/is/a/very/long/path/to/a/socket/file/that/might/exist/somewhere/podman.sock", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.containerHost != "" { + os.Setenv("CONTAINER_HOST", tc.containerHost) + } else { + os.Unsetenv("CONTAINER_HOST") + } + if tc.dockerHost != "" { + os.Setenv("DOCKER_HOST", tc.dockerHost) + } else { + os.Unsetenv("DOCKER_HOST") + } + + result := detectSocketPath() + assert.Equal(t, tc.expected, result) + }) + } +} diff --git a/pkg/commands/podman_test.go b/pkg/commands/podman_test.go new file mode 100644 index 00000000..c0d70e84 --- /dev/null +++ b/pkg/commands/podman_test.go @@ -0,0 +1,533 @@ +package commands + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// TestNewCommandObject tests the NewCommandObject method +func TestNewCommandObject(t *testing.T) { + podmanCmd := NewDummyPodmanCommand() + + t.Run("returns default object when empty input", func(t *testing.T) { + obj := podmanCmd.NewCommandObject(CommandObject{}) + assert.Equal(t, "podman-compose", obj.PodmanCompose) + }) + + t.Run("merges passed object with defaults", func(t *testing.T) { + container := &Container{ID: "abc123", Name: "test-container"} + obj := podmanCmd.NewCommandObject(CommandObject{Container: container}) + + assert.Equal(t, "podman-compose", obj.PodmanCompose) + assert.NotNil(t, obj.Container) + assert.Equal(t, "abc123", obj.Container.ID) + }) +} + +// TestPodmanCommandClose tests the Close method +func TestPodmanCommandClose(t *testing.T) { + t.Run("closes runtime when set", func(t *testing.T) { + mock := &MockRuntime{} + closeCalled := false + mock.CloseFunc = func() error { + closeCalled = true + return nil + } + + podmanCmd := NewDummyPodmanCommand() + podmanCmd.Runtime = mock + + err := podmanCmd.Close() + assert.NoError(t, err) + assert.True(t, closeCalled) + }) + + t.Run("handles nil runtime gracefully", func(t *testing.T) { + podmanCmd := NewDummyPodmanCommand() + podmanCmd.Runtime = nil + + err := podmanCmd.Close() + assert.NoError(t, err) + }) +} + +// TestCalculateCPUPercentageFromEntry tests CPU percentage calculation +func TestCalculateCPUPercentageFromEntry(t *testing.T) { + testCases := []struct { + name string + stats ContainerStatsEntry + expected float64 + }{ + { + name: "normal CPU usage", + stats: ContainerStatsEntry{ + CPUStats: CPUStats{ + CPUUsage: CPUUsage{TotalUsage: 2000000000}, + SystemCPUUsage: 20000000000, + }, + PreCPUStats: CPUStats{ + CPUUsage: CPUUsage{TotalUsage: 1000000000}, + SystemCPUUsage: 10000000000, + }, + }, + expected: 10.0, // (1000000000 / 10000000000) * 100 = 10% + }, + { + name: "zero system delta", + stats: ContainerStatsEntry{ + CPUStats: CPUStats{ + CPUUsage: CPUUsage{TotalUsage: 1000000000}, + SystemCPUUsage: 10000000000, + }, + PreCPUStats: CPUStats{ + CPUUsage: CPUUsage{TotalUsage: 500000000}, + SystemCPUUsage: 10000000000, + }, + }, + expected: 0.0, // system delta is 0 + }, + { + name: "zero CPU delta", + stats: ContainerStatsEntry{ + CPUStats: CPUStats{ + CPUUsage: CPUUsage{TotalUsage: 1000000000}, + SystemCPUUsage: 20000000000, + }, + PreCPUStats: CPUStats{ + CPUUsage: CPUUsage{TotalUsage: 1000000000}, + SystemCPUUsage: 10000000000, + }, + }, + expected: 0.0, // CPU delta is 0 + }, + { + name: "all zeros", + stats: ContainerStatsEntry{ + CPUStats: CPUStats{}, + PreCPUStats: CPUStats{}, + }, + expected: 0.0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := calculateCPUPercentageFromEntry(tc.stats) + assert.InDelta(t, tc.expected, result, 0.01) + }) + } +} + +// TestCalculateMemoryPercentageFromEntry tests memory percentage calculation +func TestCalculateMemoryPercentageFromEntry(t *testing.T) { + testCases := []struct { + name string + stats ContainerStatsEntry + expected float64 + }{ + { + name: "50% memory usage", + stats: ContainerStatsEntry{ + MemoryStats: MemoryStats{ + Usage: 256 * 1024 * 1024, // 256 MiB + Limit: 512 * 1024 * 1024, // 512 MiB + }, + }, + expected: 50.0, + }, + { + name: "100% memory usage", + stats: ContainerStatsEntry{ + MemoryStats: MemoryStats{ + Usage: 512 * 1024 * 1024, + Limit: 512 * 1024 * 1024, + }, + }, + expected: 100.0, + }, + { + name: "zero limit", + stats: ContainerStatsEntry{ + MemoryStats: MemoryStats{ + Usage: 256 * 1024 * 1024, + Limit: 0, + }, + }, + expected: 0.0, + }, + { + name: "zero usage", + stats: ContainerStatsEntry{ + MemoryStats: MemoryStats{ + Usage: 0, + Limit: 512 * 1024 * 1024, + }, + }, + expected: 0.0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := calculateMemoryPercentageFromEntry(tc.stats) + assert.InDelta(t, tc.expected, result, 0.01) + }) + } +} + +// TestConvertStatsEntryToContainerStats tests stats conversion +func TestConvertStatsEntryToContainerStats(t *testing.T) { + now := time.Now() + entry := ContainerStatsEntry{ + Read: now, + PreRead: now.Add(-time.Second), + CPUStats: CPUStats{ + CPUUsage: CPUUsage{ + TotalUsage: 1000000000, + PercpuUsage: []int64{500000000, 500000000}, + UsageInKernelmode: 100000000, + UsageInUsermode: 900000000, + }, + SystemCPUUsage: 10000000000, + OnlineCpus: 2, + }, + PreCPUStats: CPUStats{ + CPUUsage: CPUUsage{ + TotalUsage: 900000000, + }, + SystemCPUUsage: 9000000000, + OnlineCpus: 2, + }, + MemoryStats: MemoryStats{ + Usage: 104857600, + MaxUsage: 209715200, + Limit: 536870912, + }, + PidsStats: PidsStats{ + Current: 10, + }, + Name: "test-container", + ID: "abc123", + } + + result := convertStatsEntryToContainerStats(entry) + + assert.Equal(t, now, result.Read) + assert.Equal(t, int64(1000000000), result.CPUStats.CPUUsage.TotalUsage) + assert.Equal(t, 2, result.CPUStats.OnlineCpus) + assert.Equal(t, 104857600, result.MemoryStats.Usage) + assert.Equal(t, int64(536870912), result.MemoryStats.Limit) + assert.Equal(t, 10, result.PidsStats.Current) + assert.Equal(t, "test-container", result.Name) + assert.Equal(t, "abc123", result.ID) +} + +// TestPodmanCommandGetContainers tests GetContainers method +func TestPodmanCommandGetContainers(t *testing.T) { + mock := &MockRuntime{} + podmanCmd := NewDummyPodmanCommand() + podmanCmd.Runtime = mock + + t.Run("returns containers from runtime", func(t *testing.T) { + mock.ListContainersFunc = func(ctx context.Context) ([]ContainerSummary, error) { + return []ContainerSummary{ + { + ID: "abc123", + Names: []string{"/my-container"}, + Image: "alpine:latest", + State: "running", + Labels: map[string]string{}, + }, + { + ID: "def456", + Names: []string{"/another-container"}, + Image: "nginx:latest", + State: "exited", + Labels: map[string]string{"name": "custom-name"}, + }, + }, nil + } + + mock.InspectContainerFunc = func(ctx context.Context, id string) (*ContainerDetails, error) { + return &ContainerDetails{ID: id}, nil + } + + containers, err := podmanCmd.GetContainers(nil) + assert.NoError(t, err) + assert.Len(t, containers, 2) + assert.Equal(t, "abc123", containers[0].ID) + assert.Equal(t, "my-container", containers[0].Name) + assert.Equal(t, "custom-name", containers[1].Name) // Uses label name + }) + + t.Run("reuses existing container data", func(t *testing.T) { + existingContainer := &Container{ + ID: "abc123", + Name: "existing-container", + } + + mock.ListContainersFunc = func(ctx context.Context) ([]ContainerSummary, error) { + return []ContainerSummary{ + { + ID: "abc123", + Names: []string{"/my-container"}, + State: "running", + Labels: map[string]string{}, + }, + }, nil + } + + containers, err := podmanCmd.GetContainers([]*Container{existingContainer}) + assert.NoError(t, err) + assert.Len(t, containers, 1) + assert.Same(t, existingContainer, containers[0]) + }) + + t.Run("extracts compose labels", func(t *testing.T) { + mock.ListContainersFunc = func(ctx context.Context) ([]ContainerSummary, error) { + return []ContainerSummary{ + { + ID: "abc123", + Names: []string{"/project_service_1"}, + State: "running", + Labels: map[string]string{ + "com.docker.compose.service": "web", + "com.docker.compose.project": "myproject", + "com.docker.compose.container": "1", + "com.docker.compose.oneoff": "True", + }, + }, + }, nil + } + + containers, err := podmanCmd.GetContainers(nil) + assert.NoError(t, err) + assert.Len(t, containers, 1) + assert.Equal(t, "web", containers[0].ServiceName) + assert.Equal(t, "myproject", containers[0].ProjectName) + assert.Equal(t, "1", containers[0].ContainerNumber) + assert.True(t, containers[0].OneOff) + }) +} + +// TestPodmanCommandRefreshImages tests RefreshImages method +func TestPodmanCommandRefreshImages(t *testing.T) { + mock := &MockRuntime{} + podmanCmd := NewDummyPodmanCommand() + podmanCmd.Runtime = mock + + t.Run("returns images from runtime", func(t *testing.T) { + mock.ListImagesFunc = func(ctx context.Context) ([]ImageSummary, error) { + return []ImageSummary{ + { + ID: "sha256:abc123", + RepoTags: []string{"alpine:latest", "alpine:3.19"}, + Size: 5242880, + }, + { + ID: "sha256:def456", + RepoTags: []string{"nginx:1.25"}, + Size: 142606336, + }, + }, nil + } + + images, err := podmanCmd.RefreshImages() + assert.NoError(t, err) + assert.Len(t, images, 2) + assert.Equal(t, "sha256:abc123", images[0].ID) + assert.Equal(t, "alpine", images[0].Name) + assert.Equal(t, "latest", images[0].Tag) + }) + + t.Run("handles images with no tags", func(t *testing.T) { + mock.ListImagesFunc = func(ctx context.Context) ([]ImageSummary, error) { + return []ImageSummary{ + { + ID: "sha256:abc123", + RepoTags: []string{}, + Size: 5242880, + }, + }, nil + } + + images, err := podmanCmd.RefreshImages() + assert.NoError(t, err) + assert.Len(t, images, 1) + assert.Equal(t, "none", images[0].Name) + assert.Equal(t, "", images[0].Tag) + }) +} + +// TestPodmanCommandRefreshVolumes tests RefreshVolumes method +func TestPodmanCommandRefreshVolumes(t *testing.T) { + mock := &MockRuntime{} + podmanCmd := NewDummyPodmanCommand() + podmanCmd.Runtime = mock + + mock.ListVolumesFunc = func(ctx context.Context) ([]VolumeSummary, error) { + return []VolumeSummary{ + { + Name: "mydata", + Driver: "local", + Mountpoint: "/var/lib/containers/storage/volumes/mydata/_data", + }, + { + Name: "cache", + Driver: "local", + Mountpoint: "/var/lib/containers/storage/volumes/cache/_data", + }, + }, nil + } + + volumes, err := podmanCmd.RefreshVolumes() + assert.NoError(t, err) + assert.Len(t, volumes, 2) + assert.Equal(t, "mydata", volumes[0].Name) + assert.Equal(t, "cache", volumes[1].Name) +} + +// TestPodmanCommandRefreshNetworks tests RefreshNetworks method +func TestPodmanCommandRefreshNetworks(t *testing.T) { + mock := &MockRuntime{} + podmanCmd := NewDummyPodmanCommand() + podmanCmd.Runtime = mock + + mock.ListNetworksFunc = func(ctx context.Context) ([]NetworkSummary, error) { + return []NetworkSummary{ + {Name: "bridge", ID: "net1", Driver: "bridge"}, + {Name: "host", ID: "net2", Driver: "host"}, + }, nil + } + + networks, err := podmanCmd.RefreshNetworks() + assert.NoError(t, err) + assert.Len(t, networks, 2) + assert.Equal(t, "bridge", networks[0].Name) + assert.Equal(t, "host", networks[1].Name) +} + +// TestPodmanCommandPruneOperations tests prune methods +func TestPodmanCommandPruneOperations(t *testing.T) { + mock := &MockRuntime{} + podmanCmd := NewDummyPodmanCommand() + podmanCmd.Runtime = mock + + t.Run("PruneContainers", func(t *testing.T) { + mock.PruneContainersFunc = func(ctx context.Context) error { + return nil + } + + err := podmanCmd.PruneContainers() + assert.NoError(t, err) + assert.True(t, mock.WasCalled("PruneContainers")) + }) + + mock.Reset() + + t.Run("PruneImages", func(t *testing.T) { + mock.PruneImagesFunc = func(ctx context.Context) error { + return nil + } + + err := podmanCmd.PruneImages() + assert.NoError(t, err) + assert.True(t, mock.WasCalled("PruneImages")) + }) + + mock.Reset() + + t.Run("PruneVolumes", func(t *testing.T) { + mock.PruneVolumesFunc = func(ctx context.Context) error { + return nil + } + + err := podmanCmd.PruneVolumes() + assert.NoError(t, err) + assert.True(t, mock.WasCalled("PruneVolumes")) + }) + + mock.Reset() + + t.Run("PruneNetworks", func(t *testing.T) { + mock.PruneNetworksFunc = func(ctx context.Context) error { + return nil + } + + err := podmanCmd.PruneNetworks() + assert.NoError(t, err) + assert.True(t, mock.WasCalled("PruneNetworks")) + }) +} + +// TestPodmanCommandAssignContainersToServices tests service assignment +func TestPodmanCommandAssignContainersToServices(t *testing.T) { + podmanCmd := NewDummyPodmanCommand() + + containers := []*Container{ + {ID: "c1", ServiceName: "web", OneOff: false}, + {ID: "c2", ServiceName: "db", OneOff: false}, + {ID: "c3", ServiceName: "web", OneOff: true}, // OneOff should not be assigned + } + + services := []*Service{ + {Name: "web"}, + {Name: "db"}, + {Name: "cache"}, + } + + podmanCmd.assignContainersToServices(containers, services) + + assert.Equal(t, containers[0], services[0].Container) // web + assert.Equal(t, containers[1], services[1].Container) // db + assert.Nil(t, services[2].Container) // cache has no container +} + +// TestPodmanCommandGetServices tests GetServices method +func TestPodmanCommandGetServices(t *testing.T) { + t.Run("returns nil when not in compose project", func(t *testing.T) { + podmanCmd := NewDummyPodmanCommand() + podmanCmd.InComposeProject = false + + services, err := podmanCmd.GetServices() + assert.NoError(t, err) + assert.Nil(t, services) + }) +} + +// TestCommandObjectFields tests CommandObject struct +func TestCommandObjectFields(t *testing.T) { + container := &Container{ID: "c1", Name: "test"} + service := &Service{ID: "s1", Name: "web"} + image := &Image{ID: "i1", Name: "alpine"} + volume := &Volume{Name: "v1"} + network := &Network{Name: "n1"} + + obj := CommandObject{ + PodmanCompose: "podman-compose", + Service: service, + Container: container, + Image: image, + Volume: volume, + Network: network, + } + + assert.Equal(t, "podman-compose", obj.PodmanCompose) + assert.Equal(t, container, obj.Container) + assert.Equal(t, service, obj.Service) + assert.Equal(t, image, obj.Image) + assert.Equal(t, volume, obj.Volume) + assert.Equal(t, network, obj.Network) +} + +// TestPodmanCommandImplementsCloser verifies PodmanCommand implements io.Closer +func TestPodmanCommandImplementsCloser(t *testing.T) { + podmanCmd := NewDummyPodmanCommand() + var closer interface{} = podmanCmd + + _, ok := closer.(interface{ Close() error }) + assert.True(t, ok) +} diff --git a/pkg/commands/runtime_mock.go b/pkg/commands/runtime_mock.go new file mode 100644 index 00000000..8fbeb310 --- /dev/null +++ b/pkg/commands/runtime_mock.go @@ -0,0 +1,309 @@ +package commands + +import ( + "context" + "errors" +) + +// MockRuntime implements ContainerRuntime for testing purposes. +// Each method can be customized by setting the corresponding function field. +// If a function is not set, the method returns sensible defaults or errors. +type MockRuntime struct { + // Container operation mocks + ListContainersFunc func(ctx context.Context) ([]ContainerSummary, error) + InspectContainerFunc func(ctx context.Context, id string) (*ContainerDetails, error) + StartContainerFunc func(ctx context.Context, id string) error + StopContainerFunc func(ctx context.Context, id string, timeout *int) error + PauseContainerFunc func(ctx context.Context, id string) error + UnpauseContainerFunc func(ctx context.Context, id string) error + RestartContainerFunc func(ctx context.Context, id string, timeout *int) error + RemoveContainerFunc func(ctx context.Context, id string, force bool, volumes bool) error + ContainerTopFunc func(ctx context.Context, id string) ([]string, [][]string, error) + PruneContainersFunc func(ctx context.Context) error + ContainerStatsFunc func(ctx context.Context, id string, stream bool) (<-chan ContainerStatsEntry, <-chan error) + + // Image operation mocks + ListImagesFunc func(ctx context.Context) ([]ImageSummary, error) + InspectImageFunc func(ctx context.Context, id string) (*ImageDetails, error) + ImageHistoryFunc func(ctx context.Context, id string) ([]ImageHistoryEntry, error) + RemoveImageFunc func(ctx context.Context, id string, force bool) error + PruneImagesFunc func(ctx context.Context) error + + // Volume operation mocks + ListVolumesFunc func(ctx context.Context) ([]VolumeSummary, error) + RemoveVolumeFunc func(ctx context.Context, name string, force bool) error + PruneVolumesFunc func(ctx context.Context) error + + // Network operation mocks + ListNetworksFunc func(ctx context.Context) ([]NetworkSummary, error) + RemoveNetworkFunc func(ctx context.Context, name string) error + PruneNetworksFunc func(ctx context.Context) error + + // Event mock + EventsFunc func(ctx context.Context) (<-chan Event, <-chan error) + + // Lifecycle mocks + CloseFunc func() error + ModeFunc func() string + + // Track method calls for assertions + Calls []MockCall +} + +// MockCall records a method invocation for verification in tests. +type MockCall struct { + Method string + Args []interface{} +} + +// ErrMockNotImplemented is returned when a mock function is not set. +var ErrMockNotImplemented = errors.New("mock function not implemented") + +// recordCall records a method call for later verification. +func (m *MockRuntime) recordCall(method string, args ...interface{}) { + m.Calls = append(m.Calls, MockCall{Method: method, Args: args}) +} + +// Container operations + +func (m *MockRuntime) ListContainers(ctx context.Context) ([]ContainerSummary, error) { + m.recordCall("ListContainers") + if m.ListContainersFunc != nil { + return m.ListContainersFunc(ctx) + } + return nil, ErrMockNotImplemented +} + +func (m *MockRuntime) InspectContainer(ctx context.Context, id string) (*ContainerDetails, error) { + m.recordCall("InspectContainer", id) + if m.InspectContainerFunc != nil { + return m.InspectContainerFunc(ctx, id) + } + return nil, ErrMockNotImplemented +} + +func (m *MockRuntime) StartContainer(ctx context.Context, id string) error { + m.recordCall("StartContainer", id) + if m.StartContainerFunc != nil { + return m.StartContainerFunc(ctx, id) + } + return ErrMockNotImplemented +} + +func (m *MockRuntime) StopContainer(ctx context.Context, id string, timeout *int) error { + m.recordCall("StopContainer", id, timeout) + if m.StopContainerFunc != nil { + return m.StopContainerFunc(ctx, id, timeout) + } + return ErrMockNotImplemented +} + +func (m *MockRuntime) PauseContainer(ctx context.Context, id string) error { + m.recordCall("PauseContainer", id) + if m.PauseContainerFunc != nil { + return m.PauseContainerFunc(ctx, id) + } + return ErrMockNotImplemented +} + +func (m *MockRuntime) UnpauseContainer(ctx context.Context, id string) error { + m.recordCall("UnpauseContainer", id) + if m.UnpauseContainerFunc != nil { + return m.UnpauseContainerFunc(ctx, id) + } + return ErrMockNotImplemented +} + +func (m *MockRuntime) RestartContainer(ctx context.Context, id string, timeout *int) error { + m.recordCall("RestartContainer", id, timeout) + if m.RestartContainerFunc != nil { + return m.RestartContainerFunc(ctx, id, timeout) + } + return ErrMockNotImplemented +} + +func (m *MockRuntime) RemoveContainer(ctx context.Context, id string, force bool, volumes bool) error { + m.recordCall("RemoveContainer", id, force, volumes) + if m.RemoveContainerFunc != nil { + return m.RemoveContainerFunc(ctx, id, force, volumes) + } + return ErrMockNotImplemented +} + +func (m *MockRuntime) ContainerTop(ctx context.Context, id string) ([]string, [][]string, error) { + m.recordCall("ContainerTop", id) + if m.ContainerTopFunc != nil { + return m.ContainerTopFunc(ctx, id) + } + return nil, nil, ErrMockNotImplemented +} + +func (m *MockRuntime) PruneContainers(ctx context.Context) error { + m.recordCall("PruneContainers") + if m.PruneContainersFunc != nil { + return m.PruneContainersFunc(ctx) + } + return ErrMockNotImplemented +} + +func (m *MockRuntime) ContainerStats(ctx context.Context, id string, stream bool) (<-chan ContainerStatsEntry, <-chan error) { + m.recordCall("ContainerStats", id, stream) + if m.ContainerStatsFunc != nil { + return m.ContainerStatsFunc(ctx, id, stream) + } + errCh := make(chan error, 1) + errCh <- ErrMockNotImplemented + close(errCh) + return nil, errCh +} + +// Image operations + +func (m *MockRuntime) ListImages(ctx context.Context) ([]ImageSummary, error) { + m.recordCall("ListImages") + if m.ListImagesFunc != nil { + return m.ListImagesFunc(ctx) + } + return nil, ErrMockNotImplemented +} + +func (m *MockRuntime) InspectImage(ctx context.Context, id string) (*ImageDetails, error) { + m.recordCall("InspectImage", id) + if m.InspectImageFunc != nil { + return m.InspectImageFunc(ctx, id) + } + return nil, ErrMockNotImplemented +} + +func (m *MockRuntime) ImageHistory(ctx context.Context, id string) ([]ImageHistoryEntry, error) { + m.recordCall("ImageHistory", id) + if m.ImageHistoryFunc != nil { + return m.ImageHistoryFunc(ctx, id) + } + return nil, ErrMockNotImplemented +} + +func (m *MockRuntime) RemoveImage(ctx context.Context, id string, force bool) error { + m.recordCall("RemoveImage", id, force) + if m.RemoveImageFunc != nil { + return m.RemoveImageFunc(ctx, id, force) + } + return ErrMockNotImplemented +} + +func (m *MockRuntime) PruneImages(ctx context.Context) error { + m.recordCall("PruneImages") + if m.PruneImagesFunc != nil { + return m.PruneImagesFunc(ctx) + } + return ErrMockNotImplemented +} + +// Volume operations + +func (m *MockRuntime) ListVolumes(ctx context.Context) ([]VolumeSummary, error) { + m.recordCall("ListVolumes") + if m.ListVolumesFunc != nil { + return m.ListVolumesFunc(ctx) + } + return nil, ErrMockNotImplemented +} + +func (m *MockRuntime) RemoveVolume(ctx context.Context, name string, force bool) error { + m.recordCall("RemoveVolume", name, force) + if m.RemoveVolumeFunc != nil { + return m.RemoveVolumeFunc(ctx, name, force) + } + return ErrMockNotImplemented +} + +func (m *MockRuntime) PruneVolumes(ctx context.Context) error { + m.recordCall("PruneVolumes") + if m.PruneVolumesFunc != nil { + return m.PruneVolumesFunc(ctx) + } + return ErrMockNotImplemented +} + +// Network operations + +func (m *MockRuntime) ListNetworks(ctx context.Context) ([]NetworkSummary, error) { + m.recordCall("ListNetworks") + if m.ListNetworksFunc != nil { + return m.ListNetworksFunc(ctx) + } + return nil, ErrMockNotImplemented +} + +func (m *MockRuntime) RemoveNetwork(ctx context.Context, name string) error { + m.recordCall("RemoveNetwork", name) + if m.RemoveNetworkFunc != nil { + return m.RemoveNetworkFunc(ctx, name) + } + return ErrMockNotImplemented +} + +func (m *MockRuntime) PruneNetworks(ctx context.Context) error { + m.recordCall("PruneNetworks") + if m.PruneNetworksFunc != nil { + return m.PruneNetworksFunc(ctx) + } + return ErrMockNotImplemented +} + +// Events + +func (m *MockRuntime) Events(ctx context.Context) (<-chan Event, <-chan error) { + m.recordCall("Events") + if m.EventsFunc != nil { + return m.EventsFunc(ctx) + } + errCh := make(chan error, 1) + errCh <- ErrMockNotImplemented + close(errCh) + return nil, errCh +} + +// Lifecycle + +func (m *MockRuntime) Close() error { + m.recordCall("Close") + if m.CloseFunc != nil { + return m.CloseFunc() + } + return nil +} + +func (m *MockRuntime) Mode() string { + m.recordCall("Mode") + if m.ModeFunc != nil { + return m.ModeFunc() + } + return "mock" +} + +// Helper methods for test assertions + +// CallCount returns the number of times a method was called. +func (m *MockRuntime) CallCount(method string) int { + count := 0 + for _, call := range m.Calls { + if call.Method == method { + count++ + } + } + return count +} + +// WasCalled returns true if the method was called at least once. +func (m *MockRuntime) WasCalled(method string) bool { + return m.CallCount(method) > 0 +} + +// Reset clears all recorded calls. +func (m *MockRuntime) Reset() { + m.Calls = nil +} + +// Verify that MockRuntime implements ContainerRuntime at compile time. +var _ ContainerRuntime = (*MockRuntime)(nil) diff --git a/pkg/commands/runtime_test.go b/pkg/commands/runtime_test.go new file mode 100644 index 00000000..cd57bb8d --- /dev/null +++ b/pkg/commands/runtime_test.go @@ -0,0 +1,593 @@ +package commands + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// TestMockRuntimeImplementsInterface verifies MockRuntime implements ContainerRuntime +func TestMockRuntimeImplementsInterface(t *testing.T) { + var _ ContainerRuntime = (*MockRuntime)(nil) +} + +// TestMockRuntimeListContainers tests the ListContainers mock +func TestMockRuntimeListContainers(t *testing.T) { + mock := &MockRuntime{} + ctx := context.Background() + + t.Run("returns error when not implemented", func(t *testing.T) { + containers, err := mock.ListContainers(ctx) + assert.Nil(t, containers) + assert.Equal(t, ErrMockNotImplemented, err) + }) + + t.Run("returns custom result when function set", func(t *testing.T) { + expectedContainers := []ContainerSummary{ + {ID: "abc123", Names: []string{"/test-container"}, State: "running"}, + {ID: "def456", Names: []string{"/another-container"}, State: "exited"}, + } + + mock.ListContainersFunc = func(ctx context.Context) ([]ContainerSummary, error) { + return expectedContainers, nil + } + + containers, err := mock.ListContainers(ctx) + assert.NoError(t, err) + assert.Len(t, containers, 2) + assert.Equal(t, "abc123", containers[0].ID) + assert.Equal(t, "running", containers[0].State) + }) + + t.Run("returns custom error when function set", func(t *testing.T) { + customErr := errors.New("connection refused") + mock.ListContainersFunc = func(ctx context.Context) ([]ContainerSummary, error) { + return nil, customErr + } + + containers, err := mock.ListContainers(ctx) + assert.Nil(t, containers) + assert.Equal(t, customErr, err) + }) +} + +// TestMockRuntimeInspectContainer tests the InspectContainer mock +func TestMockRuntimeInspectContainer(t *testing.T) { + mock := &MockRuntime{} + ctx := context.Background() + + t.Run("returns error when not implemented", func(t *testing.T) { + details, err := mock.InspectContainer(ctx, "abc123") + assert.Nil(t, details) + assert.Equal(t, ErrMockNotImplemented, err) + }) + + t.Run("returns custom result when function set", func(t *testing.T) { + expectedDetails := &ContainerDetails{ + ID: "abc123", + Name: "/test-container", + State: &ContainerState{ + Status: "running", + Running: true, + }, + } + + mock.InspectContainerFunc = func(ctx context.Context, id string) (*ContainerDetails, error) { + assert.Equal(t, "abc123", id) + return expectedDetails, nil + } + + details, err := mock.InspectContainer(ctx, "abc123") + assert.NoError(t, err) + assert.Equal(t, "abc123", details.ID) + assert.True(t, details.State.Running) + }) +} + +// TestMockRuntimeContainerLifecycle tests container lifecycle mocks +func TestMockRuntimeContainerLifecycle(t *testing.T) { + mock := &MockRuntime{} + ctx := context.Background() + + t.Run("StartContainer", func(t *testing.T) { + mock.StartContainerFunc = func(ctx context.Context, id string) error { + assert.Equal(t, "container1", id) + return nil + } + + err := mock.StartContainer(ctx, "container1") + assert.NoError(t, err) + assert.True(t, mock.WasCalled("StartContainer")) + }) + + t.Run("StopContainer", func(t *testing.T) { + timeout := 10 + mock.StopContainerFunc = func(ctx context.Context, id string, t *int) error { + assert.Equal(t, "container1", id) + assert.Equal(t, 10, *t) + return nil + } + + err := mock.StopContainer(ctx, "container1", &timeout) + assert.NoError(t, err) + }) + + t.Run("PauseContainer", func(t *testing.T) { + mock.PauseContainerFunc = func(ctx context.Context, id string) error { + return nil + } + + err := mock.PauseContainer(ctx, "container1") + assert.NoError(t, err) + }) + + t.Run("UnpauseContainer", func(t *testing.T) { + mock.UnpauseContainerFunc = func(ctx context.Context, id string) error { + return nil + } + + err := mock.UnpauseContainer(ctx, "container1") + assert.NoError(t, err) + }) + + t.Run("RestartContainer", func(t *testing.T) { + mock.RestartContainerFunc = func(ctx context.Context, id string, t *int) error { + return nil + } + + err := mock.RestartContainer(ctx, "container1", nil) + assert.NoError(t, err) + }) + + t.Run("RemoveContainer", func(t *testing.T) { + mock.RemoveContainerFunc = func(ctx context.Context, id string, force bool, volumes bool) error { + assert.Equal(t, "container1", id) + assert.True(t, force) + assert.False(t, volumes) + return nil + } + + err := mock.RemoveContainer(ctx, "container1", true, false) + assert.NoError(t, err) + }) +} + +// TestMockRuntimeContainerTop tests the ContainerTop mock +func TestMockRuntimeContainerTop(t *testing.T) { + mock := &MockRuntime{} + ctx := context.Background() + + mock.ContainerTopFunc = func(ctx context.Context, id string) ([]string, [][]string, error) { + headers := []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"} + processes := [][]string{ + {"root", "1", "0", "0", "10:00", "?", "00:00:01", "/bin/sh"}, + {"root", "10", "1", "0", "10:00", "?", "00:00:00", "sleep infinity"}, + } + return headers, processes, nil + } + + headers, processes, err := mock.ContainerTop(ctx, "container1") + assert.NoError(t, err) + assert.Len(t, headers, 8) + assert.Len(t, processes, 2) +} + +// TestMockRuntimePruneContainers tests the PruneContainers mock +func TestMockRuntimePruneContainers(t *testing.T) { + mock := &MockRuntime{} + ctx := context.Background() + + mock.PruneContainersFunc = func(ctx context.Context) error { + return nil + } + + err := mock.PruneContainers(ctx) + assert.NoError(t, err) + assert.True(t, mock.WasCalled("PruneContainers")) +} + +// TestMockRuntimeContainerStats tests the ContainerStats mock +func TestMockRuntimeContainerStats(t *testing.T) { + mock := &MockRuntime{} + ctx := context.Background() + + t.Run("returns error channel when not implemented", func(t *testing.T) { + statsChan, errChan := mock.ContainerStats(ctx, "container1", false) + assert.Nil(t, statsChan) + + err := <-errChan + assert.Equal(t, ErrMockNotImplemented, err) + }) + + t.Run("streams stats when function set", func(t *testing.T) { + mock.ContainerStatsFunc = func(ctx context.Context, id string, stream bool) (<-chan ContainerStatsEntry, <-chan error) { + statsChan := make(chan ContainerStatsEntry, 2) + errChan := make(chan error, 1) + + go func() { + defer close(statsChan) + defer close(errChan) + + statsChan <- ContainerStatsEntry{ + ID: id, + Name: "test-container", + CPUStats: CPUStats{ + CPUUsage: CPUUsage{TotalUsage: 1000000000}, + }, + MemoryStats: MemoryStats{ + Usage: 104857600, + Limit: 536870912, + }, + } + }() + + return statsChan, errChan + } + + statsChan, errChan := mock.ContainerStats(ctx, "container1", false) + + select { + case stats := <-statsChan: + assert.Equal(t, "container1", stats.ID) + assert.Equal(t, int64(104857600), stats.MemoryStats.Usage) + case err := <-errChan: + t.Fatalf("unexpected error: %v", err) + case <-time.After(time.Second): + t.Fatal("timeout waiting for stats") + } + }) +} + +// TestMockRuntimeImageOperations tests image operation mocks +func TestMockRuntimeImageOperations(t *testing.T) { + mock := &MockRuntime{} + ctx := context.Background() + + t.Run("ListImages", func(t *testing.T) { + mock.ListImagesFunc = func(ctx context.Context) ([]ImageSummary, error) { + return []ImageSummary{ + {ID: "sha256:abc123", RepoTags: []string{"alpine:latest"}, Size: 5242880}, + }, nil + } + + images, err := mock.ListImages(ctx) + assert.NoError(t, err) + assert.Len(t, images, 1) + assert.Equal(t, "sha256:abc123", images[0].ID) + }) + + t.Run("InspectImage", func(t *testing.T) { + mock.InspectImageFunc = func(ctx context.Context, id string) (*ImageDetails, error) { + return &ImageDetails{ + ID: id, + RepoTags: []string{"alpine:latest"}, + Architecture: "amd64", + Os: "linux", + }, nil + } + + details, err := mock.InspectImage(ctx, "sha256:abc123") + assert.NoError(t, err) + assert.Equal(t, "amd64", details.Architecture) + }) + + t.Run("ImageHistory", func(t *testing.T) { + mock.ImageHistoryFunc = func(ctx context.Context, id string) ([]ImageHistoryEntry, error) { + return []ImageHistoryEntry{ + {ID: "layer1", CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]"}, + {ID: "layer2", CreatedBy: "/bin/sh -c apk add --no-cache curl"}, + }, nil + } + + history, err := mock.ImageHistory(ctx, "sha256:abc123") + assert.NoError(t, err) + assert.Len(t, history, 2) + }) + + t.Run("RemoveImage", func(t *testing.T) { + mock.RemoveImageFunc = func(ctx context.Context, id string, force bool) error { + assert.Equal(t, "sha256:abc123", id) + assert.True(t, force) + return nil + } + + err := mock.RemoveImage(ctx, "sha256:abc123", true) + assert.NoError(t, err) + }) + + t.Run("PruneImages", func(t *testing.T) { + mock.PruneImagesFunc = func(ctx context.Context) error { + return nil + } + + err := mock.PruneImages(ctx) + assert.NoError(t, err) + }) +} + +// TestMockRuntimeVolumeOperations tests volume operation mocks +func TestMockRuntimeVolumeOperations(t *testing.T) { + mock := &MockRuntime{} + ctx := context.Background() + + t.Run("ListVolumes", func(t *testing.T) { + mock.ListVolumesFunc = func(ctx context.Context) ([]VolumeSummary, error) { + return []VolumeSummary{ + {Name: "mydata", Driver: "local", Mountpoint: "/var/lib/containers/storage/volumes/mydata/_data"}, + }, nil + } + + volumes, err := mock.ListVolumes(ctx) + assert.NoError(t, err) + assert.Len(t, volumes, 1) + assert.Equal(t, "mydata", volumes[0].Name) + }) + + t.Run("RemoveVolume", func(t *testing.T) { + mock.RemoveVolumeFunc = func(ctx context.Context, name string, force bool) error { + assert.Equal(t, "mydata", name) + return nil + } + + err := mock.RemoveVolume(ctx, "mydata", false) + assert.NoError(t, err) + }) + + t.Run("PruneVolumes", func(t *testing.T) { + mock.PruneVolumesFunc = func(ctx context.Context) error { + return nil + } + + err := mock.PruneVolumes(ctx) + assert.NoError(t, err) + }) +} + +// TestMockRuntimeNetworkOperations tests network operation mocks +func TestMockRuntimeNetworkOperations(t *testing.T) { + mock := &MockRuntime{} + ctx := context.Background() + + t.Run("ListNetworks", func(t *testing.T) { + mock.ListNetworksFunc = func(ctx context.Context) ([]NetworkSummary, error) { + return []NetworkSummary{ + {Name: "bridge", ID: "net123", Driver: "bridge"}, + }, nil + } + + networks, err := mock.ListNetworks(ctx) + assert.NoError(t, err) + assert.Len(t, networks, 1) + assert.Equal(t, "bridge", networks[0].Name) + }) + + t.Run("RemoveNetwork", func(t *testing.T) { + mock.RemoveNetworkFunc = func(ctx context.Context, name string) error { + assert.Equal(t, "mynetwork", name) + return nil + } + + err := mock.RemoveNetwork(ctx, "mynetwork") + assert.NoError(t, err) + }) + + t.Run("PruneNetworks", func(t *testing.T) { + mock.PruneNetworksFunc = func(ctx context.Context) error { + return nil + } + + err := mock.PruneNetworks(ctx) + assert.NoError(t, err) + }) +} + +// TestMockRuntimeEvents tests the Events mock +func TestMockRuntimeEvents(t *testing.T) { + mock := &MockRuntime{} + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + t.Run("returns error when not implemented", func(t *testing.T) { + eventChan, errChan := mock.Events(ctx) + assert.Nil(t, eventChan) + + err := <-errChan + assert.Equal(t, ErrMockNotImplemented, err) + }) + + t.Run("streams events when function set", func(t *testing.T) { + mock.EventsFunc = func(ctx context.Context) (<-chan Event, <-chan error) { + eventChan := make(chan Event, 2) + errChan := make(chan error, 1) + + go func() { + defer close(eventChan) + defer close(errChan) + + eventChan <- Event{ + Type: "container", + Action: "start", + Actor: EventActor{ + ID: "abc123", + Attributes: map[string]string{"name": "test-container"}, + }, + Time: time.Now().Unix(), + } + }() + + return eventChan, errChan + } + + eventChan, errChan := mock.Events(ctx) + + select { + case event := <-eventChan: + assert.Equal(t, "container", event.Type) + assert.Equal(t, "start", event.Action) + case err := <-errChan: + t.Fatalf("unexpected error: %v", err) + case <-time.After(time.Second): + t.Fatal("timeout waiting for event") + } + }) +} + +// TestMockRuntimeLifecycle tests lifecycle methods +func TestMockRuntimeLifecycle(t *testing.T) { + mock := &MockRuntime{} + + t.Run("Close returns nil by default", func(t *testing.T) { + err := mock.Close() + assert.NoError(t, err) + }) + + t.Run("Close returns custom error when set", func(t *testing.T) { + customErr := errors.New("close failed") + mock.CloseFunc = func() error { + return customErr + } + + err := mock.Close() + assert.Equal(t, customErr, err) + }) + + t.Run("Mode returns mock by default", func(t *testing.T) { + mock2 := &MockRuntime{} + assert.Equal(t, "mock", mock2.Mode()) + }) + + t.Run("Mode returns custom value when set", func(t *testing.T) { + mock.ModeFunc = func() string { + return "socket" + } + + assert.Equal(t, "socket", mock.Mode()) + }) +} + +// TestMockRuntimeCallTracking tests the call tracking functionality +func TestMockRuntimeCallTracking(t *testing.T) { + mock := &MockRuntime{} + ctx := context.Background() + + // Set up functions that don't error + mock.ListContainersFunc = func(ctx context.Context) ([]ContainerSummary, error) { + return nil, nil + } + mock.InspectContainerFunc = func(ctx context.Context, id string) (*ContainerDetails, error) { + return nil, nil + } + + // Make calls + _, _ = mock.ListContainers(ctx) + _, _ = mock.ListContainers(ctx) + _, _ = mock.InspectContainer(ctx, "container1") + _, _ = mock.InspectContainer(ctx, "container2") + _ = mock.Close() + + // Verify call counts + assert.Equal(t, 2, mock.CallCount("ListContainers")) + assert.Equal(t, 2, mock.CallCount("InspectContainer")) + assert.Equal(t, 1, mock.CallCount("Close")) + assert.Equal(t, 0, mock.CallCount("StartContainer")) + + // Verify WasCalled + assert.True(t, mock.WasCalled("ListContainers")) + assert.True(t, mock.WasCalled("InspectContainer")) + assert.False(t, mock.WasCalled("StartContainer")) + + // Verify call arguments + assert.Len(t, mock.Calls, 5) + assert.Equal(t, "InspectContainer", mock.Calls[2].Method) + assert.Equal(t, "container1", mock.Calls[2].Args[0]) + assert.Equal(t, "container2", mock.Calls[3].Args[0]) + + // Test Reset + mock.Reset() + assert.Len(t, mock.Calls, 0) + assert.False(t, mock.WasCalled("ListContainers")) +} + +// TestMockRuntimeWithPodmanCommand tests integration with PodmanCommand +func TestMockRuntimeWithPodmanCommand(t *testing.T) { + mock := &MockRuntime{} + mock.ListContainersFunc = func(ctx context.Context) ([]ContainerSummary, error) { + return []ContainerSummary{ + {ID: "abc123", Names: []string{"/my-container"}, State: "running"}, + }, nil + } + + // Create a dummy PodmanCommand and set the mock runtime + podmanCmd := NewDummyPodmanCommand() + podmanCmd.Runtime = mock + + // Verify the runtime is set + assert.NotNil(t, podmanCmd.Runtime) + assert.Equal(t, "mock", podmanCmd.Runtime.Mode()) + + // Test that operations work through PodmanCommand + containers, err := podmanCmd.Runtime.ListContainers(context.Background()) + assert.NoError(t, err) + assert.Len(t, containers, 1) +} + +// TestContainerRuntimeInterfaceCompleteness ensures all interface methods are tested +func TestContainerRuntimeInterfaceCompleteness(t *testing.T) { + mock := &MockRuntime{} + ctx := context.Background() + + // Container operations + _, _ = mock.ListContainers(ctx) + _, _ = mock.InspectContainer(ctx, "id") + _ = mock.StartContainer(ctx, "id") + _ = mock.StopContainer(ctx, "id", nil) + _ = mock.PauseContainer(ctx, "id") + _ = mock.UnpauseContainer(ctx, "id") + _ = mock.RestartContainer(ctx, "id", nil) + _ = mock.RemoveContainer(ctx, "id", false, false) + _, _, _ = mock.ContainerTop(ctx, "id") + _ = mock.PruneContainers(ctx) + _, _ = mock.ContainerStats(ctx, "id", false) + + // Image operations + _, _ = mock.ListImages(ctx) + _, _ = mock.InspectImage(ctx, "id") + _, _ = mock.ImageHistory(ctx, "id") + _ = mock.RemoveImage(ctx, "id", false) + _ = mock.PruneImages(ctx) + + // Volume operations + _, _ = mock.ListVolumes(ctx) + _ = mock.RemoveVolume(ctx, "name", false) + _ = mock.PruneVolumes(ctx) + + // Network operations + _, _ = mock.ListNetworks(ctx) + _ = mock.RemoveNetwork(ctx, "name") + _ = mock.PruneNetworks(ctx) + + // Events + _, _ = mock.Events(ctx) + + // Lifecycle + _ = mock.Close() + _ = mock.Mode() + + // Verify all methods were called + expectedMethods := []string{ + "ListContainers", "InspectContainer", "StartContainer", "StopContainer", + "PauseContainer", "UnpauseContainer", "RestartContainer", "RemoveContainer", + "ContainerTop", "PruneContainers", "ContainerStats", + "ListImages", "InspectImage", "ImageHistory", "RemoveImage", "PruneImages", + "ListVolumes", "RemoveVolume", "PruneVolumes", + "ListNetworks", "RemoveNetwork", "PruneNetworks", + "Events", "Close", "Mode", + } + + for _, method := range expectedMethods { + assert.True(t, mock.WasCalled(method), "Method %s was not called", method) + } +} diff --git a/pkg/commands/runtime_types_test.go b/pkg/commands/runtime_types_test.go new file mode 100644 index 00000000..ad84bea1 --- /dev/null +++ b/pkg/commands/runtime_types_test.go @@ -0,0 +1,629 @@ +package commands + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// Test ContainerSummary type fields +func TestContainerSummaryFields(t *testing.T) { + now := time.Now() + cs := ContainerSummary{ + ID: "abc123", + Names: []string{"/my-container"}, + Image: "alpine:latest", + ImageID: "sha256:abc123", + Command: `["sleep","infinity"]`, + Created: now.Unix(), + State: "running", + Status: "Up 5 minutes", + Ports: []PortMapping{{IP: "0.0.0.0", PrivatePort: 80, PublicPort: 8080, Type: "tcp"}}, + Labels: map[string]string{"app": "test"}, + SizeRw: 1024, + SizeRootFs: 4096, + Pod: "pod123", + PodName: "my-pod", + } + + assert.Equal(t, "abc123", cs.ID) + assert.Equal(t, []string{"/my-container"}, cs.Names) + assert.Equal(t, "alpine:latest", cs.Image) + assert.Equal(t, "running", cs.State) + assert.Len(t, cs.Ports, 1) + assert.Equal(t, uint16(80), cs.Ports[0].PrivatePort) + assert.Equal(t, uint16(8080), cs.Ports[0].PublicPort) + assert.Equal(t, "pod123", cs.Pod) + assert.Equal(t, "my-pod", cs.PodName) +} + +// Test ContainerDetails with all nested types +func TestContainerDetailsFields(t *testing.T) { + now := time.Now() + cd := ContainerDetails{ + ID: "abc123", + Name: "/my-container", + Created: now, + Path: "/bin/sh", + Args: []string{"-c", "sleep infinity"}, + State: &ContainerState{ + Status: "running", + Running: true, + Paused: false, + Restarting: false, + OOMKilled: false, + Dead: false, + Pid: 12345, + ExitCode: 0, + StartedAt: now, + Health: &HealthState{ + Status: "healthy", + FailingStreak: 0, + Log: []HealthLog{{ + Start: now, + End: now.Add(time.Second), + ExitCode: 0, + Output: "OK", + }}, + }, + }, + Image: "alpine:latest", + ImageID: "sha256:abc123", + Config: &ContainerConfig{ + Hostname: "myhost", + User: "root", + Tty: true, + Env: []string{"PATH=/usr/bin"}, + Cmd: []string{"sleep", "infinity"}, + WorkingDir: "/app", + Entrypoint: []string{"/entrypoint.sh"}, + Labels: map[string]string{"version": "1.0"}, + StopSignal: "SIGTERM", + }, + NetworkSettings: &NetworkSettings{ + Bridge: "bridge0", + Ports: map[string][]PortBinding{ + "80/tcp": {{HostIP: "0.0.0.0", HostPort: "8080"}}, + }, + Networks: map[string]*EndpointSettings{ + "bridge": { + NetworkID: "net123", + IPAddress: "172.17.0.2", + MacAddress: "02:42:ac:11:00:02", + }, + }, + }, + Mounts: []Mount{{ + Type: "volume", + Name: "mydata", + Source: "/var/lib/containers/storage/volumes/mydata/_data", + Destination: "/data", + RW: true, + }}, + } + + assert.Equal(t, "abc123", cd.ID) + assert.Equal(t, "/my-container", cd.Name) + assert.NotNil(t, cd.State) + assert.True(t, cd.State.Running) + assert.NotNil(t, cd.State.Health) + assert.Equal(t, "healthy", cd.State.Health.Status) + assert.NotNil(t, cd.Config) + assert.Equal(t, "myhost", cd.Config.Hostname) + assert.NotNil(t, cd.NetworkSettings) + assert.Len(t, cd.NetworkSettings.Ports, 1) + assert.Len(t, cd.Mounts, 1) +} + +// Test ContainerStatsEntry with all stat types +func TestContainerStatsEntryFields(t *testing.T) { + now := time.Now() + stats := ContainerStatsEntry{ + Read: now, + PreRead: now.Add(-time.Second), + CPUStats: CPUStats{ + CPUUsage: CPUUsage{ + TotalUsage: 1000000000, + PercpuUsage: []int64{500000000, 500000000}, + UsageInKernelmode: 100000000, + UsageInUsermode: 900000000, + }, + SystemCPUUsage: 10000000000, + OnlineCpus: 2, + ThrottlingData: ThrottlingData{ + Periods: 100, + ThrottledPeriods: 5, + ThrottledTime: 50000000, + }, + }, + PreCPUStats: CPUStats{ + CPUUsage: CPUUsage{ + TotalUsage: 900000000, + }, + SystemCPUUsage: 9000000000, + OnlineCpus: 2, + }, + MemoryStats: MemoryStats{ + Usage: 104857600, // 100 MiB + MaxUsage: 209715200, // 200 MiB + Limit: 536870912, // 512 MiB + Stats: MemoryStatsDetails{ + ActiveAnon: 52428800, + Cache: 52428800, + Rss: 52428800, + }, + }, + PidsStats: PidsStats{ + Current: 10, + Limit: 1000, + }, + Networks: map[string]NetworkStats{ + "eth0": { + RxBytes: 1000000, + RxPackets: 1000, + TxBytes: 500000, + TxPackets: 500, + }, + }, + BlkioStats: BlkioStats{ + IoServiceBytesRecursive: []BlkioStatEntry{ + {Major: 8, Minor: 0, Op: "Read", Value: 1048576}, + {Major: 8, Minor: 0, Op: "Write", Value: 524288}, + }, + }, + Name: "my-container", + ID: "abc123", + } + + assert.Equal(t, "my-container", stats.Name) + assert.Equal(t, "abc123", stats.ID) + assert.Equal(t, int64(1000000000), stats.CPUStats.CPUUsage.TotalUsage) + assert.Equal(t, 2, stats.CPUStats.OnlineCpus) + assert.Equal(t, int64(104857600), stats.MemoryStats.Usage) + assert.Equal(t, int64(536870912), stats.MemoryStats.Limit) + assert.Equal(t, 10, stats.PidsStats.Current) + assert.Len(t, stats.Networks, 1) + assert.Equal(t, int64(1000000), stats.Networks["eth0"].RxBytes) + assert.Len(t, stats.BlkioStats.IoServiceBytesRecursive, 2) +} + +// Test ImageSummary fields +func TestImageSummaryFields(t *testing.T) { + now := time.Now() + img := ImageSummary{ + ID: "sha256:abc123", + ParentID: "sha256:parent123", + RepoTags: []string{"alpine:latest", "alpine:3.19"}, + RepoDigests: []string{"alpine@sha256:digest1"}, + Created: now.Unix(), + Size: 5242880, // 5 MiB + SharedSize: 1048576, // 1 MiB + VirtualSize: 6291456, // 6 MiB + Labels: map[string]string{"maintainer": "test@example.com"}, + Containers: 3, + } + + assert.Equal(t, "sha256:abc123", img.ID) + assert.Len(t, img.RepoTags, 2) + assert.Equal(t, int64(5242880), img.Size) + assert.Equal(t, int64(3), img.Containers) +} + +// Test ImageDetails fields +func TestImageDetailsFields(t *testing.T) { + now := time.Now() + img := ImageDetails{ + ID: "sha256:abc123", + RepoTags: []string{"alpine:latest"}, + RepoDigests: []string{"alpine@sha256:digest1"}, + Parent: "sha256:parent123", + Comment: "Test image", + Created: now, + DockerVersion: "20.10.0", + Author: "test@example.com", + Config: &ContainerConfig{ + Cmd: []string{"/bin/sh"}, + Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, + WorkingDir: "/", + }, + Architecture: "amd64", + Os: "linux", + Size: 5242880, + VirtualSize: 5242880, + RootFS: RootFS{ + Type: "layers", + Layers: []string{"sha256:layer1", "sha256:layer2"}, + }, + Metadata: ImageMetadata{ + LastTagTime: now, + }, + } + + assert.Equal(t, "sha256:abc123", img.ID) + assert.Equal(t, "amd64", img.Architecture) + assert.Equal(t, "linux", img.Os) + assert.Len(t, img.RootFS.Layers, 2) + assert.NotNil(t, img.Config) +} + +// Test ImageHistoryEntry fields +func TestImageHistoryEntryFields(t *testing.T) { + now := time.Now() + entry := ImageHistoryEntry{ + ID: "sha256:abc123", + Created: now.Unix(), + CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + Tags: []string{"alpine:latest"}, + Size: 0, + Comment: "", + } + + assert.Equal(t, "sha256:abc123", entry.ID) + assert.Contains(t, entry.CreatedBy, "CMD") +} + +// Test VolumeSummary fields +func TestVolumeSummaryFields(t *testing.T) { + now := time.Now() + vol := VolumeSummary{ + Name: "mydata", + Driver: "local", + Mountpoint: "/var/lib/containers/storage/volumes/mydata/_data", + CreatedAt: now, + Status: map[string]interface{}{"state": "available"}, + Labels: map[string]string{"app": "test"}, + Scope: "local", + Options: map[string]string{"type": "tmpfs"}, + UsageData: &VolumeUsageData{ + Size: 1048576, + RefCount: 2, + }, + } + + assert.Equal(t, "mydata", vol.Name) + assert.Equal(t, "local", vol.Driver) + assert.NotNil(t, vol.UsageData) + assert.Equal(t, int64(1048576), vol.UsageData.Size) + assert.Equal(t, int64(2), vol.UsageData.RefCount) +} + +// Test NetworkSummary fields +func TestNetworkSummaryFields(t *testing.T) { + now := time.Now() + nw := NetworkSummary{ + Name: "my-network", + ID: "net123", + Created: now, + Scope: "local", + Driver: "bridge", + EnableIPv6: false, + IPAM: IPAM{ + Driver: "default", + Config: []IPAMConfig{{ + Subnet: "172.18.0.0/16", + Gateway: "172.18.0.1", + }}, + }, + Internal: false, + Attachable: true, + Ingress: false, + Containers: map[string]EndpointResource{ + "container1": { + Name: "my-container", + EndpointID: "ep123", + MacAddress: "02:42:ac:12:00:02", + IPv4Address: "172.18.0.2/16", + }, + }, + Options: map[string]string{"com.docker.network.bridge.name": "br-net123"}, + Labels: map[string]string{"env": "test"}, + } + + assert.Equal(t, "my-network", nw.Name) + assert.Equal(t, "bridge", nw.Driver) + assert.Len(t, nw.IPAM.Config, 1) + assert.Equal(t, "172.18.0.0/16", nw.IPAM.Config[0].Subnet) + assert.Len(t, nw.Containers, 1) + assert.Equal(t, "my-container", nw.Containers["container1"].Name) +} + +// Test Event fields +func TestEventFields(t *testing.T) { + now := time.Now() + event := Event{ + Type: "container", + Action: "start", + Actor: EventActor{ + ID: "abc123", + Attributes: map[string]string{ + "name": "my-container", + "image": "alpine:latest", + }, + }, + Time: now.Unix(), + } + + assert.Equal(t, "container", event.Type) + assert.Equal(t, "start", event.Action) + assert.Equal(t, "abc123", event.Actor.ID) + assert.Equal(t, "my-container", event.Actor.Attributes["name"]) +} + +// Test PortMapping fields +func TestPortMappingFields(t *testing.T) { + pm := PortMapping{ + IP: "0.0.0.0", + PrivatePort: 80, + PublicPort: 8080, + Type: "tcp", + } + + assert.Equal(t, "0.0.0.0", pm.IP) + assert.Equal(t, uint16(80), pm.PrivatePort) + assert.Equal(t, uint16(8080), pm.PublicPort) + assert.Equal(t, "tcp", pm.Type) +} + +// Test PortBinding fields +func TestPortBindingFields(t *testing.T) { + pb := PortBinding{ + HostIP: "0.0.0.0", + HostPort: "8080", + } + + assert.Equal(t, "0.0.0.0", pb.HostIP) + assert.Equal(t, "8080", pb.HostPort) +} + +// Test EndpointSettings fields +func TestEndpointSettingsFields(t *testing.T) { + es := EndpointSettings{ + IPAMConfig: &EndpointIPAMConfig{ + IPv4Address: "172.17.0.5", + IPv6Address: "2001:db8::5", + LinkLocalIPs: []string{"169.254.0.1"}, + }, + Links: []string{"container1:alias1"}, + Aliases: []string{"web", "frontend"}, + NetworkID: "net123", + EndpointID: "ep123", + Gateway: "172.17.0.1", + IPAddress: "172.17.0.5", + IPPrefixLen: 16, + IPv6Gateway: "2001:db8::1", + GlobalIPv6Address: "2001:db8::5", + GlobalIPv6PrefixLen: 64, + MacAddress: "02:42:ac:11:00:05", + DriverOpts: map[string]string{"opt1": "value1"}, + } + + assert.NotNil(t, es.IPAMConfig) + assert.Equal(t, "172.17.0.5", es.IPAddress) + assert.Equal(t, 16, es.IPPrefixLen) + assert.Equal(t, "02:42:ac:11:00:05", es.MacAddress) +} + +// Test Mount fields +func TestMountFields(t *testing.T) { + m := Mount{ + Type: "volume", + Name: "mydata", + Source: "/var/lib/containers/storage/volumes/mydata/_data", + Destination: "/data", + Driver: "local", + Mode: "rw", + RW: true, + Propagation: "rprivate", + } + + assert.Equal(t, "volume", m.Type) + assert.Equal(t, "mydata", m.Name) + assert.True(t, m.RW) +} + +// Test empty/nil handling +func TestEmptyContainerSummary(t *testing.T) { + cs := ContainerSummary{} + assert.Empty(t, cs.ID) + assert.Nil(t, cs.Names) + assert.Nil(t, cs.Ports) + assert.Nil(t, cs.Labels) +} + +func TestNilContainerState(t *testing.T) { + cd := ContainerDetails{ + ID: "abc123", + State: nil, + } + assert.Nil(t, cd.State) +} + +func TestNilHealthState(t *testing.T) { + state := ContainerState{ + Status: "running", + Health: nil, + } + assert.Nil(t, state.Health) +} + +func TestEmptyNetworkStats(t *testing.T) { + stats := ContainerStatsEntry{ + Networks: make(map[string]NetworkStats), + } + assert.Empty(t, stats.Networks) +} + +func TestEmptyBlkioStats(t *testing.T) { + stats := ContainerStatsEntry{ + BlkioStats: BlkioStats{ + IoServiceBytesRecursive: nil, + }, + } + assert.Nil(t, stats.BlkioStats.IoServiceBytesRecursive) +} + +func TestNilVolumeUsageData(t *testing.T) { + vol := VolumeSummary{ + Name: "mydata", + UsageData: nil, + } + assert.Nil(t, vol.UsageData) +} + +func TestEmptyIPAMConfig(t *testing.T) { + nw := NetworkSummary{ + IPAM: IPAM{ + Config: []IPAMConfig{}, + }, + } + assert.Empty(t, nw.IPAM.Config) +} + +// Test edge cases for numeric types +func TestZeroValues(t *testing.T) { + stats := ContainerStatsEntry{ + CPUStats: CPUStats{ + CPUUsage: CPUUsage{ + TotalUsage: 0, + }, + SystemCPUUsage: 0, + OnlineCpus: 0, + }, + MemoryStats: MemoryStats{ + Usage: 0, + Limit: 0, + }, + } + assert.Equal(t, int64(0), stats.CPUStats.CPUUsage.TotalUsage) + assert.Equal(t, int64(0), stats.MemoryStats.Usage) +} + +// Test maximum values for int64 fields +func TestMaxInt64Values(t *testing.T) { + const maxInt64 = int64(9223372036854775807) + stats := ContainerStatsEntry{ + MemoryStats: MemoryStats{ + Limit: maxInt64, + }, + } + assert.Equal(t, maxInt64, stats.MemoryStats.Limit) +} + +// Test ContainerConfig ExposedPorts and Volumes map types +func TestContainerConfigMaps(t *testing.T) { + cfg := ContainerConfig{ + ExposedPorts: map[string]struct{}{ + "80/tcp": {}, + "443/tcp": {}, + }, + Volumes: map[string]struct{}{ + "/data": {}, + "/cache": {}, + }, + } + + _, exists := cfg.ExposedPorts["80/tcp"] + assert.True(t, exists) + _, exists = cfg.Volumes["/data"] + assert.True(t, exists) +} + +// Test TopResponse type +func TestTopResponseFields(t *testing.T) { + top := TopResponse{ + Titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"}, + Processes: [][]string{{"root", "1", "0", "0", "10:00", "?", "00:00:01", "/bin/sh"}}, + } + + assert.Len(t, top.Titles, 8) + assert.Equal(t, "PID", top.Titles[1]) + assert.Len(t, top.Processes, 1) + assert.Equal(t, "1", top.Processes[0][1]) +} + +// Test BlkioStatEntry fields +func TestBlkioStatEntryFields(t *testing.T) { + entry := BlkioStatEntry{ + Major: 8, + Minor: 0, + Op: "Read", + Value: 1048576, + } + + assert.Equal(t, int64(8), entry.Major) + assert.Equal(t, int64(0), entry.Minor) + assert.Equal(t, "Read", entry.Op) + assert.Equal(t, int64(1048576), entry.Value) +} + +// Test MemoryStatsDetails all fields +func TestMemoryStatsDetailsFields(t *testing.T) { + details := MemoryStatsDetails{ + ActiveAnon: 1024, + ActiveFile: 2048, + Cache: 4096, + Dirty: 512, + HierarchicalMemoryLimit: 536870912, + HierarchicalMemswLimit: 1073741824, + InactiveAnon: 256, + InactiveFile: 1024, + MappedFile: 2048, + Pgfault: 1000, + Pgmajfault: 10, + Pgpgin: 500, + Pgpgout: 400, + Rss: 52428800, + RssHuge: 0, + TotalActiveAnon: 1024, + TotalActiveFile: 2048, + TotalCache: 4096, + TotalDirty: 512, + TotalInactiveAnon: 256, + TotalInactiveFile: 1024, + TotalMappedFile: 2048, + TotalPgfault: 1000, + TotalPgmajfault: 10, + TotalPgpgin: 500, + TotalPgpgout: 400, + TotalRss: 52428800, + TotalRssHuge: 0, + TotalUnevictable: 0, + TotalWriteback: 0, + Unevictable: 0, + Writeback: 0, + } + + assert.Equal(t, int64(4096), details.Cache) + assert.Equal(t, int64(52428800), details.Rss) + assert.Equal(t, int64(1000), details.Pgfault) +} + +// Test ThrottlingData fields +func TestThrottlingDataFields(t *testing.T) { + data := ThrottlingData{ + Periods: 100, + ThrottledPeriods: 5, + ThrottledTime: 50000000, + } + + assert.Equal(t, 100, data.Periods) + assert.Equal(t, 5, data.ThrottledPeriods) + assert.Equal(t, int64(50000000), data.ThrottledTime) +} + +// Test RootFS and ImageMetadata +func TestRootFSAndMetadata(t *testing.T) { + now := time.Now() + rootFS := RootFS{ + Type: "layers", + Layers: []string{"sha256:abc", "sha256:def", "sha256:ghi"}, + } + metadata := ImageMetadata{ + LastTagTime: now, + } + + assert.Equal(t, "layers", rootFS.Type) + assert.Len(t, rootFS.Layers, 3) + assert.Equal(t, now, metadata.LastTagTime) +} diff --git a/scripts/integration-test.sh b/scripts/integration-test.sh new file mode 100755 index 00000000..2a7d973a --- /dev/null +++ b/scripts/integration-test.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -e + +echo "=== lazypodman Integration Tests ===" + +# Check if Podman is available +if ! command -v podman &> /dev/null; then + echo "Error: Podman is not installed" + exit 1 +fi + +# Check if Podman is running (socket available) +if ! podman info &> /dev/null; then + echo "Error: Podman is not running." + echo "" + echo "To start Podman:" + echo " Linux (rootless): systemctl --user start podman.socket" + echo " Linux (rootful): sudo systemctl start podman.socket" + echo " macOS/Windows: podman machine start" + exit 1 +fi + +echo "Podman is available and running." +echo "" + +# Display Podman version +echo "Podman version:" +podman --version +echo "" + +# Display socket info +echo "Podman socket info:" +if [ -n "$CONTAINER_HOST" ]; then + echo " CONTAINER_HOST: $CONTAINER_HOST" +elif [ -n "$DOCKER_HOST" ]; then + echo " DOCKER_HOST: $DOCKER_HOST" +else + echo " Using default socket path" +fi +echo "" + +# Run integration tests +echo "Running integration tests..." +echo "" + +export GOFLAGS=-mod=vendor + +# Run integration tests with verbose output +go test -tags=integration -v ./pkg/commands/... + +echo "" +echo "=== Integration tests completed ===" diff --git a/test/podman-compose.yml b/test/podman-compose.yml new file mode 100644 index 00000000..9ea6a3c1 --- /dev/null +++ b/test/podman-compose.yml @@ -0,0 +1,25 @@ +version: "3" +services: + test-service: + build: + dockerfile: Dockerfile + context: . + command: /app/print-random-stuff.sh + depends_on: + - test-service2 + ports: + - "8080:80" + + test-service2: + build: + dockerfile: Dockerfile + context: . + command: /app/print-random-stuff.sh + ports: + - "8081:81" + + test-service3: + build: + dockerfile: Dockerfile + context: . + command: /app/print-random-stuff.sh