shimtest

package module
v0.0.0-...-7c48d3a Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 1, 2026 License: Apache-2.0 Imports: 49 Imported by: 0

README

shimtest

A conformance test suite for containerd shim implementations. Tests the task lifecycle (create, start, exec, kill, delete), stdio round-trip, clock synchronization across a VM boundary, the transfer service, and UDS socket forwarding.

Prerequisites

  • A built shim binary (e.g., containerd-shim-runc-v2 or containerd-shim-nerdbox-v1)
  • Go toolchain (for compiling the test binary and cmd/testbin)

No containerd daemon is required. The test rootfs is built in-process from an embedded Go binary (cmd/testbin) compressed as _output/testbin.gz and written into an erofs image at test time.

Building

make build

This builds cmd/testbin (a small multicall binary embedded in the test rootfs) and compiles the test binary to _output/shimtest.test. The testbin is always cross-compiled for linux/amd64 via CGO_ENABLED=0; override with TESTBIN_GOOS/TESTBIN_GOARCH or drop a pre-built _output/testbin.gz in place before make build.

Configuration

Tests are driven by one or more JSON configuration files. See shimtest.config.sample.json for all options. The key fields are:

Field Type Description
shim_binary string Name or path of the shim binary to test (required)
uid int UID to run as; defaults to the current user's UID. If set to a value different from the current UID and the effective UID is 0, the harness re-execs itself as that user via sudo
gid int GID to run as
format_mounts bool Provide the rootfs as formatted erofs/ext4 images with a format/mkdir/overlay descriptor for the shim to mount. Default (false) extracts the rootfs and provides a pre-mounted overlay (or plain directory when rootless)
skip []string Feature names to skip (exec, oom, transfer, uds)
env map Additional environment variables for the test run
debug bool Enable debug logging on the shim

Running

shimtest runs in one of two modes.

Single config
_output/shimtest.test -test.v -shimtest.config=profiles/myconfig.json
Config directory

All *.json files in the directory are loaded; each becomes a subtest named after the file:

_output/shimtest.test -test.v -shimtest.configdir=profiles/

Configs with a uid that doesn't match the current process are skipped, or re-exec'd via sudo when the effective UID is 0. The parent serializes the full config into a temp file and passes it to the child so no fields are lost across the re-exec.

Examples
# Run a single test case in a single config
_output/shimtest.test -test.v -test.run='TestShim/myconfig/Lifecycle' \
  -shimtest.config=profiles/myconfig.json

# Run all configs in profiles/
sudo _output/shimtest.test -test.v -shimtest.configdir=profiles/

# Run StartupPhases benchmark across all configs (benchtime=3x is quick)
_output/shimtest.test -test.run='^$' \
  -test.bench='BenchmarkShim/[^/]+/StartupPhases' -test.benchtime=3x \
  -shimtest.configdir=profiles/

Tests

All tests are subtests of the single entry point TestShim. Within each config, the tree is TestShim/<config-name>/<test-name>.

Test Feature Description
Lifecycle Full create/start/kill/wait/delete cycle
Exec exec Exec a process inside a running container
StdioRoundTrip exec Write to stdin, read from stdout via exec
Clock exec Verify VM clock is synchronized with host
ExitCodes exec Exec processes that exit with a range of status codes and verify propagation
InitExitCodes Run the container's init process with /bin/exit N and verify task-level exit status propagation
OutputThenExit Run a process that prints 50 lines over 50ms then exits non-zero; verify both exit status and every line of output
Events Bind a TTRPC events recorder at TTRPC_ADDRESS and verify the shim publishes create, start, exit, delete events with correct fields
LargeFileRead exec Read a 64 MiB fixture from a secondary read-only erofs layer, verify crc32-Castagnoli, report MiB/s
BindMountRead exec Bind-mount the same 64 MiB fixture from a host tempfile and verify+benchmark via the bind path
OOM oom Run a memory hog under a 128MiB limit and verify the kernel OOM-kills it (exit 137)
TransferCopyTo transfer Copy a file into a container
TransferCopyToAndFrom transfer Copy a file in and back out
TransferExecVerify transfer Copy a file in, verify via exec
UDSRoundTrip uds UDS socket forwarding round-trip
Stress (per feature) Long-running concurrent stress run. Composes subtests from the enabled features (currently transfer: stat/write/read). Each subtest runs as a goroutine until the test deadline approaches or any one fails (which cancels the rest). Skipped under -test.short.

