- tests/acme_event_loop.rs: five integration tests drive the crate's
real ACME path and assert the spawned loop's logged events through a
hand-rolled tracing test subscriber: AccountCacheStore (eager, pre-
network), the Order warn via a plain-HTTP fake directory stub on an
ephemeral 127.0.0.1 port, DeployedCachedCert from a pre-seeded
PKCS#8-key-first PEM chain, CertCacheLoad/AccountCacheLoad errors +
AccountCacheStore warn from an ENOTDIR-poisoned cache path, and
CachedCertParse from a corrupt cached PEM. Event collection is
timeout-bounded, never JoinHandle-bounded (rustls-acme 0.12.1's
stream never terminates — verified against the vendored sources).
- src/server.rs: delete the unreachable debug!("ACME: state machine
ended") line (work item 2: it cannot execute under the
never-terminating stream).
- DirCache's deterministic cache file names (SHA256 over element+NUL*
+ directory URL, base64url-nopad) are recomputed in-test via the
sha2 dependency + a new base64 dev-dependency.
- Explicitly not-covered (work item 4): DeployedNewCert, CertCacheStore
ok+warn, NewCertParse — reachable only through a successful ACME
order (a full fake CA), out of scope at this effort level.
Verification: cargo llvm-cov --all-features line coverage 99.21%
(from 98.26%), server.rs 96.08% -> 98.85% lines / 100% functions;
cargo test (93 passed), --all-features (123 passed), --features acme;
clippy (default + all-features, -D warnings); fmt; doc. The event-loop
suite is stable across five consecutive runs.
13 KiB
id, name, status, depends_on, scope, risk, impact, level, tags
| id | name | status | depends_on | scope | risk | impact | level | tags | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| acme-event-loop-test | ACME event-loop coverage — fake-directory integration test (U-1) | completed |
|
moderate | medium | component | implementation |
|
Description
The ACME event-loop body (src/server.rs:94-136) is unreachable by
tests: 23 uncovered lines — every EventOk/EventError arm, the
debug/warn/error mapping. The acme_lifecycle tests construct
the config and assert spawn + ALPN + resolver wiring; the spawned task
runs against a blackhole URL and its events are never observed. A
refactor that drops or mislevels an arm (e.g. EventError::Order warn
→ error) lands green.
Correction to this task (2026-09-12, verified against the vendored
rustls-acme 0.12.1 sources) — the review's §U-1 termination premise is
wrong, and so was this task's original work item 3. rustls-acme 0.12.1's Stream for AcmeState never terminates:
poll_next is Poll::Ready(Some(ready!(self.poll_next_infinite(cx))))
(state.rs:407-412) — it can never yield None. Order errors enter an
exponential backoff (Timer::after(1 << backoff_cnt), capped at 2^16
seconds — state.rs:371-375) and retry forever. Two consequences:
- There is no "loop ends when the stream ends" — the
while let Some(event) = state.next().awaitloop innew_acmenever exits on its own. Do NOT assert that theJoinHandleresolves; such a test hangs until timeout. The original item 3 is removed. src/server.rs:135("ACME: state machine ended") is unreachable dead code under this dependency. It cannot be covered by any test through the real stream. Either delete the line in this task (and note the removal) or explicitly accept it as uncovered — do not leave an implementer to burn hours rediscovering that it is unreachable. (The review's §U-1 framing "if upstream changes the stream's termination semantics, nothing notices" described a termination semantics that does not exist.)
The acme feature's only runtime surface is this loop; it deserves one real integration test.
Work
- Drive a
DirCache-backedAcmeStateagainst a local fake directory: a stub HTTP server (tokio,std::net::TcpListeneron an ephemeral port — no new deps) that serves a directory JSON with no usable endpoints, forcing the error path through the real event stream. (Mechanically verified: rustls-acme's HTTP layer,async_web_client0.6.3, is plainasync_net::TcpStreamforhttp://URLs and self-drives via async-io 2's reactor thread — it works under a tokio runtime, and a plain-HTTP localhost stub is reachable.) - Assert the log events fire. Two shapes, pick one (or both):
tracingtest subscriber capturing thewarn!/error!events (tracing-subscriber with a test layer — add as dev-dependency only), or- extract the event-mapping match into a helper fn taking the event, returning (level, message-class), and test the helper directly (no subscriber needed; cheaper, but the loop body itself stays uncovered — prefer the subscriber shape if the dev dep is acceptable).
- Bound event collection by time, never by stream end: collect
events under
tokio::time::timeout(or a bounded channel + a drop-with-timeout teardown) and assert on what arrived. The spawned task never resolves itsJoinHandle— see the correction above — so any "await the handle" shape hangs. Note: with#[tokio::test](current-thread runtime) the spawned loop runs on the test thread, so a thread-local default tracing subscriber is visible to it; keep the test single-threaded or the subscriber captures nothing. - Which arms are reachable, per rustls-acme 0.12.1's state machine
(state.rs
poll_next_infinite/order):EventError::Order(warn) — fires against the erroring stub.EventOk::AccountCacheStore— the first event, generated before any network I/O (the account key is generated and stored eagerly, state.rs:377-395); fires even against a dead address.EventError::AccountCacheLoad/CertCacheLoad/CachedCertParse(error) andEventOk::DeployedCachedCert/CertCacheStore(ok) — reached by pre-seeding theDirCachedirectory. The cache file names are deterministic (caches/dir.rs:38-53):cached_account_{base64url(SHA256( contact-els…directory_url))}andcached_cert_{base64url( SHA256(domain-els…directory_url))}. A corrupt pre-seeded file gives the Load/Parse error arms; a valid PEM (an ECDSA-P256 chain, PKCS#8 key first —parse_certwants key-then-cert-chain, state.rs:192-214) gives the deployed/stored ok arms. The PEM can be generated with rcgen in-test (rcgen is already a dependency on the acme feature path via the server's self-signed helper — it is a [dependencies] member).EventOk::DeployedNewCert/EventError::NewCertParse— reachable only through a successful order, i.e. a full fake ACME CA (newNonce/newAccount/newOrder/authorizations/finalize/ certificate endpoints). Out of scope at this effort level; mark explicitly not-covered rather than silent.
- Keep
TlsErrorout of it: per ADR-002/ADR-006, ACME runtime errors are stream events, not error variants — the test asserts events, never aTlsError.
Verification
- server.rs 94-136 covered under
cargo llvm-cov --all-features(line 135 either deleted or explicitly accepted as uncovered, per work item 2's second consequence) - The test performs no real network I/O (binds localhost only)
cargo test --features acme,--all-featuresgreen; default build unaffected- clippy/fmt/doc green
Acceptance Criteria
- Every reachable arm per work item 4 is executed by a test
(
Orderwarn,AccountCacheStore, the Load/Parse error arms,DeployedCachedCert/CertCacheStore);DeployedNewCert/NewCertParseare explicitly marked not-covered (full fake CA needed — out of scope) - No test awaits the event loop's
JoinHandle(it never resolves) - The ACME feature's runtime surface is no longer refactor-fragile
References
- docs/reviews/001-implementation-review.md §U-1, Part C (§U-1's termination premise is corrected — see this task's Description)
- src/server.rs:64-145 (
new_acme+ the spawned loop) - tests/acme_lifecycle.rs (the existing construction-level tests)
- ADR-006 (acme feature layout), ADR-002 (
TlsErrorscope boundary) - rustls-acme 0.12.1 sources (vendored):
state.rs:407-412(the never-endingStreamimpl),state.rs:371-375(order-error backoff + retry),state.rs:192-214(parse_cert— key first, then ≥2 PEMs),caches/dir.rs:38-53(the deterministic cache file names)
Notes
Agent fills this during implementation.
Work notes (2026-09-12, verified against the vendored rustls-acme 0.12.1 sources before implementation):
- Scope correction to work item 4 (verified, mechanically):
EventOk::CertCacheStore/EventError::CertCacheStoreare not pre-seed-reachable.DirCache::store_certis invoked only fromprocess_cert's new-cert branch (state.rs:219-227early_action = store_cert); a pre-seeded cache hit takes thecached = truebranch, which returnsDeployedCachedCertbefore any store. SoDeployedNewCert/CertCacheStore(ok+warn) /NewCertParseare all new-cert-path arms — the same full-fake-CA class the task already marks out of scope. The acceptance criterion's "…DeployedCachedCert/CertCacheStore" is satisfied as:CertCacheStoreis covered for the warn arm (below), its ok arm belongs to the not-covered set. - Arm coverage achieved (each via the real event stream, asserted
through a tracing test subscriber — work item 2's preferred shape,
hand-rolled on
tracingitself, no tracing-subscriber dev-dep):EventOk::AccountCacheStore(debug) — dead-directory test (http://127.0.0.1:9); the account key is generated and stored eagerly before any network I/O (state.rs:377-395). The test also verifies the file exists under the recomputed deterministic name.EventError::Order(warn) — plain-HTTP fake directory stub (std::net::TcpListener, ephemeral 127.0.0.1 port) serving{"newNonce": "/"}— discover fails to deserialize (the required camelCase endpoints are missing), the order future errors, the loop logs the warn. Verified the level is WARN, not ERROR.EventError::CertCacheLoad+AccountCacheLoad(errors) +EventError::AccountCacheStore(warn) —cache_dirpoisoned as a regular file: DirCache reads ofcache_dir/<name>fail with ENOTDIR (an error, not NotFound-miss) andcreate_dir_allin the store path fails — three arms in one scenario.EventError::CachedCertParse(error) — a real cache file under the recomputed name holding a corrupt PEM (load succeeds, parse fails).EventOk::DeployedCachedCert(debug) — a pre-seeded valid PEM (rcgen ECDSA-P256 self-signed, PKCS#8 key PEM first, then cert —parse_cert's required order). Zero network I/O.
- Work item 2, second consequence: the dead line was deleted.
debug!("ACME: state machine ended")(former server.rs:135) is unreachable under rustls-acme 0.12.1's never-terminating stream; removing it also removed the dead-code trap. Note llvm-cov still reports the closure's closing});(now server.rs:156) uncovered — structurally uncoverable for the same reason (the spawned closure never returns); same accepted class. - Remaining uncovered in the loop (all new-cert-path, explicitly
not-covered per work item 4):
DeployedNewCert(125),CertCacheStoreok (128),CertCacheStorewarn (139-140),NewCertParse(151-152) — reachable only through a successful order (a full fake ACME CA: newNonce/newAccount/newOrder/ authorizations/finalize/certificate). Documented here, not silent. - Mechanical notes for future maintainers: the fake directory is
served by a dedicated OS thread (one accept, then done); the
client side is rustls-acme's own
async_web_client(plainasync_net::TcpStreamforhttp://), which self-drives via async-io's reactor thread — it works under#[tokio::test]'s current-thread runtime without any tokio networking features. The subscriber is installed withtracing::subscriber::set_default(thread-local default), visible to the spawned loop because#[tokio::test]polls the task on the test thread.Timer::afteron the huge renewal wait is safe: async-io 2.6 mapsInstant::checked_addoverflow toTimer::never(no panic). - Timing: each scenario asserts on captured events after a
tokio::time::timeout-bounded poll loop (10s cap, events arrive in ~ms); the suite is stable across repeated runs.
Summary
Agent fills this on completion.
Closed the ACME event-loop coverage gap (review 001 §U-1) with
tests/acme_event_loop.rs — five integration tests driving the
crate's real ACME path (TlsServerConfig::new on an Acme identity)
and asserting the loop's logged events through a hand-rolled tracing
test subscriber:
event_loop_logs_account_store_against_a_dead_directory—EventOk::AccountCacheStorefires before any network I/O; also verifies the deterministicDirCachefile name on disk.event_loop_logs_order_warn_against_a_directory_with_no_usable_endpoints— a plain-HTTP stub on an ephemeral 127.0.0.1 port serves an unparseable directory document; theEventError::Orderwarn fires through the real event stream (real HTTP, real deserialize failure).event_loop_deploys_a_valid_pre_seeded_cached_cert— a valid pre-seeded PEM chain (PKCS#8 key first) drivesEventOk::DeployedCachedCertwith zero network I/O.event_loop_logs_cert_cache_load_errors_when_the_cache_dir_is_a_file—CertCacheLoad+AccountCacheLoaderrors and theAccountCacheStorewarn from one ENOTDIR-poisoned cache path.event_loop_logs_cached_cert_parse_error_on_a_corrupt_cert_cache— a corrupt cached PEM surfacesCachedCertParsewith the underlying error attached.
Event collection is bounded by wall-clock timeout, never by the
spawned task's JoinHandle (rustls-acme 0.12.1's stream never
terminates). No test awaits the handle. The unreachable
debug!("ACME: state machine ended") line was deleted per work item 2.
Only non-test source change: that dead-line deletion. Dev-deps: added
base64 (for the in-test recomputation of DirCache's deterministic
file names).
Verification: cargo llvm-cov --all-features line coverage 99.21%
crate-wide (from 98.26%); server.rs 96.08% → 98.85% lines /
100.00% functions. The 7 remaining uncovered lines in server.rs are
exactly the four new-cert-path arms (125, 128, 139-140, 151-152 —
need a full fake ACME CA, out of scope per work item 4) plus the
closure's closing }); (156 — structurally uncoverable, the spawned
task never returns). cargo test (93 passed), cargo test --all-features (123 passed), cargo test --features acme, clippy
(default + all-features, -D warnings), cargo fmt --check,
cargo doc --no-deps — all green. The event-loop suite is stable
across five consecutive runs.