ktstr 0.6.0

Test harness for Linux process schedulers
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
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
//! `AssertResult::note_value` / `NoteValue` plus the
//! `any_of` / `all_of` short-circuit constructors. The note_value
//! tests pin the From-impl routing per scalar type, the
//! independent-buffer invariant against `details`, the merge
//! union with last-write-wins on key collision, and the wire
//! format's `skip_serializing_if = "is_empty"` softness.

use super::*;

/// Each `From` impl on [`NoteValue`] routes to the matching enum
/// variant. Pin every variant so a regression that swapped two
/// `From` arms (e.g. `i64` mistakenly producing `NoteValue::Uint`)
/// trips here, not at the consumer's run-time mismatch.
#[test]
fn note_value_from_impls_route_to_correct_variant() {
    assert_eq!(NoteValue::from(42i64), NoteValue::Int(42));
    assert_eq!(NoteValue::from(42u64), NoteValue::Uint(42));
    assert_eq!(NoteValue::from(0.5_f64), NoteValue::Float(0.5));
    assert_eq!(NoteValue::from(true), NoteValue::Bool(true));
    assert_eq!(
        NoteValue::from("hello".to_string()),
        NoteValue::Text("hello".to_string()),
    );
    assert_eq!(
        NoteValue::from("borrowed"),
        NoteValue::Text("borrowed".to_string()),
    );
}

/// `note_value` writes into [`AssertResult::measurements`] without
/// altering the verdict. Distinct from [`Self::note`] (which
/// pushes a [`crate::assert::InfoNote`] onto
/// [`AssertResult::info_notes`]) — a producer commonly calls
/// BOTH and they occupy independent buffers.
#[test]
fn note_value_records_without_altering_verdict() {
    let mut r = AssertResult::pass();
    let was_pass = r.is_pass();
    let was_skip = r.is_skip();
    let was_outcomes = r.outcomes.len();
    r.note_value("max_wchar", 12345i64);
    r.note_value("psi_available", true);
    assert_eq!(r.is_pass(), was_pass);
    assert_eq!(r.is_skip(), was_skip);
    assert_eq!(r.outcomes.len(), was_outcomes);
    assert_eq!(r.measurements.len(), 2);
    assert_eq!(r.measurements["max_wchar"], NoteValue::Int(12345));
    assert_eq!(r.measurements["psi_available"], NoteValue::Bool(true));
}

/// Duplicate-key write overwrites: producers that re-record under
/// the same key (a producer bug, but well-defined) get the latest
/// value. Pin this so a future "first write wins" refactor surfaces
/// here.
#[test]
fn note_value_overwrites_on_duplicate_key() {
    let mut r = AssertResult::pass();
    r.note_value("counter", 1i64);
    r.note_value("counter", 2i64);
    assert_eq!(r.measurements["counter"], NoteValue::Int(2));
    assert_eq!(r.measurements.len(), 1);
}

/// `merge` folds `other.measurements` into `self.measurements`
/// with last-write-wins on key collision (matching `note_value`).
/// Pins the shape so a regression that union-with-keep-first
/// (e.g. `entry.or_insert(v)`) trips here.
#[test]
fn merge_unions_measurements_last_write_wins() {
    let mut a = AssertResult::pass();
    a.note_value("a_only", 1i64);
    a.note_value("shared", 100i64);

    let mut b = AssertResult::pass();
    b.note_value("b_only", 2i64);
    b.note_value("shared", 200i64);

    a.merge(b);
    assert_eq!(a.measurements.len(), 3);
    assert_eq!(a.measurements["a_only"], NoteValue::Int(1));
    assert_eq!(a.measurements["b_only"], NoteValue::Int(2));
    assert_eq!(
        a.measurements["shared"],
        NoteValue::Int(200),
        "merge must adopt other's value on key collision (last write wins)",
    );
}

