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
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
//! Shared test infrastructure for serializing tests that touch
//! crate-level global mutable state (errflag, paramtab, lex
//! buffers, zcontext stacks, paramtab_hashed_storage, ShellExecutor
//! singletons, etc.).
//!
//! Many ported subsystems intentionally mirror C's file-static
//! globals via `OnceLock<Mutex<…>>` / `AtomicI32`. Tests against
//! those subsystems share the same singletons. Parallel cargo
//! test execution races on read-modify-write patterns even when
//! each individual test cleans up afterwards, because two tests
//! observe each other's mid-test state.
//!
//! Pattern: tests that touch global state call
//! `let _g = crate::test_util::global_state_lock();` at entry.
//! The MutexGuard is held for the test's lifetime, serializing
//! against every other test that does the same — purely-functional
//! tests still run in parallel.
//!
//! Mutex-poisoning is recovered automatically so a single panicking
//! test doesn't break the whole suite.
use ;
/// Acquire the crate-wide test mutex. Tests that touch global
/// state (errflag, paramtab, lex buffers, zcontext, ShellExecutor,
/// etc.) hold this guard for their duration to serialize against
/// other stateful tests.
/// Load the terminal the capability-reading tests measure in, once per test
/// binary.
///
/// `%B` is `tcstr[TCBOLDFACEBEG]`, `echoti bold` reads the terminfo entry and
/// `gettermcap co` reads the termcap one; all three come from `init_term()`,
/// which fills them from the entry `$TERM` names. A test binary runs none of
/// zsh's startup, and a CI runner has no tty and no `TERM` at all — so every
/// capability came back empty there and `%B` expanded to nothing, while a
/// developer machine's own terminal made the same assertions pass. The terminal
/// is a fact about the environment, so the harness states it rather than
/// inheriting whatever the shell that started `cargo test` was using.
///
/// Three names, in order, because a minimal image carries only the older ones:
/// each of `xterm-256color`, `xterm` and `vt100` gives the capabilities these
/// tests pin (measured: the same `\e[1m`, `\e[4m` and `\e[0m` from all three).
/// If none of them loads there is no terminfo database at all, and the tests
/// that read a capability fail on that, which is what a missing database
/// deserves.
/// Detach the test binary from any controlling terminal and point its stdin
/// at `/dev/null`, once per process.
///
/// Unit tests that drive the shell's input layer (`inputline` → `shingetline`
/// → `read(SHIN)`, `SHIN` = 0) read the process's real fd 0. Under a terminal
/// that is the tty, and a test binary in a background process group (a CI
/// runner's job, or `cargo test &`) gets SIGTTIN on that read: the WHOLE
/// process stops, libtest included, and the run goes silent until the job
/// timeout. Reproduced on macOS by running the lib tests in a background group
/// under a pty: stopped after ~130 s with
/// `ported::input::tests::inputline_deterministic_on_idle_state` in `read`
/// (`input.rs:250`). With no tty (the ubuntu runner) the same read sees EOF.
/// Redirecting fd 0 is not enough on its own: other tests reach the terminal
/// through `/dev/tty` (`SHTTY`) and `tcsetattr`, which raise SIGTTOU the same
/// way (the same reproduction then stopped at ~200 s instead). So the harness
/// gives up the controlling terminal — `TIOCNOTTY` from a process that is not
/// the session leader only drops the association — after which no job-control
/// stop can reach the process, `/dev/tty` cannot be opened, and the binary runs
/// in the environment the ubuntu runner already provides. Input and the
/// terminal are facts about the environment, so the harness states them, as
/// `load_test_terminal` does for `TERM`; tests that need a terminal open their
/// own pty, and tests that need data on fd 0 dup a pipe onto it and restore it.
/// Put the process locale back to the one the environment selects, before
/// every stateful test.
///
/// The locale is process-wide C library state, and tests that pin `LC_ALL=C`
/// behaviour (`params.rs`, `computil.rs`, `compcore.rs`, `zle_params.rs`)
/// call `setlocale` and do not all put it back — a test that panics before its
/// restore never does. The next test then runs in `C`: under it a UTF-8 value
/// is sliced and case-mapped per byte, e.g.
/// `paramsubst_zsh_corpus_multibyte_slice_first_three` got
/// `"t\u{83}ã\u{83}\u{89}"` for `${ZMSL[1,3]}` of `ténébreux`, failing on CI and
/// locally in a full run while passing alone. zsh selects its locale once at
/// startup (c:Src/init.c:1208 `setlocale(LC_ALL, "")`); the harness does the
/// same once and restores that choice per test, like the option reset above.
/// Reset the completion-machinery globals a `compadd`-driven completion
/// unit test depends on, so its result reflects the test's own inputs
/// rather than whatever the previous test in the same binary left behind.
///
/// [`global_state_lock`] serialises stateful tests but restores nothing,
/// and the completion subsystem is almost entirely process-wide state:
///
/// * `$PREFIX` / `$SUFFIX` / `$IPREFIX` / `$ISUFFIX`. `addmatches`
/// re-seeds the `compprefix` / `compsuffix` / `compiprefix` /
/// `compisuffix` globals from those parameters whenever `incompfunc`
/// is set (`src/ported/zle/compcore.rs:4334-4347`) and then matches
/// every `CAF_MATCH` candidate against them
/// (`compcore.rs:4360-4373`, `Src/Zle/compcore.c:2253-2300`). A dozen
/// completion tests park a non-empty word there and never clear it
/// (`_absolute_command_paths.rs:168` `"ls"`, `_ldap_filters.rs:267`
/// `"-x"`, `_debbugs_bugnumber.rs:142` `"notabug"`, …); after any of
/// them EVERY candidate a later `compadd` offers fails to match, so
/// `compadd` returns 1 and `_all_labels` / `_wanted` report "no
/// matches" for a tag set that was registered perfectly well.
/// * `comptags[]`, which `bin_comptags` indexes by `locallevel`
/// (`Src/Zle/computil.c:3782` "Array of tag-set infos. Index is the
/// locallevel", ported at `computil.rs:6886`), plus `locallevel`
/// itself — nothing unwinds it when a test panics out of a
/// `doshfunc`-shaped port.
/// * the `zstyle` table, which `_hosts` / `_domains` / `_completers` /
/// `_call_program` all consult for their candidate lists.
/// * the three process-wide `compadd` shadows `_approximate` and
/// `_complete_help` install (`src/ported/zle/complete.rs:975-982`,
/// `:1043-1048`).
///
/// Every half of this is the boot/teardown entry point zsh itself uses,
/// not a bespoke reset: `Src/Zle/complete.c:1788 finish_` zsfree's each
/// `comp*` string global, `Src/Zle/computil.c:5124 setup_` zeroes
/// `comptags[]` and `lasttaglevel`, and `zstyle -d` with no pattern is
/// `zstyletab->emptytable` (`Src/Modules/zutil.c:639-640`).
/// Install one `zstyle` for the duration of a test, the way a user would
/// write it on the command line: `zstyle <context> <style> <value…>`
/// (`Src/Modules/zutil.c:606-616` — the no-flag arm is `setstyle`).
///
/// Completion functions that shell out through `_call_program` take the
/// command line from the `command` style
/// (`Completion/Base/Utility/_call_program:26`, ported at
/// `_call_program.rs:74-101`), so this is the upstream-sanctioned way to
/// give such a port a fixed candidate list instead of whatever the host
/// machine's `ifconfig` / `global` happens to print.
/// Put the shell on a truecolor terminal for the duration of a test, with
/// zsh/nearcolor unloaded; restores `$.term.extensions` on drop.
///
/// `match_colour` (c:Src/prompt.c:1988-1996) emits a `#RRGGBB` colour as
/// 24-bit only when no `get_color_attr` hook answers. zsh/nearcolor is that
/// hook, and with `$.term.extensions` not listing `truecolor` the C code loads
/// it on the spot (c:1990-1992), so the colour is quantized to the 256-colour
/// palette. Both are process-wide state another test may have left behind.
/// Measured with the master oracle:
/// zsh -fc 'print -rP "%F{#f00}x"' → \e[38;5;196m
/// zsh -fc '.term.extensions=(truecolor); print -rP …' → \e[38;2;255;0;0m
/// zsh -fc 'zmodload zsh/nearcolor; .term.extensions=(truecolor); …' → \e[38;5;196m
/// A test pinning the 24-bit branch states both preconditions with this.
/// A live completion scope for a test: what `callcompfunc` opens around a
/// completion function (`Src/Zle/compcore.c:814-818` — `startparamscope();
/// makecompparams(); comp_setunset(…)`). `$PREFIX`, `$words`, `$CURRENT`,
/// the other `comprparams[]` params and the `$compstate` hash exist only
/// inside it, as gsu views of the `complete.c` globals. Dropping the guard
/// ends the scope (`endparamscope()`).
;
/// Open a [`CompScope`] with every `$compstate` key set, as a completion
/// function sees them after `comp_setunset(CP_ALLREALS, 0, CP_ALLKEYS, 0)`.