Writing Uranium Tests

Traffic Server uses Catch2 for unit tests and pytest for Uranium tests. Catch2 tests reside next to the associated source code. Uranium tests are under tests/uranium_tests/. Direct Proxy Verifier replays and native Python scenarios are both collected and executed by pytest.

This document focuses on Uranium tests because Catch2 documentation is available elsewhere.

Why “Uranium”?

Uranium is enormously useful: as nuclear fuel, it powers homes with reliable, low-carbon electricity. It also demands careful handling. The name is deliberately tongue-in-cheek because these system tests have a similar dual nature: they provide indispensable confidence in a complete Traffic Server deployment, but can be so unstable as to be toxic when process lifecycles, timing, network state, and artifacts are not carefully contained. The name is a reminder to keep every test isolated, deterministic, and safe to run.

File Structure and Naming

The Uranium test runners use distinct filename conventions:

  • Tests are placed in appropriate subdirectories under tests/uranium_tests/ (e.g., cache/, pluginTest/<plugin_name>, tls/, etc.)

  • Replay-driven pytest files have a descriptive .test.yaml extension. The file is both the test registration and the Proxy Verifier replay.

  • Native scenarios use pytest’s test_*.py convention. Prefer plain test functions taking fixtures, with small helper functions for repeated setup. Use this form only when a direct replay cannot express the required client, server, or runtime mutation.

Running Uranium Tests

Configure the build with -DENABLE_URTEST=ON. The urtest build target runs the complete pytest-owned inventory:

cmake --build build --target urtest

Run only the replay suite with:

cmake --build build --target urtest-replay

Pass pytest selection or concurrency options at configure time. For example:

cmake -B build -DENABLE_URTEST=ON -DURTEST_OPTIONS="-n 4 -k cache_control"
cmake --build build --target urtest-replay

Use urtest.sh directly for the shortest single-test workflow:

cmake --build build
cmake --install build
cd build/tests
./urtest.sh -q -k <pytest_expression>

For example, to run items whose names contain header_rewrite:

./urtest.sh -q -k header_rewrite

The -k expression works for both .test.yaml and test_*.py items. It is pytest’s ordinary substring and boolean-expression selector.

To run tests in parallel, pass pytest-xdist’s -n N option. Each test gets a readable, isolated directory directly below the sandbox root and dynamically allocated listeners:

./urtest.sh -n 10 --sandbox /tmp/sbcursor -k "header_rewrite or cache_control"

Without -n, tests run sequentially. Tests listed in tests/serial_tests.txt acquire an exclusive lock and never overlap other Uranium test items.

Successful test sandboxes are removed by default. Pass --keep-sandboxes to retain them for local inspection; failed-test sandboxes are always retained. Each sandbox is <sandbox-root>/<test-name>/, using the full pytest test name and parameter IDs without a generated suffix. The name omits the file path, so test names must be unique across the whole suite, not just within one file. A rerun clears that test’s previous directory first. Collected tests must have distinct sandbox names; name collisions cause a collection error, or fail the colliding tests under -n. Because the check sees only collected tests, a selective run can still reuse the directory of a same-named test in another file from an earlier run. Unix sockets use short paths when needed independently of the artifact directory names.

Manual Tests

Mark privileged, unusually slow, or intentionally diagnostic native tests with @pytest.mark.manual when they should remain available but must not execute as part of the normal suite. For a direct replay test, set urtest.manual to true or a string explaining why it is manual. Pytest reports these tests as skipped by default. Pass --run-manual and select the intended item to run one explicitly:

./urtest.sh --run-manual -k ats_probe

The test should retain its complete implementation and perform ordinary runtime capability checks after it is enabled. For example, a privileged test can still call pytest.skip when bpftrace or the required Linux capability is unavailable.

Container Execution

The source-tree tests/urtest.sh command defaults to the official ci.trafficserver.apache.org/ats/fedora:44 image. It configures an incremental build under build-urtest-container and then runs pytest. The launcher prefers Apple container on macOS, Podman on Linux, and Docker as a fallback. Pass --run-in-container to force this behavior or --no-run-in-container to use the current environment. The older --run-in-docker and --no-run-in-docker spellings remain aliases. A short /tmp sandbox avoids Unix socket path limits; afterward, regular files are copied to build-urtest-container/sandbox on the host.

Direct execution is also a supported workflow for a natively configured build, including on macOS. The generated <build>/tests/urtest.sh records the ATS, Proxy Verifier, build, and sandbox paths, so editor test integrations can invoke it without reconstructing the build configuration. Use --no-run-in-container to select this workflow.

Running uv sync in tests/ creates an in-tree environment that editors can discover. The checked-in [tool.pyright] paths in tests/pyproject.toml make Uranium’s support modules resolvable by Pyright. A local pyrightconfig.json takes precedence over those settings, so it must repeat them if one is present.

The default changes to direct execution when either condition is true:

  • A Docker, Podman, containerd, or Kubernetes marker indicates that the process is containerized.

  • /etc/os-release identifies Fedora 44. This recognizes Apple container, which intentionally does not create Docker’s /.dockerenv marker.

Thus Jenkins, which already launches the Fedora 44 image, does not attempt nested container execution. The launch uses an init process and the SYS_PTRACE capability. Podman and Docker use host networking; Apple container uses its default network because it has no Docker-style host network. The launcher does not require --privileged or mount a host runtime socket.