1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
//! Shared test helpers for `domi-server` gated integration suites.
//!
//! Phase 2d Tasks 3 (`tools_push_smoke`), 4 (`tools_replay_smoke`), and
//! 5 (`tools_tail_smoke`) all need to:
//!
//! 1. Pick a high random TCP port.
//! 2. Spawn the real `domi-server` binary on that port with isolated
//! `--root` and `--state` tempdirs.
//! 3. Poll `/healthz` until the axum router is up.
//! 4. Run the `domi` binary against that server.
//!
//! These helpers were originally inlined in `tools_push_smoke.rs`
//! (Task 3). When Task 4 added `tools_replay_smoke.rs` the same
//! helpers would have to be duplicated; the right factoring for
//! three integration test files sharing them is `tests/common/mod.rs`
//! — the canonical Rust convention for integration-test helpers
//! (each file in `tests/` is its own crate, so shared code lives in
//! a submodule that is `mod common;`-imported by each file).
//!
//! ## Port allocation — fence approach (Phase 2d Task 4 fix)
//!
//! The previous implementation (`free_high_port()`) bound an
//! ephemeral port, captured the port number, and dropped the
//! listener before returning. That left a race window during which
//! the kernel could re-assign the same port to a concurrent gated
//! test's bind — producing ~20% flakes under `cargo test --workspace
//! -- --ignored`. The robust pattern is to **hold the listener
//! alive** as a fence until the spawned server has demonstrably
//! bound the port. After `wait_for_healthz` returns we know the
//! server has the port, so the fence can be released safely (the
//! kernel will re-bind it to our server, which already owns it).
//!
//! Use [`claim_port`] to get a `(port, fence)` pair. Hold the
//! `fence` in a local until after `wait_for_healthz` succeeds, then
//! `drop(fence)` (or let it go out of scope) before running the rest
//! of the test. The unreachable tests, which never spawn a server,
//! use [`random_unbound_port`] instead — they don't need a fence
//! because they assert "connect fails", and any collision just
//! produces a non-zero exit code.
// not every consumer uses every helper; that's fine
use TcpListener;
use PathBuf;
use Command;
use ;
use ;
/// Claim an ephemeral port and hold a fence on it until the caller
/// drops the returned [`TcpListener`].
///
/// **Caller contract:** keep `fence` alive until AFTER you have
/// confirmed the spawned server has bound the port (typically via
/// [`wait_for_healthz`]). Then `drop(fence)` (or let it go out of
/// scope) to release the reservation.
///
/// The fence guarantees no other concurrent test can bind the same
/// port during the spawn race window — the kernel won't re-assign an
/// ephemeral port while a listener socket is still bound to it.
///
/// Implementation: `bind("127.0.0.1:0")` asks the kernel for an
/// ephemeral port from its pool. Each successful `bind(0)` gets a
/// unique port, so concurrent callers cannot collide. Listener
/// sockets (unlike connection sockets) do not enter `TIME_WAIT` on
/// drop, so the port is immediately safe to re-bind once the
/// spawned server has claimed it.
/// Returns a port number that is HIGHLY LIKELY unbound. Used only
/// by the unreachable tests where the assertion is "connect fails".
///
/// Picks from the kernel's ephemeral range — `[49152, 65535]` on
/// macOS, `[32768, 60999]` on Linux — so it's well outside any
/// service's default port. Does NOT bind-check; the race window is
/// acceptable for unreachable tests because the worst-case
/// behaviour (another test's server happens to be on this port and
/// rejects our request with 4xx/5xx or times out) still produces
/// the expected non-zero exit code.
///
/// Combines `process::id()` and a sub-second nanos seed so two
/// concurrent unreachable tests in the same binary rarely collide.
/// **Deprecated.** Returns a port number without holding a fence.
/// Kept as a thin alias for [`claim_port`] to avoid breaking older
/// call sites that don't actually need the race protection (e.g.,
/// one-off manual experiments). New gated integration tests should
/// use [`claim_port`] + [`wait_for_healthz`] + `drop(fence)`.
/// Wait until the server accepts a TCP connection on `port`, polling
/// every 50ms with a deadline.
pub async
/// Poll `/healthz` until a 2xx is returned (proves the axum router is
/// up, not just the TCP listener).
pub async
/// Spawn the real `domi-server` binary on `port` with isolated `--root`
/// and `--state` directories. `kill_on_drop(true)` ensures cleanup even
/// if the test panics.
/// Run the `domi` binary synchronously with the given args. Returns
/// `(stdout, stderr, exit_code)`. `RUST_BACKTRACE=0` keeps stderr terse.
/// Per-binary atomic port counter (see `next_port`). Seeded by
/// `process_id()` at first use so different test binaries start at
/// different offsets and never collide across binaries.
static NEXT_PORT: AtomicU16 = new;
const PORT_BASE: u16 = 49152;
const PORT_RANGE: u16 = 8192;
/// Allocate the next unique port for this test binary. Each call
/// returns a different port within [PORT_BASE, PORT_BASE+RANGE).
/// Wrapping is harmless (mod RANGE) and never triggers in practice
/// (gated suites have ~10 tests per binary).
/// Boot a fresh server: creates a tempdir, claims a port from the
/// atomic counter, spawns the binary, waits for `/healthz` to return
/// 200. The caller MUST hold the returned `Child` for the duration
/// of the test (its `kill_on_drop(true)` tears down the server).
///
/// **Why this is race-free:** port assignment uses a per-binary
/// atomic counter (see [`next_port`]). Each call returns a port that
/// no other concurrent call in this binary will ever receive. Across
/// binaries (`binary_smoke`, `tools_push_smoke`, `tools_replay_smoke`,
/// `tools_tail_smoke`), the `process_id()` seeding means different
/// binaries start at different offsets in the [49152, 57344) window
/// — no cross-binary collision.
///
/// (An earlier fence approach held a `TcpListener` until the server
/// bound; that BROKE the tests because the spawned server couldn't
/// bind while we held the fence. The atomic counter avoids bind/drop
/// entirely — each call computes a unique port number; if the port is
/// somehow in use, the spawn fails loudly with EADDRINUSE.)
pub async
/// Assert exit code matches `expected` and report the captured output
/// for easier debugging when an assertion fires.
///
/// Imported via `use common::assert_exit;` and called as
/// `assert_exit!(...)`. Marked `#[macro_export]` so it
/// reaches the integration-test crate root — the canonical
/// place from which every test file in `tests/` pulls its
/// shared macros. (Rust 2018+ requires `#[macro_export]` for cross-file visibility.)