Phase5 and Phase6 completed

This commit is contained in:
christophe-duc 2026-01-07 14:09:14 -04:00
parent e728be9821
commit a1924cf1e6
9 changed files with 2749 additions and 0 deletions

View file

@ -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:

View file

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

View file

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

533
pkg/commands/podman_test.go Normal file
View file

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

View file

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

View file

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

View file

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

52
scripts/integration-test.sh Executable file
View file

@ -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 ==="

25
test/podman-compose.yml Normal file
View file

@ -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