Skip to main content

supercov_engine/
rust_runtime.rs

1//! Generated std-only runtime and strict reader for owned Rust probes.
2//!
3//! The target runtime writes an intentionally small append-only transport.
4//! It never computes coverage. Rust reads, validates, de-duplicates and maps
5//! these observations into the shared evidence-v3 model after test execution.
6
7use serde::{Deserialize, Serialize};
8use std::{
9    collections::BTreeMap,
10    fs,
11    path::{Component, Path},
12};
13
14const RUST_PROBE_MAGIC: &str = "SUPERCOV-RUST-PROBE-1";
15
16#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
17#[serde(rename_all = "camelCase", tag = "kind")]
18pub enum RustProbeObservation {
19    Hit {
20        id: String,
21    },
22    Decision {
23        id: String,
24        values: Vec<Option<bool>>,
25        outcome: bool,
26    },
27    /// Control passed a statement that asserts. A failing assertion panics,
28    /// so reaching the marker means it held.
29    Assertion {
30        id: String,
31    },
32}
33
34/// One record of an owned-runtime evidence file, with the thread that wrote
35/// it. Evidence is appended under one mutex, so a file is in execution order,
36/// but a test that spawns threads interleaves them: an assertion witnesses
37/// its own thread's records and no other's.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct RustProbeEntry {
40    pub thread: u64,
41    pub observation: RustProbeObservation,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum RustProbeReadError {
46    Io(String),
47    UnsafeEntry(String),
48    InvalidHeader,
49    InvalidRecord(usize),
50}
51
52impl std::fmt::Display for RustProbeReadError {
53    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        match self {
55            Self::Io(error) => write!(formatter, "Rust probe I/O failed: {error}"),
56            Self::UnsafeEntry(path) => write!(formatter, "unsafe Rust probe entry: {path}"),
57            Self::InvalidHeader => write!(formatter, "invalid Rust probe header"),
58            Self::InvalidRecord(line) => {
59                write!(formatter, "invalid Rust probe record at line {line}")
60            }
61        }
62    }
63}
64
65impl std::error::Error for RustProbeReadError {}
66
67/// A thread ordinal: decimal, no sign, no padding, and small enough to parse.
68fn valid_thread(value: &str) -> bool {
69    !value.is_empty()
70        && value.len() <= 20
71        && value.bytes().all(|byte| byte.is_ascii_digit())
72        && (value == "0" || !value.starts_with('0'))
73        && value.parse::<u64>().is_ok()
74}
75
76pub(crate) fn valid_probe_id(id: &str) -> bool {
77    let mut parts = id.split(':');
78    matches!(parts.next(), Some("rs"))
79        && matches!(
80            parts.next(),
81            Some("statement" | "function" | "decision" | "branch")
82        )
83        && parts.next().is_some_and(|digest| {
84            digest.len() == 24 && digest.bytes().all(|byte| byte.is_ascii_hexdigit())
85        })
86        && parts.all(|suffix| {
87            !suffix.is_empty()
88                && suffix
89                    .bytes()
90                    .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
91        })
92}
93
94pub fn render_rust_runtime(module_name: &str, crate_key: &str) -> Result<String, String> {
95    let valid_identifier = !module_name.is_empty()
96        && module_name.bytes().enumerate().all(|(index, byte)| {
97            byte == b'_' || byte.is_ascii_alphabetic() || (index > 0 && byte.is_ascii_digit())
98        });
99    if !valid_identifier {
100        return Err("invalid Rust runtime module name".into());
101    }
102    if crate_key.len() != 24 || !crate_key.bytes().all(|byte| byte.is_ascii_hexdigit()) {
103        return Err("invalid Rust runtime crate key".into());
104    }
105
106    Ok(format!(
107        r#"
108#[doc(hidden)]
109// Injected code must be immune to the HOST crate's lint configuration: serde
110// builds with `#![deny(warnings)]`, so this module's fully-qualified imports
111// (required for no_std hosts) became hard errors as "unused imports".
112#[allow(warnings)]
113mod {module_name} {{
114    // The host crate may be `#![no_std]` -- `bytes` is, and so is much of the
115    // ecosystem's foundation. Nothing here can rely on the std prelude being in
116    // scope, so std is brought in explicitly and every prelude item below is
117    // written out in full. Without this the module does not compile and the
118    // whole build fails, which is a hard failure rather than a degradation.
119    extern crate std;
120    // Every name is imported explicitly: this module is appended to a crate
121    // root, and a crate root may be `no_std` or may turn the prelude off
122    // outright (tracing's macro tests carry `#![no_implicit_prelude]`).
123    use std::cmp::{{Ord, PartialEq, PartialOrd}};
124    use std::convert::From;
125    use std::fs::{{File, OpenOptions}};
126    use std::io::Write as _;
127    use std::iter::{{IntoIterator, Iterator}};
128    use std::marker::Sized;
129    use std::mem::drop;
130    use std::option::Option::{{self, None, Some}};
131    use std::result::Result::{{self, Err, Ok}};
132    use std::string::String;
133    use std::sync::atomic::{{AtomicBool, AtomicU8, AtomicU64, AtomicUsize, Ordering}};
134    use std::sync::{{Mutex, OnceLock}};
135    use std::cell::Cell;
136    use std::ops::ControlFlow;
137    use std::task::Poll;
138    use std::vec::Vec;
139
140    const MAGIC: &[u8] = b"{RUST_PROBE_MAGIC}\n";
141    const CRATE_KEY: &str = "{crate_key}";
142
143    // NOTHING ON THE PROBE PATH MAY ALLOCATE.
144    //
145    // The crate under test may install a `#[global_allocator]` whose body -- or
146    // anything it calls, at any depth -- carries probes. An allocating probe
147    // then re-enters the allocator, which probes, which allocates. bytes-1.12.1
148    // does this in tests/test_bytes_odd_alloc.rs and tests/test_bytes_vec_alloc.rs,
149    // and both died with SIGSEGV before libtest could list a single test.
150    //
151    // A reentrancy flag cannot fix this, and the attempt is instructive: on
152    // macOS the FIRST touch of a thread-local calls `_tlv_bootstrap`, which
153    // allocates -- so the guard recursed inside its own initialisation, before
154    // it could be consulted. A guard that must allocate to answer "am I already
155    // allocating?" is unfixable. Not allocating at all is.
156    //
157    // Records are therefore built in a stack buffer and written with one call.
158    // That also removes a malloc and a free from every probe, which is where
159    // most of a probe's cost used to be.
160    const RECORD_CAPACITY: usize = 256;
161
162    /// The widest condition vector a decision can carry.
163    ///
164    /// Beyond this the frame refuses to record rather than emit a vector whose
165    /// width disagrees with the manifest -- a malformed record the runner
166    /// rejects, which is a wrong number rather than a missing one.
167    const MAX_CONDITIONS: usize = 64;
168
169    // A statement or function hit answers "did this ever run", so only the FIRST
170    // sighting in a process carries information -- and each libtest case runs in
171    // its own process, so first-in-process is first-in-test. Without this, a loop
172    // writes one identical record per iteration: bytes'
173    // advance_bytes_mut_remaining_capacity runs ~2.8M iterations and was still
174    // writing syscalls after four minutes.
175    //
176    // The table is a fixed, open-addressed set of `&'static str` POINTERS, so it
177    // never allocates and never grows -- both of which the probe path forbids.
178    // A crowded table simply writes the record again: a duplicate costs time,
179    // never correctness, whereas dropping one would cost a real observation.
180    const SEEN_SLOTS: usize = 1 << 16;
181    static SEEN: [AtomicUsize; SEEN_SLOTS] = [const {{ AtomicUsize::new(0) }}; SEEN_SLOTS];
182
183    // Which thread wrote a record. Evidence is appended under one mutex, so
184    // the file is in execution order -- but a test that spawns threads
185    // interleaves them, and an assertion on one thread witnesses nothing on
186    // another. Ordinals are handed out on a thread's first record; the main
187    // test thread is 1.
188    static NEXT_THREAD: AtomicU64 = AtomicU64::new(1);
189    std::thread_local! {{
190        static THREAD_ORDINAL: Cell<u64> = const {{ Cell::new(0) }};
191    }}
192
193    fn thread_ordinal() -> u64 {{
194        THREAD_ORDINAL.with(|slot| {{
195            let existing = slot.get();
196            if existing != 0 {{
197                return existing;
198            }}
199            let assigned = NEXT_THREAD.fetch_add(1, Ordering::Relaxed);
200            slot.set(assigned);
201            assigned
202        }})
203    }}
204
205    /// Decimal digits of `value`, written into `record`.
206    fn push_number(record: &mut [u8; RECORD_CAPACITY], length: &mut usize, value: u64) -> bool {{
207        let mut digits = [0u8; 20];
208        let mut count = 0;
209        let mut rest = value;
210        loop {{
211            digits[count] = b'0' + (rest % 10) as u8;
212            count += 1;
213            rest /= 10;
214            if rest == 0 {{
215                break;
216            }}
217        }}
218        let mut index = count;
219        while index > 0 {{
220            index -= 1;
221            if !push(record, length, &[digits[index]]) {{
222                return false;
223            }}
224        }}
225        true
226    }}
227
228    // An assertion marker is deduped separately from hits: it names the
229    // statement that asserts, and that statement's own hit has already been
230    // recorded by the time control reaches the marker.
231    const ASSERTED_SLOTS: usize = 1 << 12;
232    static ASSERTED: [AtomicUsize; ASSERTED_SLOTS] =
233        [const {{ AtomicUsize::new(0) }}; ASSERTED_SLOTS];
234
235    #[inline(always)]
236    fn first_assertion(id: &'static str) -> bool {{
237        let key = id.as_ptr() as usize;
238        let mut slot = (key >> 4) & (ASSERTED_SLOTS - 1);
239        for _ in 0..8 {{
240            let seen = ASSERTED[slot].load(Ordering::Relaxed);
241            if seen == key {{
242                return false;
243            }}
244            if seen == 0 {{
245                match ASSERTED[slot].compare_exchange(0, key, Ordering::Relaxed, Ordering::Relaxed)
246                {{
247                    Ok(_) => return true,
248                    Err(now) if now == key => return false,
249                    Err(_) => {{}}
250                }}
251            }}
252            slot = (slot + 1) & (ASSERTED_SLOTS - 1);
253        }}
254        true
255    }}
256
257    /// Control passed a statement that asserts, so every assertion in it
258    /// held: a failing one panics instead. Evidence this thread wrote before
259    /// this point was in scope for that check.
260    #[inline(always)]
261    pub fn assertion(id: &'static str) {{
262        if first_assertion(id) {{
263            record_assertion(id);
264        }}
265    }}
266
267    #[inline(never)]
268    fn record_assertion(id: &'static str) {{
269        let mut record = [0u8; RECORD_CAPACITY];
270        let mut length = 0;
271        if push(&mut record, &mut length, b"A\t")
272            && push_number(&mut record, &mut length, thread_ordinal())
273            && push(&mut record, &mut length, b"\t")
274            && push(&mut record, &mut length, id.as_bytes())
275            && push(&mut record, &mut length, b"\n")
276        {{
277            write_record(&record[..length]);
278        }}
279    }}
280
281    #[inline(always)]
282    fn first_sighting(id: &'static str) -> bool {{
283        let key = id.as_ptr() as usize;
284        let mut slot = (key >> 4) & (SEEN_SLOTS - 1);
285        for _ in 0..8 {{
286            // Almost every call is a repeat, and a plain load settles it; the
287            // CAS is for the one insertion.
288            let seen = SEEN[slot].load(Ordering::Relaxed);
289            if seen == key {{
290                return false;
291            }}
292            if seen == 0 {{
293                match SEEN[slot].compare_exchange(0, key, Ordering::Relaxed, Ordering::Relaxed) {{
294                    Ok(_) => return true,
295                    Err(now) if now == key => return false,
296                    Err(_) => {{}}
297                }}
298            }}
299            slot = (slot + 1) & (SEEN_SLOTS - 1);
300        }}
301        true
302    }}
303
304    // Decisions cannot collapse by id the way hits do: MC/DC needs the SET of
305    // distinct condition vectors, so every vector must reach the log once. What
306    // carries nothing is the REPEAT of a vector already seen, and in a loop that
307    // is nearly all of them -- bytes' advance_bytes_mut_remaining_capacity took
308    // 40.6s against a 0.367s baseline writing one syscall per evaluation across
309    // ~2.8M iterations.
310    //
311    // Entries hold the whole record -- id pointer, outcome, width, values -- and
312    // are compared word for word. A hash would be smaller and faster, but a
313    // collision would silently drop a distinct vector and understate MC/DC, and
314    // that is exactly the kind of wrong number this project refuses to risk.
315    // A full probe chain falls back to writing, which costs a duplicate.
316    //
317    // The table is lock-free: a decision in a hot loop is evaluated millions
318    // of times, and a mutex per evaluation was most of what a probe cost. A
319    // slot's state goes 0 (empty) -> 1 (being written) -> 2 (readable); a
320    // reader compares only readable slots, so a slot mid-write reads as
321    // "different" and at worst costs one duplicate record.
322    const DECISION_SLOTS: usize = 1 << 11;
323    const DECISION_ENTRY: usize = 10 + MAX_CONDITIONS;
324    const DECISION_WORDS: usize = DECISION_ENTRY.div_ceil(8);
325
326    struct DecisionSlot {{
327        state: AtomicU8,
328        words: [AtomicU64; DECISION_WORDS],
329    }}
330
331    static DECISIONS: [DecisionSlot; DECISION_SLOTS] = [const {{
332        DecisionSlot {{
333            state: AtomicU8::new(0),
334            words: [const {{ AtomicU64::new(0) }}; DECISION_WORDS],
335        }}
336    }}; DECISION_SLOTS];
337
338    // This module is compiled in the crate's own profile -- unoptimized under
339    // `cargo test` -- so the paths below are plain indexed loops: iterator
340    // chains and closures cost real function calls there, and a decision in a
341    // hot loop pays them millions of times.
342    fn first_decision(frame: &DecisionFrame, outcome: bool) -> bool {{
343        let key = frame.id.as_ptr() as usize;
344        let conditions = if frame.conditions < MAX_CONDITIONS {{ frame.conditions }} else {{ MAX_CONDITIONS }};
345        // Word 0 is the id pointer; word 1 starts with outcome and width, then
346        // the condition values fill the remaining bytes. Entries of the same
347        // width are zero beyond `used` words, and the width byte sits in word
348        // 1, so comparing `used` words is exact.
349        let mut words = [0u64; DECISION_WORDS];
350        words[0] = key as u64;
351        words[1] = (if outcome {{ 2u64 }} else {{ 1u64 }}) | ((conditions as u64) << 8);
352        let mut index = 0;
353        while index < conditions {{
354            let byte = 10 + index;
355            words[byte / 8] |= (frame.value(index) as u64) << ((byte % 8) * 8);
356            index += 1;
357        }}
358        let used = (10 + conditions).div_ceil(8);
359        let mut slot = (key >> 4) & (DECISION_SLOTS - 1);
360        let mut attempts = 0;
361        while attempts < 16 {{
362            let cell = &DECISIONS[slot];
363            let state = cell.state.load(Ordering::Acquire);
364            if state == 2 {{
365                let mut same = true;
366                let mut word = 0;
367                while word < used {{
368                    if cell.words[word].load(Ordering::Relaxed) != words[word] {{
369                        same = false;
370                        break;
371                    }}
372                    word += 1;
373                }}
374                if same {{
375                    return false;
376                }}
377            }} else if state == 0 {{
378                if cell
379                    .state
380                    .compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
381                    .is_ok()
382                {{
383                    let mut word = 0;
384                    while word < DECISION_WORDS {{
385                        cell.words[word].store(words[word], Ordering::Relaxed);
386                        word += 1;
387                    }}
388                    cell.state.store(2, Ordering::Release);
389                    return true;
390                }}
391                // Another thread took this slot first: look at it again.
392                continue;
393            }}
394            slot = (slot + 1) & (DECISION_SLOTS - 1);
395            attempts += 1;
396        }}
397        true
398    }}
399
400    fn writer() -> Option<&'static Mutex<File>> {{
401        static WRITER: OnceLock<Option<Mutex<File>>> = OnceLock::new();
402        static OPENING: AtomicBool = AtomicBool::new(false);
403        if let Some(writer) = WRITER.get() {{
404            return writer.as_ref();
405        }}
406        // Opening the file is the one step that must allocate: an environment
407        // lookup, a path, a formatted file name. That allocation re-enters an
408        // instrumented allocator, whose probe arrives back here while the
409        // OnceLock is still unset. Declining for the duration of the open costs
410        // a few observations at startup and makes the recursion impossible.
411        if OPENING.swap(true, Ordering::SeqCst) {{
412            return None;
413        }}
414        let opened = WRITER.get_or_init(|| {{
415            let directory = std::env::var_os("SUPERCOV_RUST_EVIDENCE_DIR")?;
416            let directory = std::path::PathBuf::from(directory);
417            std::fs::create_dir_all(&directory).ok()?;
418            let path =
419                directory.join(std::format!("{{CRATE_KEY}}-{{}}.events", std::process::id()));
420            let empty = std::fs::metadata(&path).map_or(true, |metadata| metadata.len() == 0);
421            let mut file = OpenOptions::new().create(true).append(true).open(path).ok()?;
422            if empty {{
423                file.write_all(MAGIC).ok()?;
424            }}
425            let guarded = Mutex::new(file);
426            // `std::sync::Mutex` boxes a platform mutex on its FIRST lock, and
427            // that allocation would otherwise land on the probe path and
428            // re-enter the host allocator. Force it here, where `OPENING`
429            // already makes re-entry harmless.
430            drop(guarded.lock());
431            Some(guarded)
432        }});
433        OPENING.store(false, Ordering::SeqCst);
434        opened.as_ref()
435    }}
436
437    fn write_record(record: &[u8]) {{
438        let Some(writer) = writer() else {{ return }};
439        let Ok(mut writer) = writer.lock() else {{ return }};
440        let _ = writer.write_all(record);
441    }}
442
443    /// Append to a stack record, reporting whether it all fit.
444    fn push(record: &mut [u8; RECORD_CAPACITY], length: &mut usize, bytes: &[u8]) -> bool {{
445        let Some(slice) = record.get_mut(*length..*length + bytes.len()) else {{
446            return false;
447        }};
448        slice.copy_from_slice(bytes);
449        *length += bytes.len();
450        true
451    }}
452
453    // A frame lives on the stack of the function whose decision it records,
454    // once per decision, and a recursive descent parser carries every one of
455    // them down every level: serde_json's recursion-limit test overflowed the
456    // test thread's stack when each frame held two 64-byte arrays. Values are
457    // two bits each in a u128 (0 unevaluated, 1 false, 2 true) and reached
458    // marks one bit each, so a frame is 48 bytes.
459    pub struct DecisionFrame {{
460        id: &'static str,
461        values: u128,
462        /// Let-chain conditions the evaluation got to: a `let` cannot be
463        /// wrapped, so it is marked reached instead and resolved later.
464        reached: u64,
465        conditions: usize,
466        recordable: bool,
467    }}
468
469    impl DecisionFrame {{
470        pub fn new(id: &'static str, conditions: usize) -> Self {{
471            Self {{
472                id,
473                values: 0,
474                reached: 0,
475                conditions,
476                recordable: conditions <= MAX_CONDITIONS,
477            }}
478        }}
479
480        #[inline(always)]
481        fn value(&self, index: usize) -> u8 {{
482            ((self.values >> (2 * index)) & 3) as u8
483        }}
484
485        #[inline(always)]
486        fn set_value(&mut self, index: usize, value: u8) {{
487            let shift = 2 * index;
488            self.values = (self.values & !(3u128 << shift)) | ((value as u128) << shift);
489        }}
490
491        #[inline(always)]
492        fn is_reached(&self, index: usize) -> bool {{
493            index < MAX_CONDITIONS && (self.reached >> index) & 1 == 1
494        }}
495    }}
496
497    /// A let chain got to condition `index` (0 marks the chain evaluated at
498    /// all). Always true, so it sits in the chain as an operand.
499    #[inline(always)]
500    pub fn reached(frame: &mut DecisionFrame, index: usize) -> bool {{
501        if index < MAX_CONDITIONS {{
502            frame.reached |= 1u64 << index;
503        }}
504        true
505    }}
506
507    /// A let chain decided. A chain tries its conditions in order and stops
508    /// at the first that fails, so every reached `let` before the last
509    /// reached condition held, the last one held when the chain was taken and
510    /// failed when it was not, and conditions never reached stay unevaluated.
511    /// `operators` lists the `&&` whose left side holds a `let`, by the index
512    /// of their right side's first condition: reached means the operator
513    /// evaluated its right side, otherwise it short-circuited. The frame then
514    /// resets for the next evaluation, which a `while let` makes every turn.
515    pub fn decision_chain(
516        frame: &mut DecisionFrame,
517        outcome: bool,
518        operators: &[(usize, &'static str, &'static str)],
519    ) {{
520        if !frame.is_reached(0) {{
521            return;
522        }}
523        let conditions = frame.conditions.min(MAX_CONDITIONS);
524        let mut last = 0;
525        let mut index = 0;
526        while index < conditions {{
527            if frame.is_reached(index) || frame.value(index) != 0 {{
528                last = index;
529            }}
530            index += 1;
531        }}
532        let mut index = 0;
533        while index < conditions {{
534            if frame.value(index) == 0 && frame.is_reached(index) {{
535                frame.set_value(index, if index < last || outcome {{ 2 }} else {{ 1 }});
536            }}
537            index += 1;
538        }}
539        for (first, short_circuit, evaluated) in operators {{
540            let got_there = frame.is_reached(*first)
541                || (*first < MAX_CONDITIONS && frame.value(*first) != 0);
542            hit(if got_there {{ evaluated }} else {{ short_circuit }});
543        }}
544        decision(outcome, frame);
545        frame.values = 0;
546        frame.reached = 0;
547    }}
548
549    // The hot paths are inlined into every probe site, so nothing with a
550    // stack buffer may be: an inlined 256-byte record per site turned each
551    // instrumented function's frame into kilobytes, and serde_json's
552    // recursion-limit test overflowed. Writing a record is the rare path and
553    // stays a call of its own.
554    #[inline(always)]
555    pub fn hit(id: &'static str) {{
556        if first_sighting(id) {{
557            record_hit(id);
558        }}
559    }}
560
561    #[inline(never)]
562    fn record_hit(id: &'static str) {{
563        let mut record = [0u8; RECORD_CAPACITY];
564        let mut length = 0;
565        if push(&mut record, &mut length, b"H\t")
566            && push_number(&mut record, &mut length, thread_ordinal())
567            && push(&mut record, &mut length, b"\t")
568            && push(&mut record, &mut length, id.as_bytes())
569            && push(&mut record, &mut length, b"\n")
570        {{
571            write_record(&record[..length]);
572        }}
573    }}
574
575    /// One arm of a match was selected. `ids` holds each arm's `not selected`
576    /// and `selected` IDs in source order, so every arm before `selected` was
577    /// considered and passed over. Each ID is a distinct static string, which
578    /// is what `hit` dedupes on.
579    #[inline(always)]
580    pub fn arms(ids: &[&'static str], selected: usize) {{
581        for arm in 0..selected {{
582            if let Some(id) = ids.get(arm * 2) {{
583                hit(id);
584            }}
585        }}
586        if let Some(id) = ids.get(selected * 2 + 1) {{
587            hit(id);
588        }}
589    }}
590
591    /// The left operand of `&&` or `||`: it short-circuits when it equals
592    /// `short_circuits_when`, otherwise the right operand is about to run.
593    #[inline(always)]
594    pub fn logical(
595        left: bool,
596        short_circuits_when: bool,
597        short_circuit: &'static str,
598        evaluated: &'static str,
599    ) -> bool {{
600        hit(if left == short_circuits_when {{ short_circuit }} else {{ evaluated }});
601        left
602    }}
603
604    /// A `for` loop's iterator, recording on the first `next` whether the
605    /// body ran at all. `size_hint` passes through so collection sizing is
606    /// unchanged; nothing else about the iterator is observable to the loop.
607    pub struct ForLoop<I> {{
608        inner: I,
609        first: bool,
610        zero: &'static str,
611        entered: &'static str,
612    }}
613
614    impl<I: Iterator> Iterator for ForLoop<I> {{
615        type Item = I::Item;
616
617        #[inline(always)]
618        fn next(&mut self) -> Option<I::Item> {{
619            let item = self.inner.next();
620            if self.first {{
621                self.first = false;
622                hit(if item.is_some() {{ self.entered }} else {{ self.zero }});
623            }}
624            item
625        }}
626
627        #[inline(always)]
628        fn size_hint(&self) -> (usize, Option<usize>) {{
629            self.inner.size_hint()
630        }}
631    }}
632
633    #[inline(always)]
634    pub fn for_loop<I: IntoIterator>(
635        iterable: I,
636        zero: &'static str,
637        entered: &'static str,
638    ) -> ForLoop<I::IntoIter> {{
639        ForLoop {{ inner: iterable.into_iter(), first: true, zero, entered }}
640    }}
641
642    /// A `while` body ran: clear the loop's flag on the first entry.
643    #[inline(always)]
644    pub fn entered(first: &mut bool, id: &'static str) {{
645        if *first {{
646            *first = false;
647            hit(id);
648        }}
649    }}
650
651    /// A `while` loop is over: a flag still set means the body never ran.
652    #[inline(always)]
653    pub fn zero_iterations(first: bool, id: &'static str) {{
654        if first {{
655            hit(id);
656        }}
657    }}
658
659    /// The operand of `?`, recording which way the operator goes. Every type
660    /// `?` accepts on stable Rust implements this.
661    pub trait TryProbe: Sized {{
662        fn probe(self, continued: &'static str, returned: &'static str) -> Self;
663    }}
664
665    impl<T> TryProbe for Option<T> {{
666        #[inline(always)]
667        fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
668            hit(if self.is_some() {{ continued }} else {{ returned }});
669            self
670        }}
671    }}
672
673    impl<T, E> TryProbe for Result<T, E> {{
674        #[inline(always)]
675        fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
676            hit(if self.is_ok() {{ continued }} else {{ returned }});
677            self
678        }}
679    }}
680
681    impl<B, C> TryProbe for ControlFlow<B, C> {{
682        #[inline(always)]
683        fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
684            hit(match self {{
685                ControlFlow::Continue(_) => continued,
686                _ => returned,
687            }});
688            self
689        }}
690    }}
691
692    impl<T, E> TryProbe for Poll<Result<T, E>> {{
693        #[inline(always)]
694        fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
695            hit(match self {{
696                Poll::Ready(Err(_)) => returned,
697                _ => continued,
698            }});
699            self
700        }}
701    }}
702
703    impl<T, E> TryProbe for Poll<Option<Result<T, E>>> {{
704        #[inline(always)]
705        fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
706            hit(match self {{
707                Poll::Ready(Some(Err(_))) => returned,
708                _ => continued,
709            }});
710            self
711        }}
712    }}
713
714    // Anything `!` turns into a bool: `assert!(was_seen)` compiles with a
715    // `&bool` (tokio iterates `for was_seen in &seen`), since the macro only
716    // negates its operand.
717    #[inline(always)]
718    pub fn condition<V: std::ops::Not<Output = bool>>(
719        value: V,
720        frame: &mut DecisionFrame,
721        index: usize,
722    ) -> bool {{
723        let value = !!value;
724        if index < frame.conditions && index < MAX_CONDITIONS {{
725            frame.set_value(index, if value {{ 2 }} else {{ 1 }});
726        }}
727        value
728    }}
729
730    #[inline(always)]
731    pub fn decision(value: bool, frame: &mut DecisionFrame) -> bool {{
732        if frame.recordable {{
733            record_decision(value, frame);
734        }}
735        value
736    }}
737
738    #[inline(never)]
739    fn record_decision(value: bool, frame: &DecisionFrame) {{
740        // `writer()` comes first so the file's mutex boxes its platform mutex
741        // while `OPENING` still makes re-entry harmless; the decision table
742        // itself is lock-free and allocates nothing.
743        if writer().is_none() || !first_decision(frame, value) {{
744            return;
745        }}
746        let mut record = [0u8; RECORD_CAPACITY];
747        let mut length = 0;
748        let mut fits = push(&mut record, &mut length, b"D\t")
749            && push_number(&mut record, &mut length, thread_ordinal())
750            && push(&mut record, &mut length, b"\t")
751            && push(&mut record, &mut length, frame.id.as_bytes())
752            && push(&mut record, &mut length, b"\t");
753        let mut index = 0;
754        while index < frame.conditions {{
755            fits = fits && push(&mut record, &mut length, &[b'0' + frame.value(index)]);
756            index += 1;
757        }}
758        fits = fits
759            && push(&mut record, &mut length, b"\t")
760            && push(&mut record, &mut length, if value {{ b"1" }} else {{ b"0" }})
761            && push(&mut record, &mut length, b"\n");
762        if fits {{
763            write_record(&record[..length]);
764        }}
765    }}
766}}
767"#
768    ))
769}
770
771pub fn parse_rust_probe_events(input: &[u8]) -> Result<Vec<RustProbeEntry>, RustProbeReadError> {
772    let text = std::str::from_utf8(input).map_err(|_| RustProbeReadError::InvalidHeader)?;
773    let mut lines = text.lines();
774    if lines.next() != Some(RUST_PROBE_MAGIC) {
775        return Err(RustProbeReadError::InvalidHeader);
776    }
777    let mut observations = Vec::new();
778    for (index, line) in lines.enumerate() {
779        let line_number = index + 2;
780        let fields = line.split('\t').collect::<Vec<_>>();
781        match fields.as_slice() {
782            ["H", thread, id] if valid_probe_id(id) && valid_thread(thread) => {
783                observations.push(RustProbeEntry {
784                    thread: thread.parse().expect("a validated thread ordinal"),
785                    observation: RustProbeObservation::Hit { id: (*id).into() },
786                })
787            }
788            ["A", thread, id] if valid_probe_id(id) && valid_thread(thread) => {
789                observations.push(RustProbeEntry {
790                    thread: thread.parse().expect("a validated thread ordinal"),
791                    observation: RustProbeObservation::Assertion { id: (*id).into() },
792                })
793            }
794            ["D", thread, id, digits, outcome]
795                if valid_probe_id(id)
796                    && valid_thread(thread)
797                    && id.starts_with("rs:decision:")
798                    && !digits.is_empty()
799                    && digits
800                        .bytes()
801                        .all(|digit| matches!(digit, b'0' | b'1' | b'2'))
802                    && matches!(*outcome, "0" | "1") =>
803            {
804                observations.push(RustProbeEntry {
805                    thread: thread.parse().expect("a validated thread ordinal"),
806                    observation: RustProbeObservation::Decision {
807                        id: (*id).into(),
808                        values: digits
809                            .bytes()
810                            .map(|digit| match digit {
811                                b'0' => None,
812                                b'1' => Some(false),
813                                b'2' => Some(true),
814                                _ => unreachable!(),
815                            })
816                            .collect(),
817                        outcome: *outcome == "1",
818                    },
819                });
820            }
821            _ => return Err(RustProbeReadError::InvalidRecord(line_number)),
822        }
823    }
824    Ok(observations)
825}
826
827pub fn read_rust_probe_directory(
828    directory: &Path,
829) -> Result<BTreeMap<String, Vec<RustProbeEntry>>, RustProbeReadError> {
830    let mut files = fs::read_dir(directory)
831        .map_err(|error| RustProbeReadError::Io(error.to_string()))?
832        .collect::<Result<Vec<_>, _>>()
833        .map_err(|error| RustProbeReadError::Io(error.to_string()))?;
834    files.sort_by_key(|entry| entry.file_name());
835    let mut observations = BTreeMap::new();
836    for entry in files {
837        let name = entry
838            .file_name()
839            .into_string()
840            .map_err(|_| RustProbeReadError::UnsafeEntry("<non-utf8>".into()))?;
841        if Path::new(&name)
842            .components()
843            .any(|component| !matches!(component, Component::Normal(_)))
844            || !name.ends_with(".events")
845        {
846            return Err(RustProbeReadError::UnsafeEntry(name));
847        }
848        let metadata = fs::symlink_metadata(entry.path())
849            .map_err(|error| RustProbeReadError::Io(error.to_string()))?;
850        if !metadata.file_type().is_file() {
851            return Err(RustProbeReadError::UnsafeEntry(name));
852        }
853        let contents =
854            fs::read(entry.path()).map_err(|error| RustProbeReadError::Io(error.to_string()))?;
855        observations.insert(name, parse_rust_probe_events(&contents)?);
856    }
857    Ok(observations)
858}
859
860#[cfg(test)]
861mod tests {
862    use std::{
863        fs,
864        process::Command,
865        time::{SystemTime, UNIX_EPOCH},
866    };
867
868    use super::*;
869    use crate::rust_instrumenter::instrument_rust_source;
870
871    fn temporary_directory(name: &str) -> std::path::PathBuf {
872        let nonce = SystemTime::now()
873            .duration_since(UNIX_EPOCH)
874            .unwrap()
875            .as_nanos();
876        let path = std::env::temp_dir().join(format!(
877            "supercov-rust-runtime-{}-{nonce}-{name}",
878            std::process::id()
879        ));
880        fs::create_dir(&path).unwrap();
881        path
882    }
883
884    #[test]
885    fn generated_runtime_records_owned_points_and_exact_short_circuit_vectors() {
886        let source = r#"fn choose(first: bool, second: bool) -> i32 {
887    if first && second { 7 } else { 3 }
888}
889
890fn main() {
891    println!("{} {}", choose(false, true), choose(true, true));
892}
893"#;
894        let transformed =
895            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
896        let runtime =
897            render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
898        let directory = temporary_directory("record");
899        let input = directory.join("main.rs");
900        let binary = directory.join("program");
901        let evidence = directory.join("evidence");
902        fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
903        let compile = Command::new("rustc")
904            .arg("--edition=2024")
905            .arg(&input)
906            .arg("-o")
907            .arg(&binary)
908            .output()
909            .unwrap();
910        assert!(
911            compile.status.success(),
912            "{}",
913            String::from_utf8_lossy(&compile.stderr)
914        );
915        let output = Command::new(&binary)
916            .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
917            .output()
918            .unwrap();
919        assert!(output.status.success());
920        assert_eq!(output.stdout, b"3 7\n");
921        let files = read_rust_probe_directory(&evidence).unwrap();
922        assert_eq!(files.len(), 1);
923        let observations = files.values().next().unwrap();
924        let decisions = observations
925            .iter()
926            .filter_map(|entry| match &entry.observation {
927                RustProbeObservation::Decision {
928                    values, outcome, ..
929                } => Some((values.clone(), *outcome)),
930                RustProbeObservation::Hit { .. } | RustProbeObservation::Assertion { .. } => None,
931            })
932            .collect::<Vec<_>>();
933        assert_eq!(
934            decisions,
935            [
936                (vec![Some(false), None], false),
937                (vec![Some(true), Some(true)], true)
938            ]
939        );
940        fs::remove_dir_all(directory).unwrap();
941    }
942
943    #[test]
944    fn generated_runtime_compiles_into_a_no_std_host_crate() {
945        // `#![no_std]` swaps the std prelude for core's, so `format!`, `vec!`
946        // and `Vec` are simply not in scope. The injected module named them
947        // unqualified and every no_std crate failed to build -- found on
948        // bytes-1.12.1, which is `#![no_std]` (src/lib.rs:6). `extern crate std`
949        // here mirrors what the injected module does: it links std without
950        // restoring the prelude, which is precisely the condition under test.
951        let source = r#"#![no_std]
952
953extern crate std;
954
955fn choose(first: bool, second: bool) -> i32 {
956    if first && second { 7 } else { 3 }
957}
958
959fn main() {
960    std::println!("{} {}", choose(false, true), choose(true, true));
961}
962"#;
963        let transformed =
964            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
965        let runtime =
966            render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
967        let directory = temporary_directory("no-std");
968        let input = directory.join("main.rs");
969        let binary = directory.join("program");
970        let evidence = directory.join("evidence");
971        fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
972        let compile = Command::new("rustc")
973            .arg("--edition=2024")
974            .arg(&input)
975            .arg("-o")
976            .arg(&binary)
977            .output()
978            .unwrap();
979        assert!(
980            compile.status.success(),
981            "{}",
982            String::from_utf8_lossy(&compile.stderr)
983        );
984        let output = Command::new(&binary)
985            .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
986            .output()
987            .unwrap();
988        assert!(output.status.success());
989        assert_eq!(output.stdout, b"3 7\n");
990        // Probes must still record, not merely compile.
991        let files = read_rust_probe_directory(&evidence).unwrap();
992        assert_eq!(files.len(), 1);
993        assert!(!files.values().next().unwrap().is_empty());
994        fs::remove_dir_all(directory).unwrap();
995    }
996
997    #[test]
998    fn probes_reached_through_a_global_allocator_do_not_recurse() {
999        // The shape from bytes-1.12.1 tests/test_bytes_vec_alloc.rs: the
1000        // allocator's `alloc` calls an inherent method, which calls a FREE
1001        // FUNCTION. Skipping `impl GlobalAlloc` blocks syntactically does not
1002        // cover `note`, and nothing syntactic can -- the chain may leave the
1003        // file or the crate. Only the runtime knows a probe is already running,
1004        // so the guard has to live there. Without it this binary dies with
1005        // SIGSEGV instead of printing anything.
1006        let source = r#"use std::alloc::{GlobalAlloc, Layout, System};
1007use std::sync::atomic::{AtomicUsize, Ordering};
1008
1009static SEEN: AtomicUsize = AtomicUsize::new(0);
1010
1011fn note(size: usize) {
1012    if size > 0 {
1013        SEEN.fetch_add(1, Ordering::SeqCst);
1014    }
1015}
1016
1017struct Ledger;
1018
1019impl Ledger {
1020    fn record(&self, size: usize) {
1021        note(size);
1022    }
1023}
1024
1025unsafe impl GlobalAlloc for Ledger {
1026    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
1027        self.record(layout.size());
1028        System.alloc(layout)
1029    }
1030
1031    unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
1032        // dealloc must be instrumented too, or the test never exercises the
1033        // ladder that actually crashed: freeing the probe's OWN buffer re-enters
1034        // here, and a guard released before that free recurses without bound.
1035        self.record(layout.size());
1036        System.dealloc(pointer, layout);
1037    }
1038}
1039
1040#[global_allocator]
1041static LEDGER: Ledger = Ledger;
1042
1043fn classify(flag: bool) -> usize {
1044    if flag { 1 } else { 2 }
1045}
1046
1047fn main() {
1048    let held = std::vec![7u8; 32];
1049    println!("{} {}", classify(!held.is_empty()), held.len());
1050}
1051"#;
1052        let transformed =
1053            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1054        // `note` is a free function, so it IS instrumented -- proving the guard,
1055        // not a syntactic skip, is what prevents the recursion.
1056        assert!(
1057            transformed
1058                .code
1059                .contains("fn note(size: usize) {\ncrate::__supercov_runtime_v1::hit("),
1060            "the free function reached from the allocator should still be probed"
1061        );
1062        let runtime =
1063            render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
1064        let directory = temporary_directory("allocator-reentry");
1065        let input = directory.join("main.rs");
1066        let binary = directory.join("program");
1067        let evidence = directory.join("evidence");
1068        fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
1069        let compile = Command::new("rustc")
1070            .arg("--edition=2024")
1071            .arg(&input)
1072            .arg("-o")
1073            .arg(&binary)
1074            .output()
1075            .unwrap();
1076        assert!(
1077            compile.status.success(),
1078            "{}",
1079            String::from_utf8_lossy(&compile.stderr)
1080        );
1081        let output = Command::new(&binary)
1082            .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
1083            .output()
1084            .unwrap();
1085        assert!(
1086            output.status.success(),
1087            "instrumented allocator did not survive: {:?}",
1088            output.status
1089        );
1090        assert_eq!(output.stdout, b"1 32\n");
1091        let files = read_rust_probe_directory(&evidence).unwrap();
1092        let observations = files.values().next().unwrap();
1093        let decisions = observations
1094            .iter()
1095            .filter_map(|entry| match &entry.observation {
1096                RustProbeObservation::Decision {
1097                    id,
1098                    values,
1099                    outcome,
1100                } => Some((id.clone(), values.clone(), *outcome)),
1101                RustProbeObservation::Hit { .. } | RustProbeObservation::Assertion { .. } => None,
1102            })
1103            .collect::<Vec<_>>();
1104        // Suppression costs only duplicates: `classify` runs outside any probe,
1105        // so its decision is still recorded exactly.
1106        let classify = transformed
1107            .manifest
1108            .decisions
1109            .iter()
1110            .find(|decision| decision.source == "flag")
1111            .expect("classify's decision reached the manifest");
1112        assert!(
1113            decisions
1114                .iter()
1115                .any(|(id, values, outcome)| id == &classify.id
1116                    && values == &[Some(true)]
1117                    && *outcome),
1118            "classify's decision was lost: {decisions:?}"
1119        );
1120        // `note` records too -- every ordinary allocation reaches it outside a
1121        // probe -- which is why suppressing the nested ones loses nothing.
1122        let note = transformed
1123            .manifest
1124            .decisions
1125            .iter()
1126            .find(|decision| decision.source == "size > 0")
1127            .expect("note's decision reached the manifest");
1128        assert!(decisions.iter().any(|(id, ..)| id == &note.id));
1129        // No frame built while nested may reach the log: a zero-width vector
1130        // for a one-condition decision is a malformed record, not a lost one.
1131        assert!(
1132            decisions.iter().all(|(_, values, _)| values.len() == 1),
1133            "a suppressed frame emitted a malformed vector: {decisions:?}"
1134        );
1135        fs::remove_dir_all(directory).unwrap();
1136    }
1137
1138    #[test]
1139    fn a_hit_in_a_loop_is_written_once_but_decisions_keep_every_vector() {
1140        // bytes' advance_bytes_mut_remaining_capacity is a triple-nested loop of
1141        // ~2.8M iterations. One write syscall per probe per iteration left it
1142        // still running after four minutes. A hit only answers "did this ever
1143        // run", so the repeats carry nothing -- but a decision's condition
1144        // vector differs per iteration and every distinct one must survive.
1145        let source = r#"fn step(value: usize) -> bool {
1146    let doubled = value * 2;
1147    doubled > 4
1148}
1149
1150fn main() {
1151    let mut seen = 0;
1152    for value in 0..64 {
1153        if step(value) { seen += 1; }
1154    }
1155    println!("{seen}");
1156}
1157"#;
1158        let transformed =
1159            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1160        let runtime =
1161            render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
1162        let directory = temporary_directory("dedup");
1163        let input = directory.join("main.rs");
1164        let binary = directory.join("program");
1165        let evidence = directory.join("evidence");
1166        fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
1167        let compile = Command::new("rustc")
1168            .arg("--edition=2024")
1169            .arg(&input)
1170            .arg("-o")
1171            .arg(&binary)
1172            .output()
1173            .unwrap();
1174        assert!(
1175            compile.status.success(),
1176            "{}",
1177            String::from_utf8_lossy(&compile.stderr)
1178        );
1179        let output = Command::new(&binary)
1180            .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
1181            .output()
1182            .unwrap();
1183        assert_eq!(output.stdout, b"61\n");
1184        let files = read_rust_probe_directory(&evidence).unwrap();
1185        let observations = files.values().next().unwrap();
1186
1187        // `doubled > 4` runs 64 times; its hit is recorded once.
1188        let mut hits = BTreeMap::<&str, usize>::new();
1189        for entry in observations {
1190            if let RustProbeObservation::Hit { id } = &entry.observation {
1191                *hits.entry(id.as_str()).or_default() += 1;
1192            }
1193        }
1194        assert!(!hits.is_empty(), "no hits recorded at all");
1195        assert!(
1196            hits.values().all(|count| *count == 1),
1197            "a repeated hit was written more than once: {hits:?}"
1198        );
1199
1200        // MC/DC needs the SET of condition vectors, not how often each recurred,
1201        // so a repeat of an already-seen vector carries nothing -- but every
1202        // DISTINCT vector must still arrive. `doubled > 4` is evaluated 64 times
1203        // and takes exactly two distinct shapes, so exactly two records survive.
1204        let decisions = observations
1205            .iter()
1206            .filter_map(|entry| match &entry.observation {
1207                RustProbeObservation::Decision {
1208                    values, outcome, ..
1209                } => Some((values.clone(), *outcome)),
1210                RustProbeObservation::Hit { .. } | RustProbeObservation::Assertion { .. } => None,
1211            })
1212            .collect::<Vec<_>>();
1213        assert_eq!(
1214            decisions,
1215            // In first-occurrence order: `step(0)` is false before any value
1216            // exceeds the threshold.
1217            [(vec![Some(false)], false), (vec![Some(true)], true)],
1218            "both distinct vectors must survive, and neither may repeat"
1219        );
1220        fs::remove_dir_all(directory).unwrap();
1221    }
1222
1223    #[test]
1224    fn generated_runtime_survives_a_host_without_the_prelude() {
1225        // tracing's macro tests carry `#![no_implicit_prelude]` on purpose,
1226        // to prove their macros do not depend on it. The runtime imports
1227        // every name it uses for `no_std` hosts, but three `TryProbe` impls
1228        // reached for `matches!`, which is a prelude macro.
1229        let source = r#"#![no_implicit_prelude]
1230
1231fn parse(text: &str) -> ::std::option::Option<i32> {
1232    let value: i32 = text.parse().ok()?;
1233    ::std::option::Option::Some(value * 2)
1234}
1235
1236fn main() {
1237    ::std::println!("{:?} {:?}", parse("4"), parse("x"));
1238}
1239"#;
1240        let transformed =
1241            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1242        // The `?` takes a probe, which is what pulls in the TryProbe impls.
1243        assert!(transformed.code.contains("TryProbe"));
1244        let runtime =
1245            render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
1246        let directory = temporary_directory("no-implicit-prelude");
1247        let input = directory.join("main.rs");
1248        let binary = directory.join("program");
1249        fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
1250        let compile = Command::new("rustc")
1251            .arg("--edition=2024")
1252            .arg("--cap-lints=warn")
1253            .arg(&input)
1254            .arg("-o")
1255            .arg(&binary)
1256            .output()
1257            .unwrap();
1258        assert!(
1259            compile.status.success(),
1260            "{}",
1261            String::from_utf8_lossy(&compile.stderr)
1262        );
1263        let output = Command::new(&binary).output().unwrap();
1264        assert_eq!(output.stdout, b"Some(8) None\n");
1265        fs::remove_dir_all(directory).unwrap();
1266    }
1267
1268    #[test]
1269    fn generated_runtime_survives_a_deny_warnings_host() {
1270        // serde builds with `#![deny(warnings)]`; the injected module's
1271        // fully-qualified imports (required for no_std hosts) read as unused
1272        // imports and became hard errors. Injected code must be immune to the
1273        // host's lint policy.
1274        let source = r#"#![deny(warnings)]
1275
1276fn choose(first: bool, second: bool) -> i32 {
1277    if first && second { 7 } else { 3 }
1278}
1279
1280fn main() {
1281    println!("{} {}", choose(false, true), choose(true, true));
1282}
1283"#;
1284        let transformed =
1285            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1286        let runtime =
1287            render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
1288        let directory = temporary_directory("deny-warnings");
1289        let input = directory.join("main.rs");
1290        let binary = directory.join("program");
1291        fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
1292        // --cap-lints=warn mirrors what the runner passes for the instrumented
1293        // workspace: the host policy must not reject generated code, including
1294        // the `if ({{ frame ... }})` decision wrapping that trips unused_parens.
1295        let compile = Command::new("rustc")
1296            .arg("--edition=2024")
1297            .arg("--cap-lints=warn")
1298            .arg(&input)
1299            .arg("-o")
1300            .arg(&binary)
1301            .output()
1302            .unwrap();
1303        assert!(
1304            compile.status.success(),
1305            "{}",
1306            String::from_utf8_lossy(&compile.stderr)
1307        );
1308        let output = Command::new(&binary).output().unwrap();
1309        assert_eq!(output.stdout, b"3 7\n");
1310        fs::remove_dir_all(directory).unwrap();
1311    }
1312
1313    #[test]
1314    fn reader_rejects_truncation_invalid_digits_and_non_files() {
1315        assert_eq!(
1316            parse_rust_probe_events(
1317                b"SUPERCOV-RUST-PROBE-1\nD\trs:decision:0123456789abcdef01234567\t03\t1\n"
1318            ),
1319            Err(RustProbeReadError::InvalidRecord(2))
1320        );
1321        assert_eq!(
1322            parse_rust_probe_events(b"SUPERCOV-RUST-PROBE-"),
1323            Err(RustProbeReadError::InvalidHeader)
1324        );
1325
1326        let directory = temporary_directory("unsafe");
1327        fs::create_dir(directory.join("nested.events")).unwrap();
1328        assert!(matches!(
1329            read_rust_probe_directory(&directory),
1330            Err(RustProbeReadError::UnsafeEntry(_))
1331        ));
1332        fs::remove_dir_all(directory).unwrap();
1333    }
1334}