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}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum RustProbeReadError {
31    Io(String),
32    UnsafeEntry(String),
33    InvalidHeader,
34    InvalidRecord(usize),
35}
36
37impl std::fmt::Display for RustProbeReadError {
38    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            Self::Io(error) => write!(formatter, "Rust probe I/O failed: {error}"),
41            Self::UnsafeEntry(path) => write!(formatter, "unsafe Rust probe entry: {path}"),
42            Self::InvalidHeader => write!(formatter, "invalid Rust probe header"),
43            Self::InvalidRecord(line) => {
44                write!(formatter, "invalid Rust probe record at line {line}")
45            }
46        }
47    }
48}
49
50impl std::error::Error for RustProbeReadError {}
51
52pub(crate) fn valid_probe_id(id: &str) -> bool {
53    let mut parts = id.split(':');
54    matches!(parts.next(), Some("rs"))
55        && matches!(
56            parts.next(),
57            Some("statement" | "function" | "decision" | "branch")
58        )
59        && parts.next().is_some_and(|digest| {
60            digest.len() == 24 && digest.bytes().all(|byte| byte.is_ascii_hexdigit())
61        })
62        && parts.all(|suffix| {
63            !suffix.is_empty()
64                && suffix
65                    .bytes()
66                    .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
67        })
68}
69
70pub fn render_rust_runtime(module_name: &str, crate_key: &str) -> Result<String, String> {
71    let valid_identifier = !module_name.is_empty()
72        && module_name.bytes().enumerate().all(|(index, byte)| {
73            byte == b'_' || byte.is_ascii_alphabetic() || (index > 0 && byte.is_ascii_digit())
74        });
75    if !valid_identifier {
76        return Err("invalid Rust runtime module name".into());
77    }
78    if crate_key.len() != 24 || !crate_key.bytes().all(|byte| byte.is_ascii_hexdigit()) {
79        return Err("invalid Rust runtime crate key".into());
80    }
81
82    Ok(format!(
83        r#"
84#[doc(hidden)]
85// Injected code must be immune to the HOST crate's lint configuration: serde
86// builds with `#![deny(warnings)]`, so this module's fully-qualified imports
87// (required for no_std hosts) became hard errors as "unused imports".
88#[allow(warnings)]
89mod {module_name} {{
90    // The host crate may be `#![no_std]` -- `bytes` is, and so is much of the
91    // ecosystem's foundation. Nothing here can rely on the std prelude being in
92    // scope, so std is brought in explicitly and every prelude item below is
93    // written out in full. Without this the module does not compile and the
94    // whole build fails, which is a hard failure rather than a degradation.
95    extern crate std;
96    use std::fs::{{File, OpenOptions}};
97    use std::io::Write as _;
98    use std::option::Option::{{self, None, Some}};
99    use std::string::String;
100    use std::sync::atomic::{{AtomicBool, AtomicUsize, Ordering}};
101    use std::sync::{{Mutex, OnceLock}};
102    use std::ops::ControlFlow;
103    use std::task::Poll;
104    use std::vec::Vec;
105
106    const MAGIC: &[u8] = b"{RUST_PROBE_MAGIC}\n";
107    const CRATE_KEY: &str = "{crate_key}";
108
109    // NOTHING ON THE PROBE PATH MAY ALLOCATE.
110    //
111    // The crate under test may install a `#[global_allocator]` whose body -- or
112    // anything it calls, at any depth -- carries probes. An allocating probe
113    // then re-enters the allocator, which probes, which allocates. bytes-1.12.1
114    // does this in tests/test_bytes_odd_alloc.rs and tests/test_bytes_vec_alloc.rs,
115    // and both died with SIGSEGV before libtest could list a single test.
116    //
117    // A reentrancy flag cannot fix this, and the attempt is instructive: on
118    // macOS the FIRST touch of a thread-local calls `_tlv_bootstrap`, which
119    // allocates -- so the guard recursed inside its own initialisation, before
120    // it could be consulted. A guard that must allocate to answer "am I already
121    // allocating?" is unfixable. Not allocating at all is.
122    //
123    // Records are therefore built in a stack buffer and written with one call.
124    // That also removes a malloc and a free from every probe, which is where
125    // most of a probe's cost used to be.
126    const RECORD_CAPACITY: usize = 256;
127
128    /// The widest condition vector a decision can carry.
129    ///
130    /// Beyond this the frame refuses to record rather than emit a vector whose
131    /// width disagrees with the manifest -- a malformed record the runner
132    /// rejects, which is a wrong number rather than a missing one.
133    const MAX_CONDITIONS: usize = 64;
134
135    // A statement or function hit answers "did this ever run", so only the FIRST
136    // sighting in a process carries information -- and each libtest case runs in
137    // its own process, so first-in-process is first-in-test. Without this, a loop
138    // writes one identical record per iteration: bytes'
139    // advance_bytes_mut_remaining_capacity runs ~2.8M iterations and was still
140    // writing syscalls after four minutes.
141    //
142    // The table is a fixed, open-addressed set of `&'static str` POINTERS, so it
143    // never allocates and never grows -- both of which the probe path forbids.
144    // A crowded table simply writes the record again: a duplicate costs time,
145    // never correctness, whereas dropping one would cost a real observation.
146    const SEEN_SLOTS: usize = 1 << 16;
147    static SEEN: [AtomicUsize; SEEN_SLOTS] = [const {{ AtomicUsize::new(0) }}; SEEN_SLOTS];
148
149    fn first_sighting(id: &'static str) -> bool {{
150        let key = id.as_ptr() as usize;
151        let mut slot = (key >> 4) & (SEEN_SLOTS - 1);
152        for _ in 0..8 {{
153            match SEEN[slot].compare_exchange(0, key, Ordering::Relaxed, Ordering::Relaxed) {{
154                Ok(_) => return true,
155                Err(seen) if seen == key => return false,
156                Err(_) => slot = (slot + 1) & (SEEN_SLOTS - 1),
157            }}
158        }}
159        true
160    }}
161
162    // Decisions cannot collapse by id the way hits do: MC/DC needs the SET of
163    // distinct condition vectors, so every vector must reach the log once. What
164    // carries nothing is the REPEAT of a vector already seen, and in a loop that
165    // is nearly all of them -- bytes' advance_bytes_mut_remaining_capacity took
166    // 40.6s against a 0.367s baseline writing one syscall per evaluation across
167    // ~2.8M iterations.
168    //
169    // Entries hold the whole record -- id pointer, outcome, width, values -- and
170    // are compared byte for byte. A hash would be smaller and faster, but a
171    // collision would silently drop a distinct vector and understate MC/DC, and
172    // that is exactly the kind of wrong number this project refuses to risk.
173    // A full probe chain falls back to writing, which costs a duplicate.
174    const DECISION_SLOTS: usize = 1 << 11;
175    const DECISION_ENTRY: usize = 10 + MAX_CONDITIONS;
176
177    struct DecisionTable {{
178        entries: [[u8; DECISION_ENTRY]; DECISION_SLOTS],
179    }}
180
181    impl DecisionTable {{
182        const fn new() -> Self {{
183            Self {{ entries: [[0; DECISION_ENTRY]; DECISION_SLOTS] }}
184        }}
185    }}
186
187    // `Mutex::new` is const, so the table is a genuine static with no lazy
188    // allocation of its own. Its first lock still boxes a platform mutex, which
189    // `writer()` forces during startup.
190    static DECISIONS: Mutex<DecisionTable> = Mutex::new(DecisionTable::new());
191
192    fn first_decision(frame: &DecisionFrame, outcome: bool) -> bool {{
193        let key = frame.id.as_ptr() as usize;
194        let mut entry = [0u8; DECISION_ENTRY];
195        entry[..8].copy_from_slice(&(key as u64).to_le_bytes());
196        // Non-zero for an occupied slot, so an all-zero entry means empty.
197        entry[8] = if outcome {{ 2 }} else {{ 1 }};
198        entry[9] = frame.conditions as u8;
199        entry[10..10 + frame.conditions].copy_from_slice(&frame.values[..frame.conditions]);
200        let Ok(mut table) = DECISIONS.lock() else {{
201            return true;
202        }};
203        let mut slot = (key >> 4) & (DECISION_SLOTS - 1);
204        for _ in 0..16 {{
205            if table.entries[slot] == entry {{
206                return false;
207            }}
208            if table.entries[slot][8] == 0 {{
209                table.entries[slot] = entry;
210                return true;
211            }}
212            slot = (slot + 1) & (DECISION_SLOTS - 1);
213        }}
214        true
215    }}
216
217    fn writer() -> Option<&'static Mutex<File>> {{
218        static WRITER: OnceLock<Option<Mutex<File>>> = OnceLock::new();
219        static OPENING: AtomicBool = AtomicBool::new(false);
220        if let Some(writer) = WRITER.get() {{
221            return writer.as_ref();
222        }}
223        // Opening the file is the one step that must allocate: an environment
224        // lookup, a path, a formatted file name. That allocation re-enters an
225        // instrumented allocator, whose probe arrives back here while the
226        // OnceLock is still unset. Declining for the duration of the open costs
227        // a few observations at startup and makes the recursion impossible.
228        if OPENING.swap(true, Ordering::SeqCst) {{
229            return None;
230        }}
231        let opened = WRITER.get_or_init(|| {{
232            let directory = std::env::var_os("SUPERCOV_RUST_EVIDENCE_DIR")?;
233            let directory = std::path::PathBuf::from(directory);
234            std::fs::create_dir_all(&directory).ok()?;
235            let path =
236                directory.join(std::format!("{{CRATE_KEY}}-{{}}.events", std::process::id()));
237            let empty = std::fs::metadata(&path).map_or(true, |metadata| metadata.len() == 0);
238            let mut file = OpenOptions::new().create(true).append(true).open(path).ok()?;
239            if empty {{
240                file.write_all(MAGIC).ok()?;
241            }}
242            let guarded = Mutex::new(file);
243            // `std::sync::Mutex` boxes a platform mutex on its FIRST lock, and
244            // that allocation would otherwise land on the probe path and
245            // re-enter the host allocator. Force it here, where `OPENING`
246            // already makes re-entry harmless.
247            drop(guarded.lock());
248            // Same reason: the decision table's mutex boxes on first lock.
249            drop(DECISIONS.lock());
250            Some(guarded)
251        }});
252        OPENING.store(false, Ordering::SeqCst);
253        opened.as_ref()
254    }}
255
256    fn write_record(record: &[u8]) {{
257        let Some(writer) = writer() else {{ return }};
258        let Ok(mut writer) = writer.lock() else {{ return }};
259        let _ = writer.write_all(record);
260    }}
261
262    /// Append to a stack record, reporting whether it all fit.
263    fn push(record: &mut [u8; RECORD_CAPACITY], length: &mut usize, bytes: &[u8]) -> bool {{
264        let Some(slice) = record.get_mut(*length..*length + bytes.len()) else {{
265            return false;
266        }};
267        slice.copy_from_slice(bytes);
268        *length += bytes.len();
269        true
270    }}
271
272    pub struct DecisionFrame {{
273        id: &'static str,
274        values: [u8; MAX_CONDITIONS],
275        /// Let-chain conditions the evaluation got to: a `let` cannot be
276        /// wrapped, so it is marked reached instead and resolved later.
277        reached: [bool; MAX_CONDITIONS],
278        conditions: usize,
279        recordable: bool,
280    }}
281
282    impl DecisionFrame {{
283        pub fn new(id: &'static str, conditions: usize) -> Self {{
284            Self {{
285                id,
286                values: [0; MAX_CONDITIONS],
287                reached: [false; MAX_CONDITIONS],
288                conditions,
289                recordable: conditions <= MAX_CONDITIONS,
290            }}
291        }}
292    }}
293
294    /// A let chain got to condition `index` (0 marks the chain evaluated at
295    /// all). Always true, so it sits in the chain as an operand.
296    #[inline]
297    pub fn reached(frame: &mut DecisionFrame, index: usize) -> bool {{
298        if let Some(slot) = frame.reached.get_mut(index) {{
299            *slot = true;
300        }}
301        true
302    }}
303
304    /// A let chain decided. A chain tries its conditions in order and stops
305    /// at the first that fails, so every reached `let` before the last
306    /// reached condition held, the last one held when the chain was taken and
307    /// failed when it was not, and conditions never reached stay unevaluated.
308    /// `operators` lists the `&&` whose left side holds a `let`, by the index
309    /// of their right side's first condition: reached means the operator
310    /// evaluated its right side, otherwise it short-circuited. The frame then
311    /// resets for the next evaluation, which a `while let` makes every turn.
312    pub fn decision_chain(
313        frame: &mut DecisionFrame,
314        outcome: bool,
315        operators: &[(usize, &'static str, &'static str)],
316    ) {{
317        if !frame.reached[0] {{
318            return;
319        }}
320        let conditions = frame.conditions.min(MAX_CONDITIONS);
321        let mut last = 0;
322        for index in 0..conditions {{
323            if frame.reached[index] || frame.values[index] != 0 {{
324                last = index;
325            }}
326        }}
327        for index in 0..conditions {{
328            if frame.values[index] == 0 && frame.reached[index] {{
329                frame.values[index] = if index < last || outcome {{ 2 }} else {{ 1 }};
330            }}
331        }}
332        for (first, short_circuit, evaluated) in operators {{
333            let got_there = frame
334                .reached
335                .get(*first)
336                .copied()
337                .unwrap_or(false)
338                || frame.values.get(*first).is_some_and(|value| *value != 0);
339            hit(if got_there {{ evaluated }} else {{ short_circuit }});
340        }}
341        decision(outcome, frame);
342        frame.values = [0; MAX_CONDITIONS];
343        frame.reached = [false; MAX_CONDITIONS];
344    }}
345
346    #[inline]
347    pub fn hit(id: &'static str) {{
348        if !first_sighting(id) {{
349            return;
350        }}
351        let mut record = [0u8; RECORD_CAPACITY];
352        let mut length = 0;
353        if push(&mut record, &mut length, b"H\t")
354            && push(&mut record, &mut length, id.as_bytes())
355            && push(&mut record, &mut length, b"\n")
356        {{
357            write_record(&record[..length]);
358        }}
359    }}
360
361    /// One arm of a match was selected. `ids` holds each arm's `not selected`
362    /// and `selected` IDs in source order, so every arm before `selected` was
363    /// considered and passed over. Each ID is a distinct static string, which
364    /// is what `hit` dedupes on.
365    #[inline]
366    pub fn arms(ids: &[&'static str], selected: usize) {{
367        for arm in 0..selected {{
368            if let Some(id) = ids.get(arm * 2) {{
369                hit(id);
370            }}
371        }}
372        if let Some(id) = ids.get(selected * 2 + 1) {{
373            hit(id);
374        }}
375    }}
376
377    /// The left operand of `&&` or `||`: it short-circuits when it equals
378    /// `short_circuits_when`, otherwise the right operand is about to run.
379    #[inline]
380    pub fn logical(
381        left: bool,
382        short_circuits_when: bool,
383        short_circuit: &'static str,
384        evaluated: &'static str,
385    ) -> bool {{
386        hit(if left == short_circuits_when {{ short_circuit }} else {{ evaluated }});
387        left
388    }}
389
390    /// A `for` loop's iterator, recording on the first `next` whether the
391    /// body ran at all. `size_hint` passes through so collection sizing is
392    /// unchanged; nothing else about the iterator is observable to the loop.
393    pub struct ForLoop<I> {{
394        inner: I,
395        first: bool,
396        zero: &'static str,
397        entered: &'static str,
398    }}
399
400    impl<I: Iterator> Iterator for ForLoop<I> {{
401        type Item = I::Item;
402
403        #[inline]
404        fn next(&mut self) -> Option<I::Item> {{
405            let item = self.inner.next();
406            if self.first {{
407                self.first = false;
408                hit(if item.is_some() {{ self.entered }} else {{ self.zero }});
409            }}
410            item
411        }}
412
413        #[inline]
414        fn size_hint(&self) -> (usize, Option<usize>) {{
415            self.inner.size_hint()
416        }}
417    }}
418
419    #[inline]
420    pub fn for_loop<I: IntoIterator>(
421        iterable: I,
422        zero: &'static str,
423        entered: &'static str,
424    ) -> ForLoop<I::IntoIter> {{
425        ForLoop {{ inner: iterable.into_iter(), first: true, zero, entered }}
426    }}
427
428    /// A `while` body ran: clear the loop's flag on the first entry.
429    #[inline]
430    pub fn entered(first: &mut bool, id: &'static str) {{
431        if *first {{
432            *first = false;
433            hit(id);
434        }}
435    }}
436
437    /// A `while` loop is over: a flag still set means the body never ran.
438    #[inline]
439    pub fn zero_iterations(first: bool, id: &'static str) {{
440        if first {{
441            hit(id);
442        }}
443    }}
444
445    /// The operand of `?`, recording which way the operator goes. Every type
446    /// `?` accepts on stable Rust implements this.
447    pub trait TryProbe: Sized {{
448        fn probe(self, continued: &'static str, returned: &'static str) -> Self;
449    }}
450
451    impl<T> TryProbe for Option<T> {{
452        #[inline]
453        fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
454            hit(if self.is_some() {{ continued }} else {{ returned }});
455            self
456        }}
457    }}
458
459    impl<T, E> TryProbe for Result<T, E> {{
460        #[inline]
461        fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
462            hit(if self.is_ok() {{ continued }} else {{ returned }});
463            self
464        }}
465    }}
466
467    impl<B, C> TryProbe for ControlFlow<B, C> {{
468        #[inline]
469        fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
470            hit(if matches!(self, ControlFlow::Continue(_)) {{ continued }} else {{ returned }});
471            self
472        }}
473    }}
474
475    impl<T, E> TryProbe for Poll<Result<T, E>> {{
476        #[inline]
477        fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
478            hit(if matches!(self, Poll::Ready(Err(_))) {{ returned }} else {{ continued }});
479            self
480        }}
481    }}
482
483    impl<T, E> TryProbe for Poll<Option<Result<T, E>>> {{
484        #[inline]
485        fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
486            hit(if matches!(self, Poll::Ready(Some(Err(_)))) {{ returned }} else {{ continued }});
487            self
488        }}
489    }}
490
491    #[inline]
492    pub fn condition(value: bool, frame: &mut DecisionFrame, index: usize) -> bool {{
493        if index < frame.conditions {{
494            if let Some(slot) = frame.values.get_mut(index) {{
495                *slot = if value {{ 2 }} else {{ 1 }};
496            }}
497        }}
498        value
499    }}
500
501    #[inline]
502    pub fn decision(value: bool, frame: &mut DecisionFrame) -> bool {{
503        // `writer()` must come FIRST: it forces the decision table's mutex to box
504        // its platform mutex while `OPENING` still makes re-entry harmless.
505        // Deduplicating before that put the very first lock -- and its
506        // allocation -- on the probe path, which recursed straight back through
507        // an instrumented allocator. The allocator gate caught it.
508        if !frame.recordable || writer().is_none() || !first_decision(frame, value) {{
509            return value;
510        }}
511        let mut record = [0u8; RECORD_CAPACITY];
512        let mut length = 0;
513        let mut fits = push(&mut record, &mut length, b"D\t")
514            && push(&mut record, &mut length, frame.id.as_bytes())
515            && push(&mut record, &mut length, b"\t");
516        for index in 0..frame.conditions {{
517            fits = fits && push(&mut record, &mut length, &[b'0' + frame.values[index]]);
518        }}
519        fits = fits
520            && push(&mut record, &mut length, b"\t")
521            && push(&mut record, &mut length, if value {{ b"1" }} else {{ b"0" }})
522            && push(&mut record, &mut length, b"\n");
523        if fits {{
524            write_record(&record[..length]);
525        }}
526        value
527    }}
528}}
529"#
530    ))
531}
532
533pub fn parse_rust_probe_events(
534    input: &[u8],
535) -> Result<Vec<RustProbeObservation>, RustProbeReadError> {
536    let text = std::str::from_utf8(input).map_err(|_| RustProbeReadError::InvalidHeader)?;
537    let mut lines = text.lines();
538    if lines.next() != Some(RUST_PROBE_MAGIC) {
539        return Err(RustProbeReadError::InvalidHeader);
540    }
541    let mut observations = Vec::new();
542    for (index, line) in lines.enumerate() {
543        let line_number = index + 2;
544        let fields = line.split('\t').collect::<Vec<_>>();
545        match fields.as_slice() {
546            ["H", id] if valid_probe_id(id) => {
547                observations.push(RustProbeObservation::Hit { id: (*id).into() })
548            }
549            ["D", id, digits, outcome]
550                if valid_probe_id(id)
551                    && id.starts_with("rs:decision:")
552                    && !digits.is_empty()
553                    && digits
554                        .bytes()
555                        .all(|digit| matches!(digit, b'0' | b'1' | b'2'))
556                    && matches!(*outcome, "0" | "1") =>
557            {
558                observations.push(RustProbeObservation::Decision {
559                    id: (*id).into(),
560                    values: digits
561                        .bytes()
562                        .map(|digit| match digit {
563                            b'0' => None,
564                            b'1' => Some(false),
565                            b'2' => Some(true),
566                            _ => unreachable!(),
567                        })
568                        .collect(),
569                    outcome: *outcome == "1",
570                });
571            }
572            _ => return Err(RustProbeReadError::InvalidRecord(line_number)),
573        }
574    }
575    Ok(observations)
576}
577
578pub fn read_rust_probe_directory(
579    directory: &Path,
580) -> Result<BTreeMap<String, Vec<RustProbeObservation>>, RustProbeReadError> {
581    let mut files = fs::read_dir(directory)
582        .map_err(|error| RustProbeReadError::Io(error.to_string()))?
583        .collect::<Result<Vec<_>, _>>()
584        .map_err(|error| RustProbeReadError::Io(error.to_string()))?;
585    files.sort_by_key(|entry| entry.file_name());
586    let mut observations = BTreeMap::new();
587    for entry in files {
588        let name = entry
589            .file_name()
590            .into_string()
591            .map_err(|_| RustProbeReadError::UnsafeEntry("<non-utf8>".into()))?;
592        if Path::new(&name)
593            .components()
594            .any(|component| !matches!(component, Component::Normal(_)))
595            || !name.ends_with(".events")
596        {
597            return Err(RustProbeReadError::UnsafeEntry(name));
598        }
599        let metadata = fs::symlink_metadata(entry.path())
600            .map_err(|error| RustProbeReadError::Io(error.to_string()))?;
601        if !metadata.file_type().is_file() {
602            return Err(RustProbeReadError::UnsafeEntry(name));
603        }
604        let contents =
605            fs::read(entry.path()).map_err(|error| RustProbeReadError::Io(error.to_string()))?;
606        observations.insert(name, parse_rust_probe_events(&contents)?);
607    }
608    Ok(observations)
609}
610
611#[cfg(test)]
612mod tests {
613    use std::{
614        fs,
615        process::Command,
616        time::{SystemTime, UNIX_EPOCH},
617    };
618
619    use super::*;
620    use crate::rust_instrumenter::instrument_rust_source;
621
622    fn temporary_directory(name: &str) -> std::path::PathBuf {
623        let nonce = SystemTime::now()
624            .duration_since(UNIX_EPOCH)
625            .unwrap()
626            .as_nanos();
627        let path = std::env::temp_dir().join(format!(
628            "supercov-rust-runtime-{}-{nonce}-{name}",
629            std::process::id()
630        ));
631        fs::create_dir(&path).unwrap();
632        path
633    }
634
635    #[test]
636    fn generated_runtime_records_owned_points_and_exact_short_circuit_vectors() {
637        let source = r#"fn choose(first: bool, second: bool) -> i32 {
638    if first && second { 7 } else { 3 }
639}
640
641fn main() {
642    println!("{} {}", choose(false, true), choose(true, true));
643}
644"#;
645        let transformed =
646            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
647        let runtime =
648            render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
649        let directory = temporary_directory("record");
650        let input = directory.join("main.rs");
651        let binary = directory.join("program");
652        let evidence = directory.join("evidence");
653        fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
654        let compile = Command::new("rustc")
655            .arg("--edition=2024")
656            .arg(&input)
657            .arg("-o")
658            .arg(&binary)
659            .output()
660            .unwrap();
661        assert!(
662            compile.status.success(),
663            "{}",
664            String::from_utf8_lossy(&compile.stderr)
665        );
666        let output = Command::new(&binary)
667            .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
668            .output()
669            .unwrap();
670        assert!(output.status.success());
671        assert_eq!(output.stdout, b"3 7\n");
672        let files = read_rust_probe_directory(&evidence).unwrap();
673        assert_eq!(files.len(), 1);
674        let observations = files.values().next().unwrap();
675        let decisions = observations
676            .iter()
677            .filter_map(|observation| match observation {
678                RustProbeObservation::Decision {
679                    values, outcome, ..
680                } => Some((values.clone(), *outcome)),
681                RustProbeObservation::Hit { .. } => None,
682            })
683            .collect::<Vec<_>>();
684        assert_eq!(
685            decisions,
686            [
687                (vec![Some(false), None], false),
688                (vec![Some(true), Some(true)], true)
689            ]
690        );
691        fs::remove_dir_all(directory).unwrap();
692    }
693
694    #[test]
695    fn generated_runtime_compiles_into_a_no_std_host_crate() {
696        // `#![no_std]` swaps the std prelude for core's, so `format!`, `vec!`
697        // and `Vec` are simply not in scope. The injected module named them
698        // unqualified and every no_std crate failed to build -- found on
699        // bytes-1.12.1, which is `#![no_std]` (src/lib.rs:6). `extern crate std`
700        // here mirrors what the injected module does: it links std without
701        // restoring the prelude, which is precisely the condition under test.
702        let source = r#"#![no_std]
703
704extern crate std;
705
706fn choose(first: bool, second: bool) -> i32 {
707    if first && second { 7 } else { 3 }
708}
709
710fn main() {
711    std::println!("{} {}", choose(false, true), choose(true, true));
712}
713"#;
714        let transformed =
715            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
716        let runtime =
717            render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
718        let directory = temporary_directory("no-std");
719        let input = directory.join("main.rs");
720        let binary = directory.join("program");
721        let evidence = directory.join("evidence");
722        fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
723        let compile = Command::new("rustc")
724            .arg("--edition=2024")
725            .arg(&input)
726            .arg("-o")
727            .arg(&binary)
728            .output()
729            .unwrap();
730        assert!(
731            compile.status.success(),
732            "{}",
733            String::from_utf8_lossy(&compile.stderr)
734        );
735        let output = Command::new(&binary)
736            .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
737            .output()
738            .unwrap();
739        assert!(output.status.success());
740        assert_eq!(output.stdout, b"3 7\n");
741        // Probes must still record, not merely compile.
742        let files = read_rust_probe_directory(&evidence).unwrap();
743        assert_eq!(files.len(), 1);
744        assert!(!files.values().next().unwrap().is_empty());
745        fs::remove_dir_all(directory).unwrap();
746    }
747
748    #[test]
749    fn probes_reached_through_a_global_allocator_do_not_recurse() {
750        // The shape from bytes-1.12.1 tests/test_bytes_vec_alloc.rs: the
751        // allocator's `alloc` calls an inherent method, which calls a FREE
752        // FUNCTION. Skipping `impl GlobalAlloc` blocks syntactically does not
753        // cover `note`, and nothing syntactic can -- the chain may leave the
754        // file or the crate. Only the runtime knows a probe is already running,
755        // so the guard has to live there. Without it this binary dies with
756        // SIGSEGV instead of printing anything.
757        let source = r#"use std::alloc::{GlobalAlloc, Layout, System};
758use std::sync::atomic::{AtomicUsize, Ordering};
759
760static SEEN: AtomicUsize = AtomicUsize::new(0);
761
762fn note(size: usize) {
763    if size > 0 {
764        SEEN.fetch_add(1, Ordering::SeqCst);
765    }
766}
767
768struct Ledger;
769
770impl Ledger {
771    fn record(&self, size: usize) {
772        note(size);
773    }
774}
775
776unsafe impl GlobalAlloc for Ledger {
777    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
778        self.record(layout.size());
779        System.alloc(layout)
780    }
781
782    unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
783        // dealloc must be instrumented too, or the test never exercises the
784        // ladder that actually crashed: freeing the probe's OWN buffer re-enters
785        // here, and a guard released before that free recurses without bound.
786        self.record(layout.size());
787        System.dealloc(pointer, layout);
788    }
789}
790
791#[global_allocator]
792static LEDGER: Ledger = Ledger;
793
794fn classify(flag: bool) -> usize {
795    if flag { 1 } else { 2 }
796}
797
798fn main() {
799    let held = std::vec![7u8; 32];
800    println!("{} {}", classify(!held.is_empty()), held.len());
801}
802"#;
803        let transformed =
804            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
805        // `note` is a free function, so it IS instrumented -- proving the guard,
806        // not a syntactic skip, is what prevents the recursion.
807        assert!(
808            transformed
809                .code
810                .contains("fn note(size: usize) {\ncrate::__supercov_runtime_v1::hit("),
811            "the free function reached from the allocator should still be probed"
812        );
813        let runtime =
814            render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
815        let directory = temporary_directory("allocator-reentry");
816        let input = directory.join("main.rs");
817        let binary = directory.join("program");
818        let evidence = directory.join("evidence");
819        fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
820        let compile = Command::new("rustc")
821            .arg("--edition=2024")
822            .arg(&input)
823            .arg("-o")
824            .arg(&binary)
825            .output()
826            .unwrap();
827        assert!(
828            compile.status.success(),
829            "{}",
830            String::from_utf8_lossy(&compile.stderr)
831        );
832        let output = Command::new(&binary)
833            .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
834            .output()
835            .unwrap();
836        assert!(
837            output.status.success(),
838            "instrumented allocator did not survive: {:?}",
839            output.status
840        );
841        assert_eq!(output.stdout, b"1 32\n");
842        let files = read_rust_probe_directory(&evidence).unwrap();
843        let observations = files.values().next().unwrap();
844        let decisions = observations
845            .iter()
846            .filter_map(|observation| match observation {
847                RustProbeObservation::Decision {
848                    id,
849                    values,
850                    outcome,
851                } => Some((id.clone(), values.clone(), *outcome)),
852                RustProbeObservation::Hit { .. } => None,
853            })
854            .collect::<Vec<_>>();
855        // Suppression costs only duplicates: `classify` runs outside any probe,
856        // so its decision is still recorded exactly.
857        let classify = transformed
858            .manifest
859            .decisions
860            .iter()
861            .find(|decision| decision.source == "flag")
862            .expect("classify's decision reached the manifest");
863        assert!(
864            decisions
865                .iter()
866                .any(|(id, values, outcome)| id == &classify.id
867                    && values == &[Some(true)]
868                    && *outcome),
869            "classify's decision was lost: {decisions:?}"
870        );
871        // `note` records too -- every ordinary allocation reaches it outside a
872        // probe -- which is why suppressing the nested ones loses nothing.
873        let note = transformed
874            .manifest
875            .decisions
876            .iter()
877            .find(|decision| decision.source == "size > 0")
878            .expect("note's decision reached the manifest");
879        assert!(decisions.iter().any(|(id, ..)| id == &note.id));
880        // No frame built while nested may reach the log: a zero-width vector
881        // for a one-condition decision is a malformed record, not a lost one.
882        assert!(
883            decisions.iter().all(|(_, values, _)| values.len() == 1),
884            "a suppressed frame emitted a malformed vector: {decisions:?}"
885        );
886        fs::remove_dir_all(directory).unwrap();
887    }
888
889    #[test]
890    fn a_hit_in_a_loop_is_written_once_but_decisions_keep_every_vector() {
891        // bytes' advance_bytes_mut_remaining_capacity is a triple-nested loop of
892        // ~2.8M iterations. One write syscall per probe per iteration left it
893        // still running after four minutes. A hit only answers "did this ever
894        // run", so the repeats carry nothing -- but a decision's condition
895        // vector differs per iteration and every distinct one must survive.
896        let source = r#"fn step(value: usize) -> bool {
897    let doubled = value * 2;
898    doubled > 4
899}
900
901fn main() {
902    let mut seen = 0;
903    for value in 0..64 {
904        if step(value) { seen += 1; }
905    }
906    println!("{seen}");
907}
908"#;
909        let transformed =
910            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
911        let runtime =
912            render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
913        let directory = temporary_directory("dedup");
914        let input = directory.join("main.rs");
915        let binary = directory.join("program");
916        let evidence = directory.join("evidence");
917        fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
918        let compile = Command::new("rustc")
919            .arg("--edition=2024")
920            .arg(&input)
921            .arg("-o")
922            .arg(&binary)
923            .output()
924            .unwrap();
925        assert!(
926            compile.status.success(),
927            "{}",
928            String::from_utf8_lossy(&compile.stderr)
929        );
930        let output = Command::new(&binary)
931            .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
932            .output()
933            .unwrap();
934        assert_eq!(output.stdout, b"61\n");
935        let files = read_rust_probe_directory(&evidence).unwrap();
936        let observations = files.values().next().unwrap();
937
938        // `doubled > 4` runs 64 times; its hit is recorded once.
939        let mut hits = BTreeMap::<&str, usize>::new();
940        for observation in observations {
941            if let RustProbeObservation::Hit { id } = observation {
942                *hits.entry(id.as_str()).or_default() += 1;
943            }
944        }
945        assert!(!hits.is_empty(), "no hits recorded at all");
946        assert!(
947            hits.values().all(|count| *count == 1),
948            "a repeated hit was written more than once: {hits:?}"
949        );
950
951        // MC/DC needs the SET of condition vectors, not how often each recurred,
952        // so a repeat of an already-seen vector carries nothing -- but every
953        // DISTINCT vector must still arrive. `doubled > 4` is evaluated 64 times
954        // and takes exactly two distinct shapes, so exactly two records survive.
955        let decisions = observations
956            .iter()
957            .filter_map(|observation| match observation {
958                RustProbeObservation::Decision {
959                    values, outcome, ..
960                } => Some((values.clone(), *outcome)),
961                RustProbeObservation::Hit { .. } => None,
962            })
963            .collect::<Vec<_>>();
964        assert_eq!(
965            decisions,
966            // In first-occurrence order: `step(0)` is false before any value
967            // exceeds the threshold.
968            [(vec![Some(false)], false), (vec![Some(true)], true)],
969            "both distinct vectors must survive, and neither may repeat"
970        );
971        fs::remove_dir_all(directory).unwrap();
972    }
973
974    #[test]
975    fn generated_runtime_survives_a_deny_warnings_host() {
976        // serde builds with `#![deny(warnings)]`; the injected module's
977        // fully-qualified imports (required for no_std hosts) read as unused
978        // imports and became hard errors. Injected code must be immune to the
979        // host's lint policy.
980        let source = r#"#![deny(warnings)]
981
982fn choose(first: bool, second: bool) -> i32 {
983    if first && second { 7 } else { 3 }
984}
985
986fn main() {
987    println!("{} {}", choose(false, true), choose(true, true));
988}
989"#;
990        let transformed =
991            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
992        let runtime =
993            render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
994        let directory = temporary_directory("deny-warnings");
995        let input = directory.join("main.rs");
996        let binary = directory.join("program");
997        fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
998        // --cap-lints=warn mirrors what the runner passes for the instrumented
999        // workspace: the host policy must not reject generated code, including
1000        // the `if ({{ frame ... }})` decision wrapping that trips unused_parens.
1001        let compile = Command::new("rustc")
1002            .arg("--edition=2024")
1003            .arg("--cap-lints=warn")
1004            .arg(&input)
1005            .arg("-o")
1006            .arg(&binary)
1007            .output()
1008            .unwrap();
1009        assert!(
1010            compile.status.success(),
1011            "{}",
1012            String::from_utf8_lossy(&compile.stderr)
1013        );
1014        let output = Command::new(&binary).output().unwrap();
1015        assert_eq!(output.stdout, b"3 7\n");
1016        fs::remove_dir_all(directory).unwrap();
1017    }
1018
1019    #[test]
1020    fn reader_rejects_truncation_invalid_digits_and_non_files() {
1021        assert_eq!(
1022            parse_rust_probe_events(
1023                b"SUPERCOV-RUST-PROBE-1\nD\trs:decision:0123456789abcdef01234567\t03\t1\n"
1024            ),
1025            Err(RustProbeReadError::InvalidRecord(2))
1026        );
1027        assert_eq!(
1028            parse_rust_probe_events(b"SUPERCOV-RUST-PROBE-"),
1029            Err(RustProbeReadError::InvalidHeader)
1030        );
1031
1032        let directory = temporary_directory("unsafe");
1033        fs::create_dir(directory.join("nested.events")).unwrap();
1034        assert!(matches!(
1035            read_rust_probe_directory(&directory),
1036            Err(RustProbeReadError::UnsafeEntry(_))
1037        ));
1038        fs::remove_dir_all(directory).unwrap();
1039    }
1040}