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
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
//! Host-side exit-code contract tests for
//! `ktstr-jemalloc-alloc-worker`.
//!
//! These tests spawn the alloc-worker binary directly via
//! `Command::new` and assert the exit-code contract spelled out in
//! the worker's module doc. No VM, no probe — pure host-side
//! exercise of the fail-fast branches. Runs in well under a second
//! per test.
//!
//! Lives in its own integration-test file (rather than alongside the
//! VM-based `jemalloc_probe_tests.rs`) because the ktstr early-
//! dispatch ctor at `test_support::dispatch::ktstr_test_early_dispatch`
//! intercepts `--list` / `--exact` in nextest protocol mode when the
//! linked binary contains any real `#[ktstr_test]` entries. That
//! intercept emits only the ktstr variants and hides plain `#[test]`
//! functions. This file carries no `#[ktstr_test]` entries so
//! `KTSTR_TESTS.iter().any(e => e.name != "__unit_test_dummy__")`
//! returns false, the intercept is skipped, and the standard
//! rustc test harness picks up the `#[test]` functions below.
use ;
/// Render a `std::process::ExitStatus` as a human-actionable string
/// for assertion-failure diagnostics.
///
/// The default `Debug` / `{:?}` for `status.code()` collapses every
/// signal-kill to a bare `None`, which strips the single most
/// important fact a failing test needs: whether the worker was
/// terminated by a signal at all and, if so, which one. A reader
/// staring at `got None; stderr: ""` in CI output cannot
/// distinguish SIGSEGV from SIGKILL from a genuinely-missing exit
/// code, and must cross-reference the binary's behavior to decide
/// whether the failure is a crash or an orderly signal-kill.
///
/// This helper produces one of:
/// - `"exit code N"` when `status.code()` is `Some(N)` — the
/// normal setup-failure path documented in the worker's "Exit
/// codes" legend.
/// - `"signal-killed (signal N)"` when `status.code()` is `None`
/// and `ExitStatusExt::signal()` yields `Some(N)` on unix.
/// - `"signal-killed"` when both are `None` (the non-unix fallback
/// / defense-in-depth — unreachable on the Linux test platform
/// but kept so the helper compiles everywhere and never panics).
/// Spawn the alloc-worker with `args` and the env pairs in `envs`,
/// then assert it exited with exactly `expected_code`. Returns the
/// captured `Output` so the caller can chain stderr-substring asserts.
///
/// `reason` is a short fragment (e.g. "bytes=0") that appears in the
/// assertion failure message so a failing test's banner names the
/// branch under test — a bare "got exit code X" without context would
/// force the reader to map the call site back to the branch by hand.
///
/// Centralises the spawn + `output.status.code()` +
/// `format_exit_status` + stderr-lossy render triad that every
/// test in this file otherwise retypes. A retype-drift (e.g.
/// swapping `Some(expected)` for `Ok(expected)`, or dropping the
/// stderr render on failure) would previously hide in one test's
/// boilerplate; centralising forces every caller through the same
/// assertion shape.
///
/// Most callers hand a `&[(&str, &str)]` slice literal for `envs`;
/// tests that set zero env vars pass `&[]`. The `&[(&str, &str)]`
/// signature is load-bearing: a `HashMap` would drop the ordering
/// guarantee that the caller-side comment-beside-env pairing
/// implicitly relies on (`background_thread:false` stability under
/// sibling-test env leakage, etc.).
/// bytes=0 must exit with code 2. The worker's module doc pins
/// `2: bytes == 0`; this test catches any refactor that silently
/// re-routes the zero-size alloc guard to a different code or drops
/// it entirely.
/// Missing positional `<BYTES>` must exit with code 5 (argument
/// parse failure). Covers the argv-absent branch of the
/// `expect() → exit(5)` refactor.
/// Non-numeric `<BYTES>` must exit with code 5. Covers the
/// parse-error branch.
/// Ready-marker write failure must exit with code 4. Uses the
/// [`ktstr::worker_ready::WORKER_READY_MARKER_OVERRIDE_ENV`]
/// test-only env hook to point the write at a path under a
/// non-existent parent directory, which `std::fs::write`'s internal
/// `open(..., O_CREAT)` can't create → ENOENT → exit 4. Bypasses
/// the race-prone alternative of pre-creating a directory at the
/// pid-scoped default path. Passes `1024` as BYTES so the
/// self-check + allocation succeed; the ready-marker write is the
/// first failure the worker hits.
/// `/proc/self/task` thread count != 1 must exit with code 3. The
/// worker's default mode rejects any silent extra thread (background
/// allocator threads, a runtime pulled in by a new dep, etc.) via
/// the single-thread self-check in `main` before the allocation is
/// materialized. Forcing that branch from the host side requires
/// the worker to start with a helper thread already alive at the
/// self-check; the cleanest way without patching the binary is to
/// opt into jemalloc's background-thread worker via
/// `background_thread:true`, which spawns the helper during
/// allocator init (before `main` reads `/proc/self/task`).
///
/// The env var is set under both the generic `MALLOC_CONF` name and
/// the tikv-jemallocator runtime-prefix alias `_RJEM_MALLOC_CONF`.
/// tikv-jemallocator's default build prefixes the symbol table with
/// `_rjem_` (the `unprefixed_malloc_on_supported_platforms` Cargo
/// feature is NOT enabled in this workspace — see `Cargo.toml`'s
/// `tikv-jemallocator = { version = "0.6", features = ["stats"] }`
/// stanza), so the generic `MALLOC_CONF` is not read by the
/// in-process jemalloc copy. Setting both variants keeps the test
/// robust against a future feature flip that unprefixes the symbols.
///
/// # Dependency on jemalloc-init-via-`std::env::args()`
///
/// This test's correctness rests on an implicit invariant in the
/// worker binary's `main`: the FIRST call into any jemalloc-backed
/// code path must occur AFTER the process's environment is
/// readable. In the current worker, that first call is implicit —
/// `std::env::args().skip(1).collect::<Vec<String>>()` at the top
/// of `main` allocates a `Vec<String>`, which goes through
/// `tikv_jemallocator::Jemalloc` (the `#[global_allocator]`) and
/// forces jemalloc to initialize on the spot. The initializer
/// reads `MALLOC_CONF` / `_RJEM_MALLOC_CONF` via
/// `getenv()` / `__environ` exactly once during that first
/// allocation, sees `background_thread:true`, and spawns the
/// helper thread as part of init. By the time the worker reaches
/// the `/proc/self/task` self-check, the helper is live, the
/// thread count is ≥ 2, and the exit-3 branch fires.
///
/// A future refactor that (a) marks the env read as pre-main via
/// a `ctor::ctor` constructor, (b) moves argv parsing into a
/// no-alloc path (e.g. `argv.iter()` on a raw `&[&str]` provided
/// by a shim), or (c) adds an `unsafe extern "C" fn main` that
/// bypasses the Rust runtime's env initialization would BREAK this
/// test in a subtle way: jemalloc would still initialize on some
/// later allocation, but by then the env read could race the
/// `/proc/self/task` scan and produce a flaky exit 3 ↔ exit 0
/// result depending on thread scheduling. If you are the author of
/// such a refactor, update this test to force the first allocation
/// explicitly (e.g. via a `let _ = Vec::<u8>::with_capacity(1)` at
/// the top of `main` under a `// jemalloc-init probe` comment) or
/// switch to a more robust forcing mechanism.
/// Every fail-fast stderr line the worker emits must start with the
/// shared [`WORKER_STDERR_PREFIX`]. Pins the "one source of truth
/// for the worker's stderr prefix" contract: a literal-vs-const
/// drift — someone retypes `"jemalloc-alloc-worker:"` with a typo,
/// or omits it on a new eprintln! — would have this assertion
/// trip on the specific failure path. Drives one failure mode per
/// exit code the binary can produce from the host side (missing
/// argv → 5, bytes=0 → 2, bad marker path → 4) so every stderr-
/// emitting branch is sampled at least once.