Why test construction is sealed but not locked¶
What production construction installs¶
rtb_cli::Application::builder().build() does more than assemble an App. It sets
up logging, installs the miette diagnostic hook, installs a panic hook, wires signal
handlers to the shutdown token, and registers commands from BUILTIN_COMMANDS.
Skipping that pipeline in production is a genuine hazard, and the failure is quiet:
without the error hook, diagnostics render as bare Debug output or vanish; without
the signal wiring, Ctrl-C does not propagate cancellation and subsystems keep
running.
So the framework wants construction to be deliberate — obvious in review, obvious
in a Cargo.toml.
What a test actually needs¶
A unit test does not want any of that. It wants an App with a name, a version and
maybe a config, so it can call a command's run and assert on the result. Installing
global hooks from a test is worse than useless: the hooks are process-wide, tests run
in parallel, and whichever test installed first wins.
Hence a second, smaller constructor — and the question of how to stop it leaking into production.
The mechanism, and what it prevents¶
TestAppBuilder::new takes a TestWitness, a zero-sized type that implements a
crate-private Sealed trait. Only rtb-test-support can implement Sealed, so only
a crate that depends on rtb-test-support can produce the witness.
Two things follow, and both are real:
- Depending on the bypass is visible. It is a named line in a
Cargo.toml, not an import buried in a file nobody opens. [dev-dependencies]placement is enforceable by convention. A production binary depending only onrtb-appandrtb-clidoes not compilertb-test-supportat all, and cannot nameTestAppBuilder.
What it does not prevent¶
The seal is a speed bump, and the crate says so rather than overclaiming. Two ways around it remain, both fully supported by the public API:
App::newispub. It takes five arguments and builds a completeApp. There is nothing test-only about it —rtb-test-supportitself calls it — and nothing stops production code doing the same and skipping the wholertb-clipipeline.App::for_testingispub. It is#[doc(hidden)], so it does not appear in the rendered API docs, but it is notcfg(test)-gated and there is no Cargo feature guarding it. Any crate depending onrtb-appcan call it.
Three of App's fields are pub(crate) — config and typed_config_ops as of
0.4.1, trailing_args as of 0.7.0 — so an App can no longer be assembled by struct
literal. That closed part of the gap. The rest of the fields stay public for
call-site ergonomics, and the two public constructors remain.
Where this is heading¶
The direction of travel is that the seal becomes actual access control rather than a
signal: App::for_testing goes away in favour of TestAppBuilder, and the remaining
public fields move behind accessors. Until that happens, treat the seal as what it is
— a clear statement of intent that a reviewer can see, not a boundary the compiler
enforces.
Being explicit about that matters more than the mechanism. A seal described as watertight, that is not, is worse than a speed bump described as a speed bump.