smokepod
Smoke test runner for CLI applications. You write .test files describing commands and expected output, then smokepod executes them in Docker containers or against local targets and reports pass/fail.
Overview
- Language: Go
- Repo: peteretelej/smokepod
- Install:
npx smokepod --helporgo install github.com/peteretelej/smokepod/cmd/smokepod@latest - Status: active
Architecture
smokepod has a clean three-layer structure: CLI commands, a public library package, and internal parsing.
Package layout:
cmd/smokepod/main.go- CLI entry point using urfave/cli, defines four commands: run, validate, record, verifypkg/smokepod/- public Go library (importable by other projects)smokepod.go- top-level API:Run(),RunWithOptions(),RunFile(),ValidateConfig()config.go- YAML config parsing (Config,Settings,TestDefinition), validation, defaultsexecutor.go- test orchestrator with parallel/sequential execution, fail-fast, functional options patterntarget.go-Targetinterface (Exec + Close)target_local.go-LocalTargetruns commands via local shelltarget_docker.go-DockerTargetruns commands inside a testcontainers-go containertarget_process.go-ProcessTargetcommunicates via JSONL stdin/stdout with a long-running processdocker.go- container lifecycle management using testcontainers-gocompare.go- output comparison with regex and stderr supportdiff.go- unified diff generation for failure reportingfixture.go- JSON fixture read/write for record/verify workflowdiscovery.go- finds.testfiles by walking directoriesreporter.go- JSON result reporting for run modeverify_reporter.go- human-readable verify output with pass/fail/xfail/xpassresult.go- result types (Result,TestResult,SectionResult,CommandResult)platform.go- platform detection for recorded fixtureslaunch.go- test launching utilitiesrunners/- test type runnerscli.go- CLI test runner: parses .test sections, executes commands, compares outputplaywright.go+playwright_types.go- Playwright test runner for browser tests in Dockertypes.go- shared runner types
internal/testfile/parser.go-.testfile parser: sections, commands, expected output, regex/stderr suffixes, exit codes, metadata directives, xfailinternal/whitespace/- whitespace normalization for output comparisonnpm/- npm package wrapper with platform-specific native binaries
Three execution modes:
- run - reads a
smokepod.yamlconfig, spins up Docker containers via testcontainers-go, executes.testfile commands inside them, compares output against inline expectations - record - runs commands against a reference target (e.g.
/bin/bash), captures stdout/stderr/exit code, saves to JSON fixture files - verify - runs commands against a different target, compares output against previously recorded fixtures, reports diffs
Data flow for run mode:
- Parse
smokepod.yamlintoConfig Executor.Execute()iterates test definitions (parallel or sequential)- For each CLI test: parse the
.testfile, create aTarget(Docker or local), instantiate aCLIRunner - Runner iterates sections and commands, calls
target.Exec(), compares output usingcompare.go - Aggregate results into
Resultstruct, report as JSON
The .test file format:
Sections start with ## name, commands with $ command, and expected output as plain text below. Suffixes modify matching: (re) for regex, (stderr) for stderr matching, (stderr,re) for both. Exit codes are asserted with [exit:N]. Sections can be marked (xfail) for expected failures. Metadata directives (# target:, # mode:) appear before the first section.
Key Design Decisions
Custom .test format instead of YAML/JSON tests. The format is designed to look like a terminal session, making tests readable without understanding any framework. Section headers, dollar-sign commands, and plain-text expectations are intuitive. The parser handles edge cases (multi-line commands, regex suffixes, exit codes) without making the common case complex.
Docker via testcontainers-go. Rather than requiring a pre-running service, smokepod manages container lifecycle itself. Tests declare an image in the YAML config, and testcontainers-go handles pulling, starting, executing, and tearing down. This means tests are fully self-contained and reproducible.
Dual distribution: Go binary + npm wrapper. The npm package (npm/) includes platform-specific native binaries so JavaScript projects can use npx smokepod without a Go toolchain. The npm/bin/run.js launcher detects the platform and executes the correct binary. This lowers the adoption barrier for non-Go projects.
Record/verify separation from run. The three modes serve different use cases. run is for “does my app behave as documented in the .test file?” while record/verify is for “does my replacement tool produce identical output to the original?” Keeping them separate avoids conflating inline expectations with fixture-based comparison.
Target interface abstraction. The Target interface (Exec(ctx, cmd) -> ExecResult) cleanly separates command execution from test logic. LocalTarget, DockerTarget, and ProcessTarget all implement it, so the same test runner works against local binaries, Docker containers, or long-running processes that speak JSONL.
xfail sections for known failures. Sections marked ## name (xfail) are expected to fail. If they pass unexpectedly (xpass), that is treated as a failure. This supports incremental compatibility work where you track known gaps without breaking CI.
Development
go build -o smokepod ./cmd/smokepod
go test ./...
# Run smoke tests against a Docker image
./smokepod run smokepod.yaml
# Record fixtures from a reference target
./smokepod record --target /bin/bash --tests tests/ --fixtures fixtures/
# Verify a new target against recorded fixtures
./smokepod verify --target ./my-tool --tests tests/ --fixtures fixtures/