A separate top-level fuzz target exists alongside TestShim:

Fuzz target Feature Description
FuzzTransferMissing transfer Fuzzes the transfer service's not-found path. Without -fuzz only the seed corpus runs (sub-second); with -fuzz=FuzzTransferMissing it generates new inputs continuously until -fuzztime elapses or a failing input is found.
Planned tests

Candidates to add later, ranked roughly by value:

  • Signals — send SIGTERM (not SIGKILL) to init, verify exit 143. Most shims get KILL right but botch non-KILL forwarding.
  • Pause/Resume — pause a ticker process, verify output stops; resume, verify it continues.
  • Stats — call tc.Stats() and assert cgroup counters populate (probe since not all shims implement it).
  • Missing executable — set Args to a nonexistent path and verify a clean error (not a hang or panic).
  • Cold-cache IOLargeFileRead/BindMountRead currently measure warm-cache throughput. A cold-cache variant would need to drop caches between runs (root only) or grow the fixture beyond cache size.
  • Double-kill / post-exit API — Kill after exit; Wait/State after Delete. Idempotency.
  • Zombie reaping — init that forks and exits; verify the shim's pid 1 reaps the orphan.

Benchmarks

A subset of these benchmarks runs against runc-rootless and nerdbox on every push to main and is published as time-series charts at https://dmcgowan.github.io/shimtest/dev/bench/ (gh-pages).

Benchmarks live under BenchmarkShim/<config-name>/<bench-name>.

Benchmark Feature Description
Lifecycle Full container create/start/kill/wait/delete cycle
Startup Shim start through first output
StartupPhases Same as Startup with per-phase breakdown reported via custom metrics (ms/shim-start, ms/connect, ms/create, ms/task-start, ms/output, ms/total)
Start Shim start subcommand only (bootstrap, no container)
Exec exec Exec cycle inside a running container (exec/start/wait/delete)
StdioRoundTrip exec Stdio write/read at 8B, 4KB, 4MB
UDSRoundTrip uds UDS forwarded-socket throughput in both directions (HostToContainer, ContainerToHost) at 8B, 4KB, 4MB

Using shimtest in your shim's CI

To run shimtest as part of your own shim project's CI, your job needs to: build your shim, check out and build shimtest, write a JSON profile that points at your binary, then run shimtest.test against it.

Minimal workflow snippet
- name: Build my shim
  run: |
    make my-shim   # produces ./bin/containerd-shim-myshim-v1
    echo "$(pwd)/bin" >> $GITHUB_PATH

- name: Allow unprivileged user namespaces
  # Only needed if your profile uses a non-zero uid on Ubuntu 24.04+.
  run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0

- name: Check out shimtest
  uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
  with:
    repository: dmcgowan/shimtest
    ref: <commit-sha>   # pin a commit
    path: shimtest

- name: Setup Go
  uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
  with:
    go-version: "1.26.x"

- name: Build shimtest
  working-directory: shimtest
  run: make build

- name: Write shimtest profile
  run: |
    cat > shimtest/myshim.json <<'EOF'
    {
      "shim_binary": "containerd-shim-myshim-v1",
      "skip": ["transfer", "uds"]
    }
    EOF

- name: Run shimtest
  working-directory: shimtest
  run: |
    _output/shimtest.test -test.v -test.short -test.timeout=300s \
      -shimtest.config=myshim.json

The -test.short flag opts out of the long-running Stress test; the fuzz target's seed corpus runs either way (it's fast). Drop -test.short from a separate nightly/soak job to exercise the unbounded Stress run, and run active fuzzing as its own step:

- name: Run shimtest soak (nightly)
  working-directory: shimtest
  run: |
    _output/shimtest.test -test.v -test.timeout=15m \
      -test.run='TestShim/myshim/Stress' \
      -shimtest.config=myshim.json
    # Active fuzzing in a separate step:
    _output/shimtest.test -test.run='^$' \
      -test.fuzz=FuzzTransferMissing -test.fuzztime=10m \
      -test.fuzzcachedir=$RUNNER_TEMP/fuzzcache \
      -shimtest.config=myshim.json
