name: linux-testing
description: >-
Invowk Linux and Ubuntu CI testing knowledge for Go. Covers process lifecycle
(clone/fork+exec, process groups via setpgid, PDEATHSIG), full POSIX signal
handling (SIGKILL/SIGTERM/SIGINT), exec.CommandContext signal delivery and
cmd.Cancel customization, ext4/XFS case-sensitivity, inotify watch limits
(ENOSPC/EMFILE/ENFILE), file descriptor limits, cgroups and namespace
isolation for container tests, OOM killer risks with -race (10x memory),
and the full container test infrastructure (testscript deadlines,
ContainerTestContext, AcquireContainerSemaphore, flock-based cross-binary
serialization). Use when debugging Linux-only failures, exit code 137,
ping_group_range races, context deadline exceeded, [!container-available],
container test hangs, inotify errors, or the container timeout strategy.
Linux Testing Skill
Linux is the full-test platform for this project. All container integration tests run exclusively on Linux. This skill provides deep Linux OS primitive knowledge needed to understand container test infrastructure, debug Linux-only failures, and write correct platform-specific test code.
Normative Precedence
.agents/rules/testing.md-- authoritative test policy (organization, parallelism, container timeout strategy)..agents/skills/go/SKILL.md-- context propagation, code style..agents/skills/go-testing/SKILL.md-- Go testing toolchain knowledge, decision frameworks.- This skill -- Linux OS primitives and the container test infrastructure.
references/container-testing-deep.md-- comprehensive container test infrastructure deep dive.references/process-namespaces.md-- cgroups and namespace isolation for container understanding.references/filesystem-inotify.md-- inotify API, watch limits, error handling..agents/skills/testing/SKILL.md-- invowk-specific test patterns, testscript, TUI/container..agents/skills/testing/SKILL.md§ "Pre-Write Checklist" -- mandatory guardrails before writing any test code.
If this skill conflicts with a rule, follow the rule.
Cross-references:
go-testing-- primary testing entry point; routes Linux symptoms here.windows-testing-- Windows OS primitives (CreateProcess, NTFS, timer resolution).macos-testing-- macOS OS primitives (APFS, kqueue coalescing, timer coalescing).container-- container engine abstraction, Docker/Podman patterns, Linux-only policy.testing-- invowk-specific test patterns including the container timeout layers.
Process Lifecycle
Linux process creation uses the clone syscall, which is a superset of the
traditional fork. Go's runtime uses clone internally for os/exec, not raw
fork, because Go needs fine-grained control over which resources the child
inherits (memory, file descriptors, signal handlers, namespaces).
Process groups and sessions:
- Process group (
setpgid): A leader process and its children share a process group ID (PGID). Signals sent to-pgidreach the entire group. Go setscmd.SysProcAttr.Setpgid = trueto put a child in a new group, enablingsyscall.Kill(-pid, sig)to kill the child and all its descendants. - Session leader (
setsid): Creates a new session with a new process group. The session leader has no controlling terminal. Used by daemons; generally not needed for tests. SIGCHLDand zombie prevention: When a child exits, the kernel sendsSIGCHLDto the parent. Go's runtime automatically reaps child processes (callswaitpid), so zombie processes are not a concern in normal Go code.prctl(PR_SET_PDEATHSIG, SIGKILL): Ensures a child process receives a signal when its parent dies. Go does NOT set this by default, but it can be configured viacmd.SysProcAttr.Pdeathsig = syscall.SIGKILL. Without this, if the parent (e.g., the test binary) is killed by the OOM killer or a timeout, children become orphans reparented to PID 1 (init/systemd).- Orphan processes: If a parent dies without setting
PDEATHSIG, the child is reparented to PID 1. In CI, orphaned container engine subprocesses can accumulate and exhaust resources. The container cleanup patterns (seereferences/container-testing-deep.md) are the defense against this.
Implications for test subprocess cleanup:
Test processes killed by -timeout receive SIGKILL (unblockable). Deferred
cleanup functions do not run. The multi-layer timeout strategy addresses this
by using env.Defer in testscript and t.Cleanup in Go tests, which the test
framework runs before the binary-level timeout fires.
Signal Handling
Linux implements the full POSIX signal set. Key signals and their Go behavior:
| Signal | Number | Blockable | Go Default Behavior |
|---|---|---|---|
SIGKILL |
9 | No | Immediate kill, cannot be caught or ignored |
SIGTERM |
15 | Yes | Catchable; used for graceful shutdown |
SIGINT |
2 | Yes | Go converts to os.Interrupt; Ctrl+C |
SIGPIPE |
13 | Yes | Go ignores by default (broken pipe) |
SIGSTOP |
19 | No | Unblockable pause; SIGCONT resumes |
SIGCONT |
18 | Yes | Resume after SIGSTOP |
SIGQUIT |
3 | Yes | Go prints goroutine stack dump and exits |
SIGUSR1 |
10 | Yes | User-defined; no default Go behavior |
SIGUSR2 |
12 | Yes | User-defined; no default Go behavior |
exec.CommandContext on Linux:
- Default behavior: sends
SIGKILLto the child process when the context is cancelled. This is immediate and unblockable -- no cleanup handlers run in the child. cmd.Cancelcustomization: Override the default kill behavior to sendSIGTERMfirst (graceful), then rely oncmd.WaitDelayto sendSIGKILLif the process does not exit in time:
cmd := exec.CommandContext(ctx, "some-binary")
cmd.Cancel = func() error {
return cmd.Process.Signal(syscall.SIGTERM)
}
cmd.WaitDelay = 10 * time.Second // SIGKILL after 10s if SIGTERM ignored
- Process group kill: To kill a child and all its descendants, use
syscall.Kill(-pid, sig)(negative PID sends to the entire group). Setcmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}to put the child in its own process group first.
Why this matters for tests:
Container engine subprocesses (Docker/Podman) may spawn additional child
processes (conmon, crun, runc). Killing only the top-level process may leave
these orphaned. The cmd.WaitDelay = 10 * time.Second on container
subprocesses in internal/container/engine_base.go ensures pipe cleanup
even when child processes linger after the parent is killed.
File System
Linux filesystems have distinct characteristics that affect test behavior:
- Case-sensitive: ext4, XFS, btrfs, and tmpfs are all case-sensitive.
Foo.cueandfoo.cueare different files. This is the opposite of Windows (NTFS, case-insensitive) and macOS (APFS, case-preserving but insensitive). Tests that pass on Linux may fail on macOS/Windows due to case mismatches. - Inode-based: Hard links share inodes.
os.SameFile()compares inodes, not paths. Rename operations (os.Rename) are atomic on the same filesystem. - Symlinks:
os.Symlinkworks without privileges (unlike Windows, which requiresSeCreateSymbolicLinkPrivilege). Tests creating symlinks need no special handling on Linux. /procpseudo-filesystem: Per-process info at/proc/self/status(memory usage, threads), system limits at/proc/sys/fs/(inotify watches, file-max). Thego-testingskill's race detector memory analysis references/proc./dev/shm: Shared memory backed by tmpfs (RAM-based, fast). Available on all modern Linux distros./tmp: Usually tmpfs on modern distros (Fedora, Ubuntu 22.04+), meaning it is RAM-backed, fast, and auto-cleared on reboot.t.TempDir()creates directories under/tmpby default.
File Locking
Linux provides two main file locking mechanisms. Both are advisory -- they do not prevent other processes from reading or writing; both sides must cooperate by acquiring the lock before accessing the shared resource.
flock: Advisory lock on an open file description (NOT file descriptor). Duplicate file descriptors fromfork/duprefer to the same open file description, so they refer to the same lock.fcntllocks (POSIX record locks): Per-process and not inherited byfork. More granular (byte-range locks), but more complex semantics. Not used by this project.O_EXCLwithO_CREAT: Atomic file creation. Theopencall fails if the file already exists. Useful for lock-file-as-existence patterns.
Project usage of flock:
internal/container/run_lock_linux.go: Podman serialization lock on$XDG_RUNTIME_DIR/invowk-podman.lock. Prevents the rootless Podmanping_group_rangerace between concurrent invowk processes. Falls back toos.TempDir()whenXDG_RUNTIME_DIRis unset.internal/testutil/container_suite_lock_linux.go: Cross-binary test serialization viaAcquireContainerSuiteLock(). Uses flock on$XDG_RUNTIME_DIR/invowk-container-suite.lock(or/tmp/fallback). Only used bytests/clicontainer tests --internal/runtimetests use the in-process semaphore instead.run_lock_linux_test.go: Tests flock contention usingatomic.Boolandtime.Sleep(50ms)to verify that goroutine B blocks while goroutine A holds the lock, plus a serialized-counter test with 5 goroutines.
Key flock behaviors on Linux:
- Blocking:
unix.Flock(fd, unix.LOCK_EX)blocks until the lock is available. - Auto-release: The kernel releases the lock when the fd is closed, including on process crash.
Release()is idempotent: callingLOCK_UN+Close()multiple times is safe (nil-receiver guard pattern used in both production and test code).
inotify
inotify is the Linux-specific file watching API. It is fundamentally different
from macOS's kqueue and Windows's ReadDirectoryChangesW. Full deep dive in
references/filesystem-inotify.md.
Key facts for test code:
- Per-user watch limits:
/proc/sys/fs/inotify/max_user_watchescontrols how many inotify watches a single user can hold. Default varies by distro (typically 8192-524288). Exhaustion causesENOSPC. ENOSPCwhen watches exhausted: The most common inotify-related test failure. CI runners sharing many watchers across concurrent jobs can exhaust the limit. Temporary fix:sysctl -w fs.inotify.max_user_watches=524288.- Recursive watch requires manual directory walking: Unlike macOS's FSEvents
or Windows's
ReadDirectoryChangesW, inotify watches individual directories, not trees. Go'sfsnotifylibrary handles recursive walking internally. - Event coalescing: inotify coalesces consecutive identical events on the same watch descriptor. This is LESS aggressive than kqueue -- individual write events are typically preserved. Tests relying on exact event counts should still use polling/debouncing rather than exact assertions.
inotify_init1flags:IN_NONBLOCK(non-blocking reads) andIN_CLOEXEC(close-on-exec, prevents fd leak to child processes).
The watcher_fatal_unix_test.go test in internal/watch/ validates error
handling for ENOSPC (inotify exhausted), EMFILE (process fd limit), and
ENFILE (system fd limit). Build-tagged //go:build !windows.
File Descriptor Limits
- Soft limit: Historically 1024 on Linux. Go's runtime raises the soft limit to the hard limit at startup (since Go 1.19). Most modern distros set the hard limit to 1048576.
- System-wide limit:
/proc/sys/fs/file-max. Exhaustion causesENFILE. - Per-user limit:
/etc/security/limits.conf(PAM-based). EMFILE: Per-process file descriptor limit reached. Different from inotify limits --EMFILEmeans the process has too many open files overall, not specifically inotify watches.ENFILE: System-wide file table full. Rare in normal CI but possible under extreme load (many parallel test binaries with container operations).
CI impact: Each container operation opens multiple file descriptors (socket to
daemon, pipe to subprocess, lock files). The container semaphore
(INVOWK_TEST_CONTAINER_PARALLEL=2) prevents fd exhaustion by limiting
concurrent container operations.
Container Test Infrastructure
This is the most critical section -- Linux is where all container
integration tests run. The container test infrastructure uses a 5-layer timeout
strategy to prevent indefinite hangs. Full deep dive in
references/container-testing-deep.md.
The 5-Layer Timeout Strategy
| Layer | Mechanism | Scope | Timeout |
|---|---|---|---|
| 1 | testscript.Params.Deadline |
Per CLI txtar test | 3 min |
| 2 | env.Defer cleanup |
Per testscript | cleanup |
| 3 | testutil.ContainerTestContext(t, timeout) |
Per real-container Go test | 5 min |
| 4 | Go -timeout flag |
Per test binary | 10-15 min by target |
| 5 | GitHub timeout-minutes |
Per job | workflow-owned |
Critical rule: EVERY Go container test calling real engine operations such
as Execute() or ExecuteCapture() MUST use ContainerTestContext, not bare
t.Context(). Bare t.Context() has NO deadline — if the daemon hangs, the
subprocess blocks indefinitely.
ctx := testutil.ContainerTestContext(t, testutil.DefaultContainerTestTimeout)
result, err := engine.Execute(ctx, cmd)
Supporting Infrastructure
- Engine health probes:
probeEngineHealthBeforeTest()runs a 10s<engine> versioncheck before each test. Fails fast instead of waiting for 3-min deadline. - Image pre-pull: CI pulls
debian:stable-slimbefore tests — removes network dependency from test timing. - Engine masking:
sudo mv /usr/bin/docker /usr/bin/docker.disabledensuresAutoDetectEngine()picks the right engine per CI matrix entry. - Suite lock vs semaphore:
AcquireContainerSuiteLock()(flock, cross-binary) fortests/clicontainer txtar tests;AcquireContainerSemaphore()/ContainerSemaphore()(in-process channel, cap 2) for Go tests that perform real container operations ininternal/runtime,internal/provision, orinternal/container. Do NOT add suite locks to Go package tests. cmd.WaitDelay = 10s: On container subprocesses (engine_base.go). Prevents indefinite hang when killed processes leave pipes open.
Full deep dive with code examples: references/container-testing-deep.md.
Cgroups and Namespaces
Container tests run inside Linux namespaces and cgroups. Understanding these
is essential for diagnosing container test failures. Full deep dive in
references/process-namespaces.md.
Cgroup v2 unified hierarchy (default on Ubuntu 22.04+, Fedora 31+):
- Single tree hierarchy (unlike v1's multiple trees per controller).
- Resource controllers:
cpu(CFS bandwidth, weight),memory(limit, swap, OOM control),io(bandwidth, weight),pids(fork bomb protection). - Docker and Podman create per-container cgroups for resource isolation.
Linux namespaces (7 types):
- User: UID/GID mapping. Rootless Podman maps host UID 1000 to container UID 0.
- PID: Container sees its own PID space (PID 1 in container is not PID 1 on host).
- Mount: Isolated filesystem view. Overlay filesystem for container layers.
- Network: Virtual interfaces, bridge (Docker) or slirp4netns (rootless Podman).
- UTS: Isolated hostname.
- IPC: Isolated System V IPC and POSIX message queues.
- Cgroup: Isolates cgroup view (container sees itself as root of cgroup tree).
How namespaces affect tests:
- PID 1 signal handling: In containers, PID 1 does not receive signals that
have their default action as "terminate" unless the process installs signal
handlers. This is why
docker stopsends SIGTERM, waits a grace period, then SIGKILL. - Network isolation: Container processes cannot reach host services unless
explicit port mapping (
-p host:container) or host networking is configured. - Filesystem isolation: Only explicitly mounted paths are visible inside the container. The invowk binary is auto-provisioned via ephemeral layer.
Rootless Podman specifics:
- Uses user namespaces without real root:
subuid/subgidmapping in/etc/subuidand/etc/subgid. - The
ping_group_rangesysctl race: concurrent rootless Podman invocations can race on writing to/proc/sys/net/ipv4/ping_group_range. The project addresses this with (1)CONTAINERS_CONF_OVERRIDEdisablingdefault_sysctlsviapodman_sysctl_linux.go, and (2) flock serialization viarun_lock_linux.go.
OOM Killer
The Linux OOM (Out Of Memory) killer can terminate test processes under memory pressure. This is particularly relevant for container tests with the race detector.
Why tests are vulnerable:
- Race detector: The
-raceflag increases memory usage by approximately 10x. Each goroutine's memory overhead grows from ~8KB to ~80KB+. - CI runners:
ubuntu-latestGitHub Actions runners have ~7 GB RAM. Multiple parallel container tests with-racecan exceed this. - Container cgroup limits: OOM can come from the host cgroup limit, not just the container's own limit.
Symptoms:
- Test process killed with no output, exit code 137 (128 + SIGKILL = 9).
dmesg | grep -i oomorjournalctl -k | grep -i oomshows the kill.- All tests in the binary show
(unknown)status (binary killed mid-run).
Mitigation:
- Limit parallel container tests (the semaphore:
INVOWK_TEST_CONTAINER_PARALLEL=2). - Avoid
-raceon memory-intensive benchmarks (make pgo-profileuses-pgo=off, not-race). - If a test binary is OOM-killed in CI, check the CI logs for
exit code 137and correlate with the memory pressure of concurrent jobs.
Seccomp
Docker and Podman apply seccomp (Secure Computing Mode) profiles that restrict which syscalls container processes can make.
- Docker default: Blocks approximately 44 syscalls including
clonewithCLONE_NEWUSER,mount,ptrace,reboot,keyctl, and others. - Podman default: Similar profile; slightly different based on version.
- Impact on tests: Generally transparent for invowk tests since the invowk binary runs as a normal process inside the container. The blocked syscalls are primarily kernel-level operations that invowk does not use.
- Custom profiles: Can be specified via
--security-opt seccomp=profile.jsonif a test ever needs a blocked syscall (unlikely for this project). - Diagnosis: If a test fails with
EPERMoroperation not permittedinside a container for a syscall that works on the host, seccomp is the likely cause. Checkdocker inspectfor the applied security options.
CI Configuration
Runners and Matrix
Linux CI runs the full test matrix on ubuntu-latest with Docker and Podman:
| Runner | Engine | Mode | Purpose |
|---|---|---|---|
ubuntu-latest |
Docker | full | Rolling forward compatibility |
ubuntu-latest |
Podman | full | Rolling + rootless |
Total: 2 Linux CI jobs, each with timeout-minutes: 30.
Three Test Steps
Full-mode Linux jobs split tests into three separate gotestsum invocations:
- All packages except
tests/cliandinternal/runtime: The bulk of unit and integration tests. Excludes the two packages that need special container handling. internal/runtimeisolated: Container runtime tests with the in-process semaphore. Isolated to prevent interference with CLI tests.- CLI integration tests (
tests/cli/...): Testscript-based container tests withAcquireContainerSuiteLock. UsesINVOWK_TEST_CONTAINER_ENGINEto pin the engine.
Test Runner Configuration
The non-CLI and runtime full-test steps use gotestsum with --rerun-fails --rerun-fails-max-failures 5, -race, and -timeout 15m. The full CLI step is deterministic testscript execution, does not use rerun-fails, and currently uses -timeout 10m. Rerun reports produce ::warning:: annotations when enabled; JUnit XML with flaky_summary: true enables PR flaky test summaries. See go-testing skill for full gotestsum flag reference.
Invowk-Specific Linux Patterns
Build Tags
//go:build linux-- strict Linux-only code. Used for:run_lock_linux.go(flock-based Podman serialization)container_suite_lock_linux.go(flock-based test serialization)podman_sysctl_linux.go(sysctl override via temp file)
//go:build !windows-- Unix code (Linux + macOS). Used for:watcher_fatal_unix_test.go(inotify error handling test)- Tests that use POSIX signals or Unix-specific filesystem behavior
Key Linux-Specific Source Files
| File | Purpose |
|---|---|
internal/container/run_lock_linux.go |
flock on $XDG_RUNTIME_DIR/invowk-podman.lock |
internal/container/run_lock_linux_test.go |
flock contention tests with atomic.Bool |
internal/testutil/container_suite_lock_linux.go |
Cross-binary test serialization |
internal/testutil/container_context.go |
ContainerTestContext with 5-min deadline |
internal/testutil/container_semaphore.go |
ContainerSemaphore (buffered channel, cap 2) |
internal/container/podman_sysctl_linux.go |
Sysctl override temp file for Podman |
internal/container/podman_sysctl_linux_test.go |
Tests for sysctl override logic |
internal/watch/watcher_fatal_unix_test.go |
ENOSPC/EMFILE/ENFILE error handling |
internal/container/engine_base.go |
cmdWaitDelay = 10 * time.Second |
Container Integration Txtar Tests
CLI container tests live in tests/cli/testdata/container_*.txtar. They are
gated by [!container-available] skip and run serially within the
TestContainerCLI suite under AcquireContainerSuiteLock.
Container stderr in exit-code tests: container commands may produce incidental
stderr (shell prompt #). Do NOT add ! stderr . to container error-path
txtar tests.
Common Linux Test Failure Matrix
| Symptom | Likely Cause | Diagnosis | Fix |
|---|---|---|---|
| Exit code 137, no output | OOM killer | dmesg | grep -i oom |
Reduce parallel tests, check -race memory |
ENOSPC from fsnotify |
inotify watch limit | cat /proc/sys/fs/inotify/max_user_watches |
sysctl -w fs.inotify.max_user_watches=524288 |
EMFILE too many open files |
Process fd limit | ulimit -n |
Increase soft limit or reduce concurrency |
| Container test hangs 3+ min | Daemon unresponsive | Check probeEngineHealthBeforeTest output |
Restart engine, check cgroup deadlocks |
flock test flaky |
Timing-sensitive sleep | Check time.Sleep(50ms) margin |
Increase sleep or use channel-based sync |
EPERM in container |
Seccomp blocking syscall | docker inspect --format='{{.HostConfig.SecurityOpt}}' |
Custom seccomp profile (rare) |
Podman race on ping_group_range |
Concurrent rootless Podman | Check sysctl override active | Verify CONTAINERS_CONF_OVERRIDE set |
All tests (unknown) |
Binary killed (timeout or OOM) | Check exit code and dmesg |
Increase -timeout or reduce memory |
| Container cleanup failure | Orphaned containers | docker ps -a | grep invowk-test |
Manual cleanup, check env.Defer |
context deadline exceeded |
ContainerTestContext expired |
Check if 5-min timeout is sufficient | Increase timeout for slow CI runners |
| Test passes locally, fails in CI | Missing image pre-pull | Check if debian:stable-slim cached |
Verify pre-pull step runs before tests |
| Wrong engine selected | Engine masking failed | which docker && which podman |
Verify mask step ran, check INVOWK_TEST_CONTAINER_ENGINE |
Related Skills
| Skill | When to Consult |
|---|---|
go-testing |
Go test execution model, all flags, race detector, vet analyzers, benchmark/fuzz APIs |
windows-testing |
Windows process lifecycle, TerminateProcess, NTFS pitfalls, timer resolution |
macos-testing |
APFS case-insensitivity, kqueue coalescing, timer coalescing, /tmp symlink |
container |
Container engine abstraction, Docker/Podman patterns, Linux-only container runtime policy |
testing |
Invowk-specific test patterns, testscript, TUI testing, container runtime testing |
review-tests |
Test suite audit with 102-item checklist across 8 surfaces |