/// `measurements` survives serde round-trip with the
/// externally-tagged default representation flowing into the right
/// variant on deserialize. Pins the wire-format invariant so a
/// regression that re-adds `#[serde(untagged)]` — which postcard
/// cannot decode (returns `WontImplement`) — trips here at test
/// time rather than as a silent data drop at runtime. See
/// `assert_result_postcard_roundtrip` in `tests_serde.rs` for the
/// postcard-side pin.
#[test]
fn note_value_survives_serde_roundtrip() {
    let mut r = AssertResult::pass();
    r.note_value("answer", 42i64);
    r.note_value("ratio", 0.5_f64);
    r.note_value("name", "fio");
    let json = serde_json::to_string(&r).unwrap();
    assert!(
        json.contains("\"measurements\""),
        "measurements key must appear in JSON when populated: {json}",
    );
    let r2: AssertResult = serde_json::from_str(&json).unwrap();
    assert_eq!(r2.measurements["answer"], NoteValue::Int(42));
    assert_eq!(r2.measurements["ratio"], NoteValue::Float(0.5));
    assert_eq!(r2.measurements["name"], NoteValue::Text("fio".to_string()));
}

/// Empty `measurements` is present in the wire format as `{}`.
/// `skip_serializing_if` was removed because AssertResult is
/// serialized with postcard (positional) — skipping a field on
/// serialize misaligns the deserializer. `#[serde(default)]`
/// handles old sidecars that lack the key.
#[test]
fn empty_measurements_present_in_wire_format() {
    let r = AssertResult::pass();
    let json = serde_json::to_string(&r).unwrap();
    assert!(
        json.contains("\"measurements\":{}"),
        "empty measurements must be present in JSON for postcard compat: {json}",
    );
}

// -- AssertResult::any_of / AssertResult::all_of --------------------

/// `any_of` with at least one passing branch returns a passing
/// result and annotates which branch was chosen. Pin: failed-branch
/// details are dropped (they would only confuse the operator with
/// messages from not-taken paths).
#[test]
fn any_of_chooses_passing_branch() {
    let r = AssertResult::any_of([
        {
            let mut a = AssertResult::pass();
            a.record_fail(AssertDetail::new(DetailKind::Other, "boom"));
            a
        },
        AssertResult::pass(),
    ]);
    assert!(r.is_pass());
    // The "boom" detail from the failed branch must NOT appear —
    // the chosen branch's outcomes prevail.
    assert!(
        !r.failure_details().any(|d| d.message.contains("boom")),
        "failed-branch details must be dropped: {:?}",
        r.outcomes,
    );
    // The chosen-branch annotation MUST appear in info_notes with
    // branch index 1 — any_of moves its "branch N satisfied"
    // annotation to the structural info-notes stream, never the
    // failure stream.
    assert!(
        r.info_notes
            .iter()
            .any(|n| n.message.contains("any_of: branch 1 satisfied")),
        "chosen-branch annotation missing: {:?}",
        r.info_notes,
    );
}

/// `any_of` with all branches failing must prefix EVERY branch's
/// `info_notes` with the same `any_of[<idx>]:` stamp as `details`.
/// Symmetric structural treatment — the auto-repro renderer attributes
/// notes to the branch that emitted them. A regression that dropped
/// the info_notes prefix loop would silently strip attribution.
#[test]
fn any_of_all_fail_prefixes_info_notes_with_branch_index() {
    let r = AssertResult::any_of([
        {
            let mut a = AssertResult::fail(AssertDetail::new(DetailKind::Other, "boom_0"));
            a.note("context_from_branch_0");
            a
        },
        {
            let mut b = AssertResult::fail(AssertDetail::new(DetailKind::Other, "boom_1"));
            b.note("context_from_branch_1");
            b
        },
    ]);
    assert!(r.is_fail());
    let messages: Vec<&str> = r.info_notes.iter().map(|n| n.message.as_str()).collect();
    assert!(
        messages.contains(&"any_of[0]: context_from_branch_0"),
        "branch 0 note must carry index prefix: {:?}",
        r.info_notes,
    );
    assert!(
        messages.contains(&"any_of[1]: context_from_branch_1"),
        "branch 1 note must carry index prefix: {:?}",
        r.info_notes,
    );
}