What you'll need to configure
  • shim_binary: bare name (resolved via PATH) or absolute path. shimtest also adds the binary's directory to PATH so sibling helpers (kernels, libraries) co-located with the shim resolve.
  • format_mounts: set true if your shim mounts the rootfs itself from format/mkdir/overlay descriptors (VM-based shims); leave false to receive a pre-mounted overlay or plain directory.
  • uid: omit to run as the runner user. Set explicitly when you want the harness to sudo re-exec itself or rewrite the profile.
  • skip: list of feature names to disable. Currently meaningful values are exec, oom, transfer, and uds — useful when your shim doesn't implement transfer/UDS forwarding or when running rootless without cgroup delegation.
Multiple configs

If you want one job to test several profiles (e.g., rootless and root variants of the same shim) put the JSON files under a directory and pass -shimtest.configdir= instead of -shimtest.config=. Each file becomes a TestShim/<filename>/... subtest. Profiles whose uid differs from the running process are skipped, or sudo-re-exec'd when the harness is running as root.

Benchmarks

The benchmark binary is the same as the test binary — just point -test.bench at the suite and disable tests:

- name: Run benchmarks
  working-directory: shimtest
  run: |
    _output/shimtest.test -test.run='^$' \
      -test.bench='BenchmarkShim/myshim/(Lifecycle|Startup|Exec)' \
      -test.benchtime=5x \
      -shimtest.config=myshim.json | tee bench.txt

Documentation

Overview

Package shimtest is a conformance test suite for containerd shim implementations. The tests are organized into Suites — RunSuite, ExecSuite, TransferSuite, UDSSuite, OOMSuite — each gated against one feature key. Callers construct a Config and call the suite's Run method (or Bench / Fuzz) from their own test functions:

func TestMyShim(t *testing.T) {
    cfg := shimtest.Config{ShimBinary: "containerd-shim-myshim-v1"}
    shimtest.NewRunSuite(cfg).Run(t)
    shimtest.NewExecSuite(cfg).Run(t)
}

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// ShimBinary is the name (resolved via PATH) or absolute path of
	// the shim binary under test.
	ShimBinary string

	// FormatMounts, when true, provides the rootfs to the shim as
	// formatted erofs/ext4 images plus a format/mkdir/overlay mount
	// descriptor — appropriate for VM-based shims that mount the
	// rootfs themselves. When false the rootfs is extracted (or
	// overlay-mounted on the host) and provided pre-mounted.
	FormatMounts bool

	// Env is a set of environment variables propagated to processes
	// spawned by the harness (the shim binary, runc, etc.). The local
	// JSON-driven runner applies this via os.Setenv before any test
	// runs; library callers can pre-populate the process environment
	// directly and leave this empty.
	Env map[string]string

	// Debug enables verbose logging from the shim.
	Debug bool
}

Config carries everything a suite needs to bring up a shim. Callers identify their shim and tune setup behavior via this struct; suites hold a copy and use it for every test they run.

Config has no Skip field — choosing which suites to run is the caller's concern (skip a feature by not constructing the corresponding suite).

type ExecSuite

type ExecSuite struct {
	// contains filtered or unexported fields
}

ExecSuite contains tests gated on the "exec" feature: running processes inside a container, stdio round-trip, the in-VM clock, exec exit-code propagation, and file-IO benchmarks driven via hashverify.

func NewExecSuite

func NewExecSuite(cfg Config) *ExecSuite

NewExecSuite constructs an ExecSuite from the given options.

func (*ExecSuite) Bench

func (s *ExecSuite) Bench(b *testing.B)

Bench runs every benchmark in the ExecSuite as a sub-benchmark of b.

func (*ExecSuite) Run

func (s *ExecSuite) Run(t *testing.T)

Run runs every test in the suite as a subtest of t.

type OOMSuite

type OOMSuite struct {
	// contains filtered or unexported fields
}

OOMSuite contains the OOM-killer test, gated on the "oom" feature (some shim configurations don't expose memory cgroup controls or can't reliably trigger the kernel OOM killer).

func NewOOMSuite

func NewOOMSuite(cfg Config) *OOMSuite

NewOOMSuite constructs an OOMSuite from the given options.

func (*OOMSuite) Run

