mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-25 08:31:03 +00:00
Phase5 and Phase6 completed
This commit is contained in:
parent
e728be9821
commit
a1924cf1e6
9 changed files with 2749 additions and 0 deletions
32
.github/workflows/ci.yml
vendored
32
.github/workflows/ci.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
360
pkg/commands/integration_test.go
Normal file
360
pkg/commands/integration_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
216
pkg/commands/podman_host_test.go
Normal file
216
pkg/commands/podman_host_test.go
Normal 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
533
pkg/commands/podman_test.go
Normal 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)
|
||||
}
|
||||
309
pkg/commands/runtime_mock.go
Normal file
309
pkg/commands/runtime_mock.go
Normal 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)
|
||||
593
pkg/commands/runtime_test.go
Normal file
593
pkg/commands/runtime_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
629
pkg/commands/runtime_types_test.go
Normal file
629
pkg/commands/runtime_types_test.go
Normal 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
52
scripts/integration-test.sh
Executable 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
25
test/podman-compose.yml
Normal 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
|
||||
Loading…
Add table
Reference in a new issue