/// `any_of` with at least one passing branch unions info_notes from
/// every passing branch (each prefix-stamped with `any_of[<idx>]:`)
/// plus the bare arbiter "branch N satisfied" annotation. Failed-
/// branch info_notes are dropped (matching failed-branch details
/// policy). Pinned so a regression that drops the pass-path union
/// loop or the prefix stamping silently loses operator-visible
/// provenance.
#[test]
fn any_of_pass_path_unions_passing_branch_info_notes_with_prefix() {
    let r = AssertResult::any_of([
        {
            let mut a = AssertResult::pass();
            a.note("context_from_branch_0");
            a
        },
        {
            let mut b = AssertResult::pass();
            b.note("context_from_branch_1");
            b
        },
        {
            let mut c = AssertResult::fail(AssertDetail::new(DetailKind::Other, "boom"));
            c.note("context_from_branch_2_failed");
            c
        },
    ]);
    assert!(r.is_pass());
    let messages: Vec<&str> = r.info_notes.iter().map(|n| n.message.as_str()).collect();
    // Branch 0 + branch 1 notes survive, both prefix-stamped.
    assert!(
        messages.contains(&"any_of[0]: context_from_branch_0"),
        "branch 0 passing note must survive with prefix: {messages:?}"
    );
    assert!(
        messages.contains(&"any_of[1]: context_from_branch_1"),
        "branch 1 passing note must survive with prefix: {messages:?}"
    );
    // Failed-branch note is dropped (matches details policy).
    assert!(
        !messages.iter().any(|m| m.contains("branch_2_failed")),
        "failed-branch note must be dropped: {messages:?}"
    );
    // Bare arbiter annotation appended last (no prefix).
    assert!(
        messages.contains(&"any_of: branch 0 satisfied the disjunction"),
        "bare arbiter annotation missing: {messages:?}"
    );
}

/// `any_of` with all branches failing returns a failing result and
/// concatenates every branch's details under a `any_of[<idx>]:`
/// prefix so the operator can identify which branch produced
/// which failure.
#[test]
fn any_of_concatenates_branch_failures_with_index_prefixes() {
    let r = AssertResult::any_of([
        AssertResult::fail(AssertDetail::new(DetailKind::Other, "first boom")),
        AssertResult::fail(AssertDetail::new(DetailKind::Other, "second boom")),
    ]);
    assert!(r.is_fail());
    assert!(
        r.failure_details()
            .any(|d| d.message == "any_of[0]: first boom"),
        "branch 0 detail must carry index prefix: {:?}",
        r.outcomes,
    );
    assert!(
        r.failure_details()
            .any(|d| d.message == "any_of[1]: second boom"),
        "branch 1 detail must carry index prefix: {:?}",
        r.outcomes,
    );
    // A summary line names the per-disposition counts (failed /
    // inconclusive / skipped) so the operator sees the shape of
    // the disjunction, not just the failure count.
    assert!(
        r.failure_details()
            .any(|d| d.message.contains("2 failed") && d.message.contains("of 2 branches")),
        "summary line missing per-disposition counts: {:?}",
        r.outcomes,
    );
}

/// `any_of` with empty input fails — an empty disjunction is
/// logically false. Pinned to surface a producer bug as a
/// nameable failure rather than a vacuous pass.
#[test]
fn any_of_empty_input_fails() {
    let r = AssertResult::any_of(std::iter::empty());
    assert!(r.is_fail());
    assert!(
        r.failure_details()
            .any(|d| d.message.contains("empty branch list")),
        "empty disjunction must surface as named failure: {:?}",
        r.outcomes,
    );
}

/// `any_of` with all branches Inconclusive yields an Inconclusive
/// verdict (not Fail and not Pass). Pins the discriminant-aware
/// synthesis at the end of `any_of` — without this, an all-
/// Inconclusive disjunction would synthesize `Outcome::Fail` and
/// promote unevaluated branches to a real failure, lying to CI.
#[test]
fn any_of_all_inconclusive_branches_yields_inconclusive() {
    let mut b0 = AssertResult::pass();
    b0.record_inconclusive(AssertDetail::new(
        DetailKind::Migration,
        "zero denom branch 0",
    ));
    let mut b1 = AssertResult::pass();
    b1.record_inconclusive(AssertDetail::new(
        DetailKind::Benchmark,
        "zero denom branch 1",
    ));
    let r = AssertResult::any_of([b0, b1]);
    assert!(
        r.is_inconclusive(),
        "all-Inconclusive disjunction must yield Inconclusive, not Fail or Pass: {:?}",
        r.outcomes,
    );
    assert!(!r.is_fail(), "must not promote Inconclusive to Fail");
    assert!(
        !r.is_pass(),
        "must not silently pass on unevaluated branches"
    );
    // The synthesized summary line names the per-disposition counts.
    assert!(
        r.inconclusive_details()
            .any(|d| d.message.contains("2 inconclusive") && d.message.contains("of 2 branches")),
        "summary line missing inconclusive count: {:?}",
        r.outcomes,
    );
    // Every branch's payload is prefixed with `any_of[N]:` so the
    // operator can trace which branch was inconclusive.
    let inconc_messages: Vec<&str> = r
        .inconclusive_details()
        .map(|d| d.message.as_str())
        .collect();
    assert!(
        inconc_messages
            .iter()
            .any(|m| m.contains("any_of[0]: zero denom branch 0")),
        "branch 0 inconclusive missing prefix: {inconc_messages:?}",
    );
    assert!(
        inconc_messages
            .iter()
            .any(|m| m.contains("any_of[1]: zero denom branch 1")),
        "branch 1 inconclusive missing prefix: {inconc_messages:?}",
    );
}