func (s *OOMSuite) Run(t *testing.T)

Run runs every test in the suite as a subtest of t.

type RunSuite

type RunSuite struct {
	// contains filtered or unexported fields
}

RunSuite contains the always-run conformance tests: full container lifecycle, init exit-code propagation, output-then-exit, and event-stream verification. None of these tests are gated by a feature key — every shim should pass them.

func NewRunSuite

func NewRunSuite(cfg Config) *RunSuite

NewRunSuite constructs a RunSuite from the given options.

func (*RunSuite) Bench

func (s *RunSuite) Bench(b *testing.B)

Bench runs every benchmark in the RunSuite as a sub-benchmark of b. Each benchmark times one phase of a container's lifecycle so regressions can be localized.

func (*RunSuite) Run

func (s *RunSuite) Run(t *testing.T)

Run runs every test in the suite as a subtest of t.

type StressOptions

type StressOptions struct {
	// Transfer enables the bidirectional transfer-service stress
	// test. The shim under test must implement the transfer service.
	Transfer bool
}

StressOptions tunes which subtests StressSuite.Run executes.

type StressSuite

type StressSuite struct {
	// contains filtered or unexported fields
}

StressSuite contains long-running stress tests that exercise the shim under sustained load. Each subtest runs for a configurable duration (bounded by t.Deadline() - stressSoakBuffer); the whole suite is skipped under -short.

The suite verifies, on completion, that no shim processes leaked and (where applicable) that long-running shims didn't grow memory unboundedly.

func NewStressSuite

func NewStressSuite(cfg Config, options StressOptions) *StressSuite

NewStressSuite constructs a StressSuite from cfg and options.

func (*StressSuite) Run

func (s *StressSuite) Run(t *testing.T)

Run runs every configured stress test as a subtest of t. Skipped under -short. Registers a leak check that fires after all subtests (and their cleanups) complete.

type TransferSuite

type TransferSuite struct {
	// contains filtered or unexported fields
}

TransferSuite contains tests gated on the "transfer" feature: the transfer service's copy-in/out operations, the bidirectional-stream stress test, and a Fuzz target that callers can wire into a top-level FuzzXxx in their own _test.go.

func NewTransferSuite

func NewTransferSuite(cfg Config) *TransferSuite

NewTransferSuite constructs a TransferSuite from the given options.

func (*TransferSuite) Fuzz

func (s *TransferSuite) Fuzz(f *testing.F)

Fuzz exposes the missing-file fuzz body for use by a top-level FuzzXxx in any package. The caller's FuzzTransferMissing should look like:

func FuzzTransferMissing(f *testing.F) {
    shimtest.NewTransferSuite(opts).Fuzz(f)
}

Each fuzz iteration synthesizes a path under a never-created base directory and asserts the transfer service returns an application-level error (not a timeout).

func (*TransferSuite) Run

func (s *TransferSuite) Run(t *testing.T)

Run runs every test in the suite as a subtest of t. Subtest names are kept stable (TransferCopyTo, TransferCopyToAndFrom, TransferExecVerify) so existing -test.run filters and CI workflow patterns keep matching. The transfer stress test moved to StressSuite; construct one of those with StressOptions{Transfer: true} to run it.

type UDSSuite

type UDSSuite struct {
	// contains filtered or unexported fields
}

UDSSuite contains the UDS-mount tests, gated on the "uds" feature.

func NewUDSSuite

func NewUDSSuite(cfg Config) *UDSSuite

NewUDSSuite constructs a UDSSuite from the given options.

func (*UDSSuite) Bench

func (s *UDSSuite) Bench(b *testing.B)

Bench runs every benchmark in the UDSSuite as a sub-benchmark of b.

func (*UDSSuite) Run

func (s *UDSSuite) Run(t *testing.T)

Run runs every test in the suite as a subtest of t. The subtest name is kept as UDSRoundTrip to match historical -test.run filters.

Directories

Path Synopsis
cmd
testbin command
Testbin is a minimal multicall binary for use inside shimtest containers.
Testbin is a minimal multicall binary for use inside shimtest containers.
internal
transfer
Package transfer provides client-side transfer types that marshal into the containerd transfer API protobuf messages.
Package transfer provides client-side transfer types that marshal into the containerd transfer API protobuf messages.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL