Outcall
Operator guides

Testing

Testing

Outcall has three layers of automated tests:

LayerWhereWhen you run it
Unit and binary testssrc/**/*.rs, outcall/src/main_tests.rs, and crate-local test modulesevery cargo test
Integration testsoutcall*/tests/*.rs (portable plus Linux/privileged suites)every cargo test; privileged cases are explicitly ignored
End-to-end harnessMakefile + scripts/e2e/tests/*.sh (Docker-based)make test / make test-e2e

This guide walks each layer in order. The first two are what most people mean by "tests"; the third is a cross-binary smoke harness used to confirm the whole stack lights up on a fresh Docker host.

Unit tests (cargo test)

Run the whole workspace's tests from application/:

cd application
cargo test --workspace --all-targets --locked

The portable macOS all-target run currently executes 184 non-ignored tests. Linux executes additional daemon modules and target-gated tests. The suite covers API serialization, CLI parsing and first-run behavior, rules, policy editing, recipes, host-resource boundaries, the local UI bridge, and daemon subsystems.

Most non-ignored tests need no privileges. Some portable tests create loopback TCP or Unix sockets. Linux-only tests that require network administration are marked #[ignore] and run in the privileged CI jobs.

Running a subset

cargo test -p outcalld                     # just the daemon
cargo test -p outcalld rules::             # just the rule engine module
cargo test -p outcalld sni_empty           # one named test
cargo test -- --nocapture                  # show println!/dbg! output
cargo test -- --test-threads=1             # serial execution (handy when state leaks)

Async tests

Anything that needs the Tokio runtime uses #[tokio::test]:

#[tokio::test]
async fn reload_picks_up_new_rules() { … }

You don't have to set up a runtime yourself.

Integration tests (cargo test --test ...)

Integration tests live in outcall/tests/, outcall-api/tests/, and outcalld/tests/. They are separate test binaries and exercise public APIs, CLI behavior, sockets, and real syscalls as appropriate.

outcalld/tests/ currently contains 10 integration test files:

FileWhat it exercisesRequirements
bridge_integration.rsBridge create/destroy, nftables apply/teardownLinux + root
cli_integration.rsCLI subcommands over Unix socketoutcalld running
agent_api_integration.rsAgent shim verdict round-trip via agent.sockLinux
proxy_http_integration.rsHTTP proxy ALLOW/BLOCKoutcalld + bridge up
proxy_https_integration.rsHTTPS CONNECT + SNI-based BLOCKoutcalld + bridge up
proxy_dns_integration.rsDNS filter + proxy interactionoutcalld + bridge up
dynamic_rules_integration.rsDynamic rule insert + flushoutcalld + bridge up
example_rules_validation.rsValidates the shipped example rulesetsNone
intercept_e2e.rsRejects intercept rules without a CA; verifies non-intercept startupLinux + root
mixed_modes_e2e.rsMixed-mode behavior for currently implemented egress modesLinux + root

The bridge test needs Linux and CAP_NET_ADMIN (or root):

sudo cargo test -p outcalld --test bridge_integration -- --nocapture

On macOS, Linux-gated files compile to zero tests. Privileged tests are also ignored during the ordinary Linux run and execute in dedicated CI jobs.

Want to write more? Drop a new .rs file in outcalld/tests/ and cargo test picks it up automatically. See S012 for gaps in coverage.

Continuous integration

application/.github/workflows/ci.yml runs the following job groups on pushes and pull requests to main:

Job groupCommand or scopeWhat fails it
checkcargo check --workspace --all-targetscompilation error
test-unit, test-integrationportable unit/binary and integration suitesany non-ignored test failure
test-privileged-sudo, test-privileged-dockerignored Linux integration suites with required capabilitiesprivileged bridge/proxy/runtime failure
installer-smokelocal release install plus Claude and Codex --version runspackaging or first-run failure
secure-install-runtimelocal install, both recipes, isolation/profile scripts, netfilter fail-closed testruntime bootstrap or security regression
coveragemake coveragetests fail or workspace line coverage drops below 50%
spec-traceabilitymake spec-checkan S000-S015 implementation/test mapping is missing or stale
fmtcargo fmt --all -- --checkformatting drift
clippycargo clippy --workspace --all-targets -- -D warningsany new clippy warning
audit, deny, unsafe-policydependency and first-party safety policyadvisory, license, ban, or unsafe-policy violation

-- -D warnings on clippy is strict: a single new warning is treated as a compilation error. Keep new code lint-clean.

The ordinary jobs do not imply privileged coverage. The sudo and Docker jobs explicitly run ignored tests with the required Linux capabilities.

Code coverage

The Outcall workspace plays well with cargo-llvm-cov, which uses LLVM's source-based coverage to produce per-file line coverage:

cargo install cargo-llvm-cov

cd application

# Run tests, write LCOV, and enforce the current CI floor
make coverage

# Per-file HTML report (open target/llvm-cov/html/index.html)
cargo llvm-cov --workspace --all-targets --locked --html

# Just the daemon, including its integration test
cargo llvm-cov -p outcalld --all-targets --locked

cargo llvm-cov recompiles with -C instrument-coverage then runs the tests; expect a fresh first run to take 1–2 minutes longer than a normal cargo test.

Realistic coverage targets

Outcall is a network daemon — large parts of it are I/O, syscalls, and async glue that is hard to unit-test. Aim for:

CrateTarget line coverageWhy
outcall-api90%+Pure types and constants. Easy.
outcalld/rules/80%+Pure-ish CEL evaluation; should be heavily covered.
outcalld/proxy/ (parsing)85%+The parser functions are pure; the IO loop isn't.
outcalld/proxy/ (handle_*)not unit-test territoryUse integration tests (S011 names a few).
outcalld/network/, outcalld/dns/, outcalld/docker/covered via integrationWire them into tests/*.rs rather than mocking everything.

CI uses a 50% workspace line floor as a regression guard and uploads target/coverage/lcov.info. It does not replace the higher subsystem targets in S012 or the requirement for real integration tests at trust boundaries.

End-to-end harness (make test / make test-e2e)

The Makefile at the repo root drives a Docker-based smoke test. This is not unit testing — it's a "does the whole binary actually do the thing on a fresh host" check.

make build          # one-time: build the outcall-daemon Docker image (~2 min)
make start          # creates network, starts outcalld in a container
make test           # runs HTTP / ICMP / DNS smoke tests against an Alpine agent
make test-e2e       # full E2E test suite from scripts/e2e/tests/
make stop           # tear everything down
Make targetWhat it does
make buildBuild outcall-daemon image
make start / make stopDaemon lifecycle in Docker
make statusoutcall bridge status inside the daemon container
make agentInteractive Alpine shell on the outcall network
make logsTail daemon logs
make exec CMD="…"Run any command inside the daemon container
make cleanStop + remove the image

make test-e2e is self-contained: it builds the image if needed and runs the test scripts in scripts/e2e/tests/ with the right capabilities (NET_ADMIN, NET_RAW, SYS_ADMIN, net.ipv4.ip_forward=1).

Specialized suites

Beyond the numbered E2E suite, the Makefile ships focused suites that each build the image if needed and run a dedicated script under scripts/:

Make targetWhat it does
make test-bypassSecurity bypass suite — attempts to escape the egress controls
make test-payloadsPayload attack suite — malformed protocol attacks
make test-tls-interceptTLS interception tests (requires a CA + mode: intercept configured)

Adding an E2E test

Drop a numbered .sh script in scripts/e2e/tests/. Each script gets:

VariableValueDescription
BRIDGEoutcall0Bridge interface name
BRIDGE_IP10.99.0.1Bridge IP
AGENT_NSagent1Network namespace name
AGENT_IP10.99.0.2Agent IP inside the namespace
TARGET_IP(dynamic)Container's eth0 IP (forwarded target)

Exit 0 = pass, non-zero = fail. The existing E2E scripts live in outcall-dev/root/scripts/e2e/tests/.

Where to dig deeper

On this page