/// `any_of` with Fail-plus-Inconclusive branches yields Fail
/// (real failure dominates an unevaluated check). Pins the
/// `if n_fail > 0` short-circuit in the synthesis — a regression
/// to "any Inconclusive demotes to Inconclusive" would silently
/// hide real failures behind a not-evaluated label.
#[test]
fn any_of_fail_plus_inconclusive_yields_fail() {
    let failing = AssertResult::fail(AssertDetail::new(DetailKind::Other, "real boom"));
    let mut inconc = AssertResult::pass();
    inconc.record_inconclusive(AssertDetail::new(DetailKind::Migration, "zero denom"));
    let r = AssertResult::any_of([failing, inconc]);
    assert!(
        r.is_fail(),
        "Fail+Inconclusive disjunction must yield Fail: {:?}",
        r.outcomes,
    );
    assert!(!r.is_inconclusive());
    // Summary names both counts (1 fail + 1 inconclusive of 2).
    assert!(
        r.failure_details()
            .any(|d| d.message.contains("1 failed") && d.message.contains("1 inconclusive")),
        "summary line missing per-disposition counts: {:?}",
        r.outcomes,
    );
}

/// `any_of` with Inconclusive-plus-Skip branches (no Fail, no
/// Pass) yields Inconclusive — Inconclusive dominates Skip per
/// `Fail > Inconclusive > Pass > Skip`. Pins that the synthesis
/// chain falls through to the `n_inc > 0` arm rather than the
/// all-Skip arm when any branch is Inconclusive.
#[test]
fn any_of_inconclusive_plus_skip_yields_inconclusive() {
    let mut inconc = AssertResult::pass();
    inconc.record_inconclusive(AssertDetail::new(DetailKind::Migration, "zero denom"));
    let skip = AssertResult::skip("topology missing");
    let r = AssertResult::any_of([inconc, skip]);
    assert!(
        r.is_inconclusive(),
        "Inconclusive+Skip disjunction must yield Inconclusive (Inconclusive dominates Skip): {:?}",
        r.outcomes,
    );
    assert!(!r.is_skip());
    assert!(!r.is_pass());
}

/// `all_of` is conjunction: passes iff every branch passes.
/// Empty input yields the passing identity (matches
/// `Iterator::all` semantics).
#[test]
fn all_of_passes_when_every_branch_passes() {
    let r = AssertResult::all_of([AssertResult::pass(), AssertResult::pass()]);
    assert!(r.is_pass());

    // One failing branch flips the verdict.
    let r = AssertResult::all_of([
        AssertResult::pass(),
        AssertResult::fail(AssertDetail::new(DetailKind::Other, "boom")),
    ]);
    assert!(r.is_fail());

    // Empty input is the passing identity.
    let r = AssertResult::all_of(std::iter::empty());
    assert!(r.is_pass());
    assert!(r.outcomes.is_empty());
}

/// `Verdict::note_value` mirrors [`AssertResult::note_value`] —
/// records under `measurements` without altering the verdict.
#[test]
fn verdict_note_value_records_into_underlying_result() {
    let mut v = Verdict::new();
    v.note_value("max_wchar", 12345i64);
    v.note_value("psi_available", false);
    let r = v.into_result();
    assert!(r.is_pass());
    assert_eq!(r.measurements.len(), 2);
    assert_eq!(r.measurements["max_wchar"], NoteValue::Int(12345));
    assert_eq!(r.measurements["psi_available"], NoteValue::Bool(false));
}