Skip to main content

supercov_engine/
python_evidence.rs

1//! Validation and normalization of the Python runtime's evidence records.
2//!
3//! Each Supercov-hooked interpreter publishes commit-framed JSON records into
4//! its own mmap: the process identity, every phase it entered (with the exact
5//! test identity that phase stands for), runner outcomes, hits, decision
6//! vectors and any measurement limitation the runtime detected. The one-byte
7//! commit marker is written last, so records completed before a hard kill
8//! remain readable while a torn tail stays inert.
9//!
10//! Probes do not write records: they store bytes into their context's slot, a
11//! small mapped file beside the transport, and the runtime harvests a slot into
12//! records when a phase ends. A process that dies between harvests leaves its
13//! slots behind; `layout.json`, written once per run, says which obligation or
14//! decision vector each byte stands for, and the slot's bytes are read as the
15//! last records of its process. Rust joins those records into the shared
16//! frontend protocol; the runtime never computes a verdict.
17
18use std::{
19    collections::{BTreeMap, BTreeSet},
20    fs::{self, File},
21    path::{Component, Path, PathBuf},
22};
23
24use memmap2::{Mmap, MmapOptions};
25use serde::Deserialize;
26use serde_json::json;
27use sha2::{Digest, Sha256};
28use supercov_contracts::{
29    AttributionPrecision, ExecutionModel, FrontendAttribution, FrontendLimitation,
30    FrontendLimitationScope, FrontendRunDeclaration, FrontendRunnerDeclaration,
31    LANGUAGE_FRONTEND_PROTOCOL_VERSION, StructuralSource,
32};
33
34use crate::{
35    coverage_analysis::McdcVector,
36    coverage_report::{
37        CoverageManifest, CoverageModelDeclaration, CoveragePhase, CoverageReportRequest,
38        DecisionMeta, DecisionSnapshot, ExecutionScope, ExitCodeInput, PersistedCoverageModel,
39        RawTestResult, RuntimeSnapshot, TestProvenance,
40    },
41    evidence_archive::EvidenceArchiveEntry,
42};
43
44pub const PYTHON_EVIDENCE_VERSION: u32 = 1;
45pub const PYTHON_FRONTEND_VERSION: &str = "python-probes-v1";
46pub const PYTEST_RUNNER: &str = "pytest";
47pub const UNITTEST_RUNNER: &str = "unittest";
48
49const TRANSPORT_MAGIC: &[u8; 8] = b"SCVPYTH1";
50/// Version 2 checksums records with CRC-32; version 1 used FNV-1a, and the
51/// runtime computed it a byte at a time in Python -- two microseconds of every
52/// record's five. Evidence kept from a failed run by an older runtime still
53/// reads, by its own checksum.
54const TRANSPORT_VERSION: u32 = 2;
55const TRANSPORT_VERSION_FNV: u32 = 1;
56const TRANSPORT_HEADER_SIZE: usize = 64;
57const TRANSPORT_RECORD_HEADER_SIZE: usize = 16;
58const TRANSPORT_MAX_RECORD_SIZE: usize = 4 * 1024 * 1024;
59
60fn default_runner() -> String {
61    PYTEST_RUNNER.into()
62}
63
64fn python_runner(runner: &str) -> bool {
65    matches!(runner, PYTEST_RUNNER | UNITTEST_RUNNER)
66}
67
68/// Every field the runtime writes is named so `deny_unknown_fields` keeps
69/// the record shape frozen, even where Rust does not read the value yet.
70#[derive(Debug, Deserialize)]
71#[allow(dead_code)]
72#[serde(tag = "t", rename_all = "lowercase", deny_unknown_fields)]
73enum Record {
74    Process {
75        v: u32,
76        run: String,
77        pid: u64,
78        worker: String,
79        python: String,
80        executable: String,
81        argv: Vec<String>,
82        /// Which observer measured the process. Probes are the only one
83        /// now; the field stays so evidence kept by an earlier runtime reads.
84        #[serde(default)]
85        frontend: Option<String>,
86    },
87    Worker {
88        worker: String,
89    },
90    Phase {
91        ctx: u64,
92        at: i64,
93        worker: String,
94        test: String,
95        retry: usize,
96        phase: String,
97    },
98    Outcome {
99        worker: String,
100        test: String,
101        retry: usize,
102        phase: String,
103        outcome: String,
104        xfail: bool,
105        #[serde(default = "default_runner")]
106        runner: String,
107        /// Where the runner says the test is defined. Absent for adapters or
108        /// synthesised tests that cannot name a file.
109        #[serde(default)]
110        file: Option<String>,
111    },
112    Hit {
113        ctx: u64,
114        id: String,
115    },
116    /// Many hits of one context in one record: the probe frontend harvests
117    /// a context's array at the phase switch and writes what it found at
118    /// once, instead of a record per first hit.
119    Hits {
120        ctx: u64,
121        ids: Vec<String>,
122    },
123    /// Hits read out of a slot, by position in the layout's ids. Made here,
124    /// never written by the runtime.
125    #[serde(skip)]
126    SlotHits {
127        ctx: u64,
128        positions: Vec<u32>,
129    },
130    Dec {
131        ctx: u64,
132        id: String,
133        v: String,
134        o: u8,
135    },
136    /// Many decision vectors of one context: (id, digits, outcome).
137    Decs {
138        ctx: u64,
139        v: Vec<(String, String, u8)>,
140    },
141    /// A harvest of one context's slot: its set bytes as runs, each a first
142    /// byte and a length, read through the run's slot layout exactly as a
143    /// slot a killed process left behind is read. The runtime names bytes,
144    /// not ids, so a harvest costs it a few dozen runs rather than a string
145    /// per hit.
146    Runs {
147        ctx: u64,
148        r: Vec<usize>,
149    },
150    /// The first assertion of a call phase: what the context recorded before
151    /// this record is the assertion's evidence too.
152    Assert {
153        ctx: u64,
154    },
155    /// One assertion site a call phase reached, once per site per test.
156    /// `unittest` reports the caller's frame and pytest reports the line its
157    /// rewriter recorded; both are resolved against the syntax inventory, and
158    /// a frame naming no inventoried site witnesses nothing.
159    Limitation {
160        id: String,
161        reason: String,
162        #[serde(default)]
163        file: Option<String>,
164        #[serde(default)]
165        obligation: Option<String>,
166    },
167    Exit {
168        at: i64,
169    },
170}
171
172#[derive(Debug)]
173pub enum PythonEvidenceError {
174    Io(String),
175    UnsafeEntry(String),
176    InvalidRecord {
177        file: String,
178        line: usize,
179        reason: String,
180    },
181    InvalidTransport {
182        file: String,
183        reason: String,
184    },
185    DroppedRecords {
186        file: String,
187        count: u64,
188    },
189    RunMismatch {
190        expected: String,
191        actual: String,
192    },
193    UnsupportedVersion(u32),
194    UnknownContext {
195        file: String,
196        line: usize,
197        context: u64,
198    },
199    UnknownObligation(String),
200    InvalidVector {
201        id: String,
202        expected: usize,
203        actual: usize,
204    },
205    NoInterpreter,
206    NoTests,
207    UnsupportedPython(String),
208}
209
210impl std::fmt::Display for PythonEvidenceError {
211    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212        match self {
213            Self::Io(reason) => write!(formatter, "could not read Python evidence: {reason}"),
214            Self::UnsafeEntry(name) => write!(formatter, "unsafe Python evidence entry: {name}"),
215            Self::InvalidRecord { file, line, reason } => {
216                write!(formatter, "invalid Python evidence record {file}:{line}: {reason}")
217            }
218            Self::InvalidTransport { file, reason } => {
219                write!(formatter, "invalid Python evidence transport {file}: {reason}")
220            }
221            Self::DroppedRecords { file, count } => write!(
222                formatter,
223                "Python evidence transport {file} exhausted its bounded capacity and dropped {count} record(s)"
224            ),
225            Self::RunMismatch { expected, actual } => write!(
226                formatter,
227                "Python evidence belongs to run {actual}, expected {expected}"
228            ),
229            Self::UnsupportedVersion(version) => {
230                write!(formatter, "unsupported Python evidence version {version}")
231            }
232            Self::UnknownContext { file, line, context } => write!(
233                formatter,
234                "Python evidence {file}:{line} references undeclared context {context}"
235            ),
236            Self::UnknownObligation(id) => {
237                write!(formatter, "Python runtime reported an unknown obligation: {id}")
238            }
239            Self::InvalidVector {
240                id,
241                expected,
242                actual,
243            } => write!(
244                formatter,
245                "Python decision {id} reported {actual} condition values, expected {expected}"
246            ),
247            Self::NoInterpreter => formatter.write_str(
248                "no Supercov-hooked Python interpreter ran: the test command did not start CPython 3.9+ with Supercov's start-up hook (PYTHONPATH may be ignored by -I/-E/-S, or the runner is not Python)",
249            ),
250            Self::NoTests => formatter.write_str(
251                "the Python run produced no test outcomes; Supercov measures Python through pytest and unittest",
252            ),
253            Self::UnsupportedPython(version) => write!(
254                formatter,
255                "Supercov measures CPython 3.9 or newer; the test command ran Python {version}"
256            ),
257        }
258    }
259}
260
261impl std::error::Error for PythonEvidenceError {}
262
263fn stable_id(prefix: &str, values: &[&str]) -> String {
264    let mut hash = Sha256::new();
265    for value in values {
266        hash.update(value.as_bytes());
267        hash.update([0]);
268    }
269    let digest = hash.finalize();
270    let mut encoded = String::with_capacity(prefix.len() + 25);
271    encoded.push_str(prefix);
272    encoded.push(':');
273    for byte in &digest[..12] {
274        use std::fmt::Write as _;
275        write!(&mut encoded, "{byte:02x}").expect("string formatting");
276    }
277    encoded
278}
279
280#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
281struct Identity {
282    worker: String,
283    test: String,
284    retry: usize,
285    phase: String,
286}
287
288type ObservedVectors = BTreeSet<(Vec<Option<bool>>, bool)>;
289/// (worker, test, retry) -> [(phase, outcome, xfail)]
290type OutcomesByAttempt = BTreeMap<(String, String, usize), Vec<(String, String, bool)>>;
291/// (worker, test, retry) -> runner that reported the attempt
292type RunnersByAttempt = BTreeMap<(String, String, usize), String>;
293/// (worker, test, retry) -> the source file the runner named for the test
294type TestFilesByAttempt = BTreeMap<(String, String, usize), String>;
295
296/// A runtime path, project-relative. Python reports both forms: a frame's
297/// `co_filename` is whatever the interpreter loaded, absolute or relative. A
298/// path outside the project names nothing.
299fn relative(root: &Path, path: &str) -> Option<String> {
300    let candidate = Path::new(path);
301    let relative = if candidate.is_absolute() {
302        candidate.strip_prefix(root).ok()?
303    } else {
304        candidate.strip_prefix("./").unwrap_or(candidate)
305    };
306    let text = relative.to_string_lossy().replace('\\', "/");
307    (!text.is_empty() && !text.starts_with("../")).then_some(text)
308}
309
310#[derive(Debug, Default)]
311struct Observations {
312    hits: BTreeSet<String>,
313    /// Hits a harvest named by slot byte, as positions in the layout's ids,
314    /// repeats included: turned into ids once, when the phase's snapshot is
315    /// built. A subprocess-heavy run harvests millions, and an owned id per
316    /// hit was most of reading its evidence.
317    slot_hits: Vec<u32>,
318    vectors: BTreeMap<String, ObservedVectors>,
319}
320
321impl Observations {
322    fn is_empty(&self) -> bool {
323        self.hits.is_empty() && self.slot_hits.is_empty() && self.vectors.is_empty()
324    }
325}
326
327#[derive(Debug, Clone)]
328struct RuntimeLimitation {
329    id: String,
330    reason: String,
331    file: Option<String>,
332    obligation: Option<String>,
333}
334
335#[derive(Debug, Default)]
336struct Evidence {
337    interpreters: usize,
338    python_versions: BTreeSet<String>,
339    per_identity: BTreeMap<Identity, Observations>,
340    background: BTreeMap<String, Observations>,
341    outcomes: OutcomesByAttempt,
342    runners: RunnersByAttempt,
343    test_files: TestFilesByAttempt,
344    limitations: BTreeMap<(String, Option<String>, Option<String>), RuntimeLimitation>,
345    /// The layout's ids, which slot hits are positions in.
346    slot_ids: Vec<String>,
347    /// Each request a server answered that named no test: its identity, when
348    /// it came, where in its process's records, that process's transport, and
349    /// its worker.
350    requests: Vec<(Identity, i64, usize, String, String)>,
351    /// By transport, the browser test attempts a process answered a request
352    /// of.
353    served_by: BTreeMap<String, BTreeSet<(String, String, usize)>>,
354    /// When each of the suite's own test phases started, and in which
355    /// process.
356    phase_starts: Vec<crate::served_tests::PhaseStart>,
357}
358
359use crate::served_tests::REQUEST_WORKER;
360
361fn read_evidence_directory(
362    directory: &Path,
363    run_id: &str,
364) -> Result<Evidence, PythonEvidenceError> {
365    let mut evidence = Evidence::default();
366    let mut files = match fs::read_dir(directory) {
367        Ok(entries) => entries
368            .collect::<Result<Vec<_>, _>>()
369            .map_err(|error| PythonEvidenceError::Io(error.to_string()))?,
370        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Vec::new(),
371        Err(error) => return Err(PythonEvidenceError::Io(error.to_string())),
372    };
373    files.sort_by_key(|entry| entry.file_name());
374    let mut transports = Vec::new();
375    let mut slots = BTreeMap::<String, Vec<(String, PathBuf)>>::new();
376    let mut layout_path = None;
377    for entry in files {
378        let name = entry
379            .file_name()
380            .into_string()
381            .map_err(|_| PythonEvidenceError::UnsafeEntry("<non-utf8>".into()))?;
382        if Path::new(&name)
383            .components()
384            .any(|component| !matches!(component, Component::Normal(_)))
385        {
386            return Err(PythonEvidenceError::UnsafeEntry(name));
387        }
388        // A layout write cut short by the process dying; another process of
389        // the run, or none, published the whole file.
390        if name.starts_with(SLOT_LAYOUT_NAME) && name.ends_with(".partial") {
391            continue;
392        }
393        let Some(metadata) = entry_metadata(&name, &entry.path())? else {
394            continue;
395        };
396        if !metadata.file_type().is_file() {
397            return Err(PythonEvidenceError::UnsafeEntry(name));
398        }
399        if name == SLOT_LAYOUT_NAME {
400            layout_path = Some(entry.path());
401        } else if name.ends_with(".mmap") {
402            transports.push((name, entry.path()));
403        } else if let Some((slot_name, transport)) = name.strip_suffix(".shm").and_then(|stem| {
404            let (transport, number) = stem.rsplit_once('.')?;
405            number
406                .parse::<u64>()
407                .ok()
408                .map(|_| (format!("{stem}.slot"), format!("{transport}.mmap")))
409        }) {
410            if let Some(path) = materialize_shared_slot(&entry.path(), &slot_name)? {
411                slots.entry(transport).or_default().push((slot_name, path));
412            }
413        } else if let Some(transport) = name
414            .strip_suffix(".slot")
415            .and_then(|stem| stem.rsplit_once('.'))
416            .filter(|(_, number)| number.parse::<u64>().is_ok())
417            .map(|(transport, _)| format!("{transport}.mmap"))
418        {
419            // Created and killed before it was sized: nothing was written.
420            if metadata.len() > 0 {
421                slots
422                    .entry(transport)
423                    .or_default()
424                    .push((name, entry.path()));
425            }
426        } else {
427            return Err(PythonEvidenceError::UnsafeEntry(name));
428        }
429    }
430    let layout = match layout_path {
431        Some(path) => Some(SlotLayout::read(&path)?),
432        None => None,
433    };
434    // Each process's transport and slots are read and parsed on their own --
435    // a subprocess-heavy suite leaves tens of thousands -- and applied to the
436    // run's evidence in the order they are listed, as they were one by one.
437    let work = transports
438        .into_iter()
439        .map(|(name, path)| {
440            let owned = slots.remove(&name).unwrap_or_default();
441            (name, path, owned)
442        })
443        .collect::<Vec<_>>();
444    let parse = |(name, path, owned): &(String, PathBuf, Vec<(String, PathBuf)>)| {
445        let contents = map_evidence(path)?;
446        let slot_contents = owned
447            .iter()
448            .filter_map(|(slot, path)| match map_slot(path) {
449                Ok(Some(contents)) => Some(Ok((slot.clone(), contents))),
450                Ok(None) => None,
451                Err(error) => Some(Err(error)),
452            })
453            .collect::<Result<Vec<_>, _>>()?;
454        parse_transport(name, &contents, &slot_contents, layout.as_ref())
455    };
456    let parsed = parallel_map(&work, parse);
457    for ((name, _, _), parsed) in work.iter().zip(parsed) {
458        apply_transport(name, parsed?, run_id, &mut evidence)?;
459    }
460    evidence.slot_ids = layout.map(|layout| layout.ids).unwrap_or_default();
461    if let Some((_, orphans)) = slots.into_iter().next() {
462        return Err(PythonEvidenceError::InvalidTransport {
463            file: orphans[0].0.clone(),
464            reason: "a slot was left without its process's transport".into(),
465        });
466    }
467    Ok(evidence)
468}
469
470/// An evidence entry's metadata, or None for a slot gone since the listing.
471///
472/// A process that outlived the test command -- multiprocessing's resource
473/// tracker is one -- can close between the listing and this read. It harvested
474/// its slot into its transport first, so a slot that is gone holds nothing to
475/// miss. Anything else missing is still an error.
476fn entry_metadata(name: &str, path: &Path) -> Result<Option<fs::Metadata>, PythonEvidenceError> {
477    match fs::symlink_metadata(path) {
478        Ok(metadata) => Ok(Some(metadata)),
479        Err(error) if error.kind() == std::io::ErrorKind::NotFound && name.ends_with(".slot") => {
480            Ok(None)
481        }
482        Err(error) => Err(PythonEvidenceError::Io(error.to_string())),
483    }
484}
485
486/// A slot a macOS process kept in POSIX shared memory, which `marker` names,
487/// copied into an ordinary slot file beside it and then removed: evidence kept
488/// after a failed publication must hold what it held, and nothing else frees
489/// an object whose process was killed. None when there is nothing to read --
490/// the process closed and removed it, died before creating it, or its bytes
491/// were copied out by an earlier read, whose slot file the listing holds.
492fn materialize_shared_slot(
493    marker: &Path,
494    slot_name: &str,
495) -> Result<Option<PathBuf>, PythonEvidenceError> {
496    let slot = marker.with_file_name(slot_name);
497    if fs::symlink_metadata(&slot).is_ok() {
498        return Ok(None);
499    }
500    let contents = match fs::read(marker) {
501        Ok(contents) => contents,
502        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
503        Err(error) => return Err(PythonEvidenceError::Io(error.to_string())),
504    };
505    // Killed between creating the marker and writing the name into it.
506    if contents.is_empty() {
507        return Ok(None);
508    }
509    // `<name> <slot bytes>`: macOS rounds the object itself up to a page.
510    let unsafe_marker = || PythonEvidenceError::UnsafeEntry(marker.to_string_lossy().into_owned());
511    let (name, size) = std::str::from_utf8(&contents)
512        .ok()
513        .and_then(|text| text.split_once(' '))
514        .and_then(|(name, size)| Some((name.as_bytes(), size.parse::<usize>().ok()?)))
515        .ok_or_else(unsafe_marker)?;
516    if name.len() > 31
517        || name.first() != Some(&b'/')
518        || !name[1..]
519            .iter()
520            .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'.')
521    {
522        return Err(unsafe_marker());
523    }
524    shared_slot_bytes(name)?
525        // Created and killed before it was sized: nothing was written.
526        .filter(|bytes| !bytes.is_empty())
527        .map(|mut bytes| {
528            if bytes.len() < size {
529                return Err(PythonEvidenceError::InvalidTransport {
530                    file: slot_name.into(),
531                    reason: "a shared slot is smaller than its process sized it".into(),
532                });
533            }
534            bytes.truncate(size);
535            let partial = marker.with_file_name(format!("{slot_name}.partial"));
536            fs::write(&partial, bytes)
537                .and_then(|()| fs::rename(&partial, &slot))
538                .map_err(|error| PythonEvidenceError::Io(error.to_string()))?;
539            Ok(slot)
540        })
541        .transpose()
542}
543
544/// Only the macOS runtime keeps slots in shared memory.
545#[cfg(target_os = "macos")]
546fn shared_slot_bytes(name: &[u8]) -> Result<Option<Vec<u8>>, PythonEvidenceError> {
547    use std::os::fd::FromRawFd;
548    let name = std::ffi::CString::new(name)
549        .map_err(|_| PythonEvidenceError::UnsafeEntry("<shared slot name>".into()))?;
550    // SAFETY: a NUL-terminated name; the descriptor is owned by `file` below.
551    let descriptor = unsafe { libc::shm_open(name.as_ptr(), libc::O_RDONLY) };
552    if descriptor < 0 {
553        let error = std::io::Error::last_os_error();
554        return if error.kind() == std::io::ErrorKind::NotFound {
555            Ok(None)
556        } else {
557            Err(PythonEvidenceError::Io(error.to_string()))
558        };
559    }
560    // SAFETY: shm_open returned a fresh descriptor this function owns.
561    let file = unsafe { File::from_raw_fd(descriptor) };
562    let length = file
563        .metadata()
564        .map_err(|error| PythonEvidenceError::Io(error.to_string()))?
565        .len();
566    let bytes = if length == 0 {
567        Vec::new()
568    } else {
569        // SAFETY: read-only; the process that wrote it has stopped measuring.
570        unsafe { MmapOptions::new().map(&file) }
571            .map_err(|error| PythonEvidenceError::Io(error.to_string()))?
572            .to_vec()
573    };
574    // SAFETY: the same NUL-terminated name.
575    unsafe { libc::shm_unlink(name.as_ptr()) };
576    Ok(Some(bytes))
577}
578
579#[cfg(not(target_os = "macos"))]
580fn shared_slot_bytes(_name: &[u8]) -> Result<Option<Vec<u8>>, PythonEvidenceError> {
581    Ok(None)
582}
583
584/// A slot, or None when it is gone by the time it is opened: taken back by a
585/// process that closed after the directory was listed, and so already
586/// harvested into its transport.
587fn map_slot(path: &Path) -> Result<Option<Mmap>, PythonEvidenceError> {
588    match File::open(path) {
589        Ok(file) => unsafe { MmapOptions::new().map(&file) }
590            .map(Some)
591            .map_err(|error| PythonEvidenceError::Io(error.to_string())),
592        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
593        Err(error) => Err(PythonEvidenceError::Io(error.to_string())),
594    }
595}
596
597fn map_evidence(path: &Path) -> Result<Mmap, PythonEvidenceError> {
598    let file = File::open(path).map_err(|error| PythonEvidenceError::Io(error.to_string()))?;
599    // The file is immutable from Supercov's perspective after the wrapped
600    // interpreter has exited. No mutable alias is created while this map is
601    // alive.
602    unsafe { MmapOptions::new().map(&file) }
603        .map_err(|error| PythonEvidenceError::Io(error.to_string()))
604}
605
606const SLOT_LAYOUT_NAME: &str = "layout.json";
607/// The widest decision the runtime gives a region, as `supercov_probes`
608/// declares it; a wider one records its vectors directly.
609const SLOT_MAX_REGION_WIDTH: usize = 6;
610
611/// What each byte of a slot stands for: `SlotLayout::header` bytes naming the
612/// context and the layout, one byte per obligation, then a region per decision.
613#[derive(Debug, Deserialize)]
614#[serde(rename_all = "camelCase", deny_unknown_fields)]
615struct SlotLayout {
616    #[allow(dead_code)]
617    version: u32,
618    digest: String,
619    header: usize,
620    bytes: usize,
621    ids: Vec<String>,
622    decisions: Vec<SlotDecision>,
623    /// A byte that stands for others besides its own: the probe of a
624    /// statement heading a function, loop body or one-condition branch is
625    /// the only store its entry, `entered` or outcome gets.
626    #[serde(default)]
627    implied: Vec<(usize, Vec<usize>)>,
628    #[serde(skip)]
629    implied_by: std::collections::HashMap<usize, Vec<usize>>,
630}
631
632#[derive(Debug, Deserialize)]
633#[serde(rename_all = "camelCase", deny_unknown_fields)]
634struct SlotDecision {
635    start: usize,
636    width: usize,
637    id: String,
638    outcome_true: String,
639    outcome_false: String,
640    logical: Vec<SlotLogical>,
641}
642
643#[derive(Debug, Deserialize)]
644#[serde(rename_all = "camelCase", deny_unknown_fields)]
645struct SlotLogical {
646    evaluated: String,
647    short_circuit: String,
648    previous_leaves: Vec<usize>,
649    operand_leaves: Vec<usize>,
650}
651
652impl SlotDecision {
653    /// A single condition has a byte per truth; a wider decision a byte per
654    /// evaluation mask and outcome, the mask one base-3 digit per condition.
655    fn region_bytes(&self) -> usize {
656        if self.width == 1 {
657            2
658        } else {
659            2 * 3_usize.pow(self.width as u32)
660        }
661    }
662
663    /// The vector a region byte stands for, as the evidence spells it.
664    fn vector(&self, offset: usize) -> (String, bool) {
665        if self.width == 1 {
666            let outcome = offset == 1;
667            return ((if outcome { "2" } else { "1" }).into(), outcome);
668        }
669        let mut mask = offset >> 1;
670        let digits = (0..self.width)
671            .map(|_| {
672                let digit = b"012"[mask % 3] as char;
673                mask /= 3;
674                digit
675            })
676            .collect();
677        (digits, offset & 1 == 1)
678    }
679
680    /// What the runtime's harvest adds for a vector: the outcome hit, and
681    /// each logical operator's alternative the vector decided.
682    fn implied(&self, digits: &str, outcome: bool, into: &mut Vec<String>) {
683        into.push(
684            if outcome {
685                &self.outcome_true
686            } else {
687                &self.outcome_false
688            }
689            .clone(),
690        );
691        let evaluated = |leaves: &[usize]| {
692            leaves.iter().any(|index| {
693                digits
694                    .as_bytes()
695                    .get(*index)
696                    .is_some_and(|digit| *digit != b'0')
697            })
698        };
699        for logical in &self.logical {
700            if evaluated(&logical.operand_leaves) {
701                into.push(logical.evaluated.clone());
702            } else if evaluated(&logical.previous_leaves) {
703                into.push(logical.short_circuit.clone());
704            }
705        }
706    }
707}
708
709impl SlotLayout {
710    fn read(path: &Path) -> Result<Self, PythonEvidenceError> {
711        let invalid = |reason: String| PythonEvidenceError::InvalidTransport {
712            file: SLOT_LAYOUT_NAME.into(),
713            reason,
714        };
715        let bytes = fs::read(path).map_err(|error| PythonEvidenceError::Io(error.to_string()))?;
716        let mut layout: Self =
717            serde_json::from_slice(&bytes).map_err(|error| invalid(error.to_string()))?;
718        if layout.digest.len() < 16 || !layout.digest.bytes().all(|byte| byte.is_ascii_hexdigit()) {
719            return Err(invalid("the layout digest is not hex".into()));
720        }
721        // Regions follow the obligations back to back, in slot order, and end
722        // where the slot does: a byte stands for exactly one thing.
723        let mut cursor = layout.header + layout.ids.len();
724        for decision in &layout.decisions {
725            if decision.start != cursor
726                || decision.width == 0
727                || decision.width > SLOT_MAX_REGION_WIDTH
728            {
729                return Err(invalid(format!(
730                    "decision {} has no valid region",
731                    decision.id
732                )));
733            }
734            if decision
735                .logical
736                .iter()
737                .flat_map(|logical| {
738                    logical
739                        .previous_leaves
740                        .iter()
741                        .chain(&logical.operand_leaves)
742                })
743                .any(|leaf| *leaf >= decision.width)
744            {
745                return Err(invalid(format!(
746                    "decision {} names a condition it lacks",
747                    decision.id
748                )));
749            }
750            cursor += decision.region_bytes();
751        }
752        if cursor != layout.bytes || layout.header < 16 {
753            return Err(invalid("the regions do not fill the slot".into()));
754        }
755        // A head is a statement's byte; what it implies is an obligation's
756        // byte or a region's, never another head.
757        let id_end = layout.header + layout.ids.len();
758        for (head, implied) in &layout.implied {
759            if *head < layout.header
760                || *head >= id_end
761                || implied
762                    .iter()
763                    .any(|byte| *byte < layout.header || *byte >= layout.bytes || *byte == *head)
764            {
765                return Err(invalid(format!(
766                    "byte {head} implies bytes outside the slot"
767                )));
768            }
769        }
770        layout.implied_by = layout.implied.iter().cloned().collect();
771        if layout
772            .implied_by
773            .values()
774            .flatten()
775            .any(|byte| layout.implied_by.contains_key(byte))
776        {
777            return Err(invalid("an implied byte implies others".into()));
778        }
779        Ok(layout)
780    }
781
782    fn tag(&self) -> Vec<u8> {
783        (0..8)
784            .map(|index| {
785                u8::from_str_radix(&self.digest[index * 2..index * 2 + 2], 16).unwrap_or(0)
786            })
787            .collect()
788    }
789
790    /// A slot's bytes as the records a harvest would have written from them.
791    fn records(&self, name: &str, contents: &[u8]) -> Result<Vec<Record>, PythonEvidenceError> {
792        let invalid = |reason: &str| PythonEvidenceError::InvalidTransport {
793            file: name.into(),
794            reason: reason.into(),
795        };
796        let tag = contents
797            .get(8..16)
798            .ok_or_else(|| invalid("slot header is missing"))?;
799        // Its process closed it after a final harvest: whatever a thread wrote
800        // after that belongs to no test.
801        if tag.iter().all(|byte| *byte == 0) {
802            return Ok(Vec::new());
803        }
804        if tag != self.tag().as_slice() {
805            return Err(invalid("slot was written for another layout"));
806        }
807        if contents.len() != self.bytes {
808            return Err(invalid("slot size does not match the layout"));
809        }
810        let context =
811            transport_u64(contents, 0).ok_or_else(|| invalid("slot context is missing"))?;
812        let mut decoded = Decoded::default();
813        for (index, byte) in contents.iter().enumerate().skip(self.header) {
814            match byte {
815                0 => continue,
816                1 => self.decode(index, &mut decoded),
817                _ => return Err(invalid("slot byte is neither 0 nor 1")),
818            }
819        }
820        Ok(decoded.into_records(context))
821    }
822
823    /// A harvest's runs as the records a slot holding those bytes reads as.
824    fn runs(&self, context: u64, runs: &[usize]) -> Result<Vec<Record>, &'static str> {
825        if !runs.len().is_multiple_of(2) {
826            return Err("slot runs must pair a first byte with a length");
827        }
828        let mut decoded = Decoded::default();
829        for run in runs.chunks_exact(2) {
830            let (start, length) = (run[0], run[1]);
831            let end = start
832                .checked_add(length)
833                .filter(|end| length > 0 && start >= self.header && *end <= self.bytes)
834                .ok_or("a slot run lies outside the layout")?;
835            for index in start..end {
836                self.decode(index, &mut decoded);
837            }
838        }
839        Ok(decoded.into_records(context))
840    }
841
842    /// What one set byte stands for, with what the layout says it implies.
843    fn decode(&self, index: usize, into: &mut Decoded) {
844        self.decode_one(index, into);
845        if let Some(implied) = self.implied_by.get(&index) {
846            for byte in implied {
847                self.decode_one(*byte, into);
848            }
849        }
850    }
851
852    /// What one byte stands for: an obligation, or a decision vector and
853    /// what the vector implies.
854    fn decode_one(&self, index: usize, into: &mut Decoded) {
855        let id_end = self.header + self.ids.len();
856        if index < id_end {
857            into.positions.push((index - self.header) as u32);
858            return;
859        }
860        let decision = &self.decisions[self
861            .decisions
862            .partition_point(|decision| decision.start <= index)
863            - 1];
864        let (digits, outcome) = decision.vector(index - decision.start);
865        decision.implied(&digits, outcome, &mut into.ids);
866        into.vectors
867            .push((decision.id.clone(), digits, u8::from(outcome)));
868    }
869}
870
871#[derive(Default)]
872struct Decoded {
873    ids: Vec<String>,
874    positions: Vec<u32>,
875    vectors: Vec<(String, String, u8)>,
876}
877
878impl Decoded {
879    fn into_records(self, context: u64) -> Vec<Record> {
880        let mut records = Vec::new();
881        if !self.positions.is_empty() {
882            records.push(Record::SlotHits {
883                ctx: context,
884                positions: self.positions,
885            });
886        }
887        if !self.ids.is_empty() {
888            records.push(Record::Hits {
889                ctx: context,
890                ids: self.ids,
891            });
892        }
893        if !self.vectors.is_empty() {
894            records.push(Record::Decs {
895                ctx: context,
896                v: self.vectors,
897            });
898        }
899        records
900    }
901}
902
903/// `items.iter().map(work)`, spread over the machine's cores, in order.
904fn parallel_map<T: Sync, R: Send>(items: &[T], work: impl Fn(&T) -> R + Sync) -> Vec<R> {
905    let threads = std::thread::available_parallelism()
906        .map_or(1, std::num::NonZeroUsize::get)
907        .min(items.len().div_ceil(64).max(1));
908    if threads <= 1 {
909        return items.iter().map(work).collect();
910    }
911    let next = std::sync::atomic::AtomicUsize::new(0);
912    let mut done = std::thread::scope(|scope| {
913        let workers = (0..threads)
914            .map(|_| {
915                scope.spawn(|| {
916                    let mut done = Vec::new();
917                    loop {
918                        let index = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
919                        let Some(item) = items.get(index) else {
920                            break done;
921                        };
922                        done.push((index, work(item)));
923                    }
924                })
925            })
926            .collect::<Vec<_>>();
927        workers
928            .into_iter()
929            .flat_map(|worker| {
930                worker
931                    .join()
932                    .unwrap_or_else(|panic| std::panic::resume_unwind(panic))
933            })
934            .collect::<Vec<_>>()
935    });
936    done.sort_unstable_by_key(|(index, _)| *index);
937    done.into_iter().map(|(_, result)| result).collect()
938}
939
940fn transport_u32(bytes: &[u8], offset: usize) -> Option<u32> {
941    bytes
942        .get(offset..offset + 4)
943        .and_then(|value| value.try_into().ok())
944        .map(u32::from_le_bytes)
945}
946
947fn transport_u64(bytes: &[u8], offset: usize) -> Option<u64> {
948    bytes
949        .get(offset..offset + 8)
950        .and_then(|value| value.try_into().ok())
951        .map(u64::from_le_bytes)
952}
953
954/// A field of a header whose length was checked: it cannot run past it.
955fn checked_u32(bytes: &[u8], offset: usize) -> u32 {
956    transport_u32(bytes, offset).expect("the header was checked to hold this field")
957}
958
959fn checked_u64(bytes: &[u8], offset: usize) -> u64 {
960    transport_u64(bytes, offset).expect("the header was checked to hold this field")
961}
962
963fn transport_checksum(payload: &[u8], version: u32) -> u32 {
964    if version == TRANSPORT_VERSION_FNV {
965        return payload.iter().fold(0x811c_9dc5_u32, |value, byte| {
966            (value ^ u32::from(*byte)).wrapping_mul(0x0100_0193)
967        });
968    }
969    crc32fast::hash(payload)
970}
971
972fn align_transport(value: usize) -> Option<usize> {
973    value.checked_add(7).map(|value| value & !7)
974}
975
976/// A transport's records, in order, with its slots' leftovers after them:
977/// what reading one process's evidence needs nothing else of the run for,
978/// so many are parsed at once.
979struct ParsedTransport {
980    pid: u64,
981    records: Vec<(usize, Record)>,
982}
983
984fn parse_transport(
985    name: &str,
986    contents: &[u8],
987    slots: &[(String, Mmap)],
988    layout: Option<&SlotLayout>,
989) -> Result<ParsedTransport, PythonEvidenceError> {
990    let invalid_transport = |reason: &str| PythonEvidenceError::InvalidTransport {
991        file: name.into(),
992        reason: reason.into(),
993    };
994    let version = transport_u32(contents, 8);
995    if contents.len() < TRANSPORT_HEADER_SIZE
996        || contents.get(..8) != Some(TRANSPORT_MAGIC.as_slice())
997        || !matches!(version, Some(TRANSPORT_VERSION | TRANSPORT_VERSION_FNV))
998        || transport_u32(contents, 12) != Some(TRANSPORT_HEADER_SIZE as u32)
999    {
1000        return Err(invalid_transport("header or version does not match"));
1001    }
1002    let version = version.unwrap_or(TRANSPORT_VERSION);
1003    let declared_capacity = checked_u64(contents, 16);
1004    if declared_capacity < TRANSPORT_HEADER_SIZE as u64 || declared_capacity > contents.len() as u64
1005    {
1006        return Err(invalid_transport(
1007            "declared capacity is outside the mapped file",
1008        ));
1009    }
1010    let dropped = checked_u64(contents, 24);
1011    if dropped != 0 {
1012        return Err(PythonEvidenceError::DroppedRecords {
1013            file: name.into(),
1014            count: dropped,
1015        });
1016    }
1017    let transport_pid = checked_u64(contents, 32);
1018    if transport_pid == 0 {
1019        return Err(invalid_transport("process id is missing"));
1020    }
1021    let mut records = Vec::new();
1022    let mut cursor = TRANSPORT_HEADER_SIZE;
1023    let mut record_index = 0;
1024    while cursor + TRANSPORT_RECORD_HEADER_SIZE <= contents.len() {
1025        let commit = contents[cursor];
1026        if commit == 0 {
1027            // Payload bytes can exist after a killed writer, but an absent
1028            // commit byte makes that frame and every later zeroed frame inert.
1029            break;
1030        }
1031        record_index += 1;
1032        let line_number = record_index;
1033        let invalid = |reason: &str| PythonEvidenceError::InvalidRecord {
1034            file: name.into(),
1035            line: line_number,
1036            reason: reason.into(),
1037        };
1038        if commit != 1
1039            || contents[cursor + 1..cursor + 4] != [0, 0, 0]
1040            || contents[cursor + 12..cursor + 16] != [0, 0, 0, 0]
1041        {
1042            return Err(invalid("commit marker or reserved bytes are invalid"));
1043        }
1044        let length = checked_u32(contents, cursor + 4) as usize;
1045        if length == 0 || length > TRANSPORT_MAX_RECORD_SIZE {
1046            return Err(invalid("payload length is outside the transport bound"));
1047        }
1048        let payload_start = cursor + TRANSPORT_RECORD_HEADER_SIZE;
1049        let payload_end = payload_start
1050            .checked_add(length)
1051            .filter(|end| *end <= contents.len())
1052            .ok_or_else(|| invalid("payload extends past the mapped file"))?;
1053        let next_cursor = align_transport(payload_end)
1054            .filter(|end| *end <= contents.len())
1055            .ok_or_else(|| invalid("aligned frame extends past the mapped file"))?;
1056        if contents[payload_end..next_cursor]
1057            .iter()
1058            .any(|byte| *byte != 0)
1059        {
1060            return Err(invalid("frame padding is not zero"));
1061        }
1062        let payload = &contents[payload_start..payload_end];
1063        let expected_checksum = checked_u32(contents, cursor + 8);
1064        if transport_checksum(payload, version) != expected_checksum {
1065            return Err(invalid("payload checksum does not match"));
1066        }
1067        let record: Record = serde_json::from_slice(payload).map_err(|error| {
1068            PythonEvidenceError::InvalidRecord {
1069                file: name.into(),
1070                line: line_number,
1071                reason: error.to_string(),
1072            }
1073        })?;
1074        if let Record::Runs { ctx, r } = record {
1075            let layout =
1076                layout.ok_or_else(|| invalid("slot runs arrived without the run's slot layout"))?;
1077            for record in layout.runs(ctx, &r).map_err(invalid)? {
1078                records.push((line_number, record));
1079            }
1080        } else {
1081            records.push((line_number, record));
1082        }
1083        cursor = next_cursor;
1084    }
1085    // What the process's slots still held when it ended: after every record
1086    // of its transport, as the harvest it never reached would have been.
1087    for (slot, slot_contents) in slots {
1088        let layout = layout.ok_or_else(|| PythonEvidenceError::InvalidTransport {
1089            file: slot.clone(),
1090            reason: "a slot was left without the run's slot layout".into(),
1091        })?;
1092        for record in layout.records(slot, slot_contents)? {
1093            record_index += 1;
1094            records.push((record_index, record));
1095        }
1096    }
1097    Ok(ParsedTransport {
1098        pid: transport_pid,
1099        records,
1100    })
1101}
1102
1103/// A parsed transport's records applied to the run's evidence, in order.
1104fn apply_transport(
1105    name: &str,
1106    parsed: ParsedTransport,
1107    run_id: &str,
1108    evidence: &mut Evidence,
1109) -> Result<(), PythonEvidenceError> {
1110    let transport_pid = parsed.pid;
1111    let mut contexts = BTreeMap::<u64, Identity>::new();
1112    // What each call phase recorded so far, kept until its first assertion
1113    // marker moves it to the phase's assertion identity.
1114    let mut before_assertion = BTreeMap::<u64, Observations>::new();
1115    let mut process_worker: Option<String> = None;
1116    let mut apply = |line_number: usize, record: Record| -> Result<(), PythonEvidenceError> {
1117        let invalid = |reason: &str| PythonEvidenceError::InvalidRecord {
1118            file: name.into(),
1119            line: line_number,
1120            reason: reason.into(),
1121        };
1122        match record {
1123            Record::Process {
1124                v,
1125                run,
1126                pid,
1127                worker,
1128                python,
1129                ..
1130            } => {
1131                if v != PYTHON_EVIDENCE_VERSION {
1132                    return Err(PythonEvidenceError::UnsupportedVersion(v));
1133                }
1134                if run != run_id {
1135                    return Err(PythonEvidenceError::RunMismatch {
1136                        expected: run_id.into(),
1137                        actual: run,
1138                    });
1139                }
1140                if pid != transport_pid {
1141                    return Err(invalid("process record does not match the transport owner"));
1142                }
1143                let supported = python
1144                    .split('.')
1145                    .take(2)
1146                    .map(|part| part.parse::<u32>().ok())
1147                    .collect::<Option<Vec<_>>>()
1148                    // The runtime measures 3.9 and newer and refuses to
1149                    // install on anything older.
1150                    .is_some_and(|parts| parts.len() == 2 && (parts[0], parts[1]) >= (3, 9));
1151                if !supported {
1152                    return Err(PythonEvidenceError::UnsupportedPython(python));
1153                }
1154                evidence.interpreters += 1;
1155                evidence.python_versions.insert(python);
1156                process_worker = Some(worker);
1157            }
1158            Record::Worker { worker } => process_worker = Some(worker),
1159            Record::Phase {
1160                ctx,
1161                at,
1162                worker,
1163                test,
1164                retry,
1165                phase,
1166            } => {
1167                if ctx == 0 {
1168                    return Err(invalid("phase context 0 is reserved for background"));
1169                }
1170                if !matches!(phase.as_str(), "setup" | "call" | "teardown") {
1171                    return Err(invalid("unknown pytest phase"));
1172                }
1173                if test.trim().is_empty() || worker.trim().is_empty() {
1174                    return Err(invalid("phase identity must name a worker and test"));
1175                }
1176                // A request is numbered within its process, which its
1177                // transport names.
1178                let worker = if worker == REQUEST_WORKER {
1179                    format!("{REQUEST_WORKER}:{name}")
1180                } else {
1181                    worker
1182                };
1183                let identity = Identity {
1184                    worker,
1185                    test,
1186                    retry,
1187                    phase,
1188                };
1189                if identity.worker.starts_with(REQUEST_WORKER) {
1190                    evidence.requests.push((
1191                        identity.clone(),
1192                        at,
1193                        line_number,
1194                        name.to_owned(),
1195                        process_worker.clone().unwrap_or_else(|| "main".into()),
1196                    ));
1197                } else {
1198                    if identity.phase == "call" {
1199                        before_assertion.insert(ctx, Observations::default());
1200                    }
1201                    let attempt = (
1202                        identity.worker.clone(),
1203                        identity.test.clone(),
1204                        identity.retry,
1205                    );
1206                    evidence
1207                        .phase_starts
1208                        .push((name.to_owned(), line_number, attempt.clone()));
1209                    evidence
1210                        .served_by
1211                        .entry(name.to_owned())
1212                        .or_default()
1213                        .insert(attempt);
1214                }
1215                contexts.insert(ctx, identity);
1216            }
1217            Record::Outcome {
1218                worker,
1219                test,
1220                retry,
1221                phase,
1222                outcome,
1223                xfail,
1224                runner,
1225                file,
1226            } => {
1227                if !matches!(phase.as_str(), "setup" | "call" | "teardown") {
1228                    return Err(invalid("unknown test outcome phase"));
1229                }
1230                if !matches!(
1231                    outcome.as_str(),
1232                    "passed" | "failed" | "skipped" | "rerun" | "error"
1233                ) {
1234                    return Err(invalid("unknown test outcome"));
1235                }
1236                if !matches!(runner.as_str(), PYTEST_RUNNER | UNITTEST_RUNNER) {
1237                    return Err(invalid("unknown Python test runner"));
1238                }
1239                let key = (worker, test, retry);
1240                if let Some(previous) = evidence.runners.get(&key)
1241                    && previous != &runner
1242                {
1243                    return Err(invalid("one attempt was reported by two runners"));
1244                }
1245                evidence.runners.insert(key.clone(), runner);
1246                if let Some(file) = file.filter(|path| !path.is_empty()) {
1247                    evidence.test_files.entry(key.clone()).or_insert(file);
1248                }
1249                fold_outcome(
1250                    evidence.outcomes.entry(key).or_default(),
1251                    phase,
1252                    outcome,
1253                    xfail,
1254                );
1255            }
1256            Record::Hit { ctx, id } => {
1257                if let Some(before) = before_assertion.get_mut(&ctx) {
1258                    before.hits.insert(id.clone());
1259                }
1260                observations(
1261                    evidence,
1262                    &contexts,
1263                    process_worker.as_deref(),
1264                    ctx,
1265                    name,
1266                    line_number,
1267                )?
1268                .hits
1269                .insert(id);
1270            }
1271            // An empty record names no phase evidence: it must not make one.
1272            Record::Hits { ids, .. } if ids.is_empty() => {}
1273            Record::SlotHits { positions, .. } if positions.is_empty() => {}
1274            Record::SlotHits { ctx, positions } => {
1275                if let Some(before) = before_assertion.get_mut(&ctx) {
1276                    before.slot_hits.extend(&positions);
1277                }
1278                observations(
1279                    evidence,
1280                    &contexts,
1281                    process_worker.as_deref(),
1282                    ctx,
1283                    name,
1284                    line_number,
1285                )?
1286                .slot_hits
1287                .extend(positions);
1288            }
1289            Record::Decs { v, .. } if v.is_empty() => {}
1290            Record::Hits { ctx, ids } => {
1291                if let Some(before) = before_assertion.get_mut(&ctx) {
1292                    before.hits.extend(ids.iter().cloned());
1293                }
1294                // One lookup of the phase for the record, not one per hit.
1295                observations(
1296                    evidence,
1297                    &contexts,
1298                    process_worker.as_deref(),
1299                    ctx,
1300                    name,
1301                    line_number,
1302                )?
1303                .hits
1304                .extend(ids);
1305            }
1306            Record::Dec { ctx, id, v, o } => {
1307                if v.is_empty() || !v.bytes().all(|digit| matches!(digit, b'0' | b'1' | b'2')) {
1308                    return Err(invalid("decision vector digits must be 0, 1 or 2"));
1309                }
1310                if o > 1 {
1311                    return Err(invalid("decision outcome must be 0 or 1"));
1312                }
1313                let values = v
1314                    .bytes()
1315                    .map(|digit| match digit {
1316                        b'0' => None,
1317                        b'1' => Some(false),
1318                        _ => Some(true),
1319                    })
1320                    .collect::<Vec<_>>();
1321                if let Some(before) = before_assertion.get_mut(&ctx) {
1322                    before
1323                        .vectors
1324                        .entry(id.clone())
1325                        .or_default()
1326                        .insert((values.clone(), o == 1));
1327                }
1328                observations(
1329                    evidence,
1330                    &contexts,
1331                    process_worker.as_deref(),
1332                    ctx,
1333                    name,
1334                    line_number,
1335                )?
1336                .vectors
1337                .entry(id)
1338                .or_default()
1339                .insert((values, o == 1));
1340            }
1341            Record::Decs { ctx, v: vectors } => {
1342                let mut before = before_assertion.get_mut(&ctx);
1343                let observed = observations(
1344                    evidence,
1345                    &contexts,
1346                    process_worker.as_deref(),
1347                    ctx,
1348                    name,
1349                    line_number,
1350                )?;
1351                for (id, v, o) in vectors {
1352                    if v.is_empty() || !v.bytes().all(|digit| matches!(digit, b'0' | b'1' | b'2')) {
1353                        return Err(invalid("decision vector digits must be 0, 1 or 2"));
1354                    }
1355                    if o > 1 {
1356                        return Err(invalid("decision outcome must be 0 or 1"));
1357                    }
1358                    let values = v
1359                        .bytes()
1360                        .map(|digit| match digit {
1361                            b'0' => None,
1362                            b'1' => Some(false),
1363                            _ => Some(true),
1364                        })
1365                        .collect::<Vec<_>>();
1366                    if let Some(before) = before.as_mut() {
1367                        before
1368                            .vectors
1369                            .entry(id.clone())
1370                            .or_default()
1371                            .insert((values.clone(), o == 1));
1372                    }
1373                    observed
1374                        .vectors
1375                        .entry(id)
1376                        .or_default()
1377                        .insert((values, o == 1));
1378                }
1379            }
1380            Record::Assert { ctx } => {
1381                // Only the first marker of a call phase moves anything; a
1382                // later one, or one outside a call phase, is inert.
1383                if let Some(before) = before_assertion.remove(&ctx) {
1384                    let identity =
1385                        contexts
1386                            .get(&ctx)
1387                            .ok_or(PythonEvidenceError::UnknownContext {
1388                                file: name.into(),
1389                                line: line_number,
1390                                context: ctx,
1391                            })?;
1392                    let asserted = evidence
1393                        .per_identity
1394                        .entry(Identity {
1395                            phase: "assertion".into(),
1396                            ..identity.clone()
1397                        })
1398                        .or_default();
1399                    asserted.hits.extend(before.hits);
1400                    asserted.slot_hits.extend(before.slot_hits);
1401                    for (id, vectors) in before.vectors {
1402                        asserted.vectors.entry(id).or_default().extend(vectors);
1403                    }
1404                }
1405            }
1406            Record::Limitation {
1407                id,
1408                reason,
1409                file,
1410                obligation,
1411            } => {
1412                // Thousands of interpreters may report the same limitation.
1413                // Deduplicate before expanding file-wide obligations, while
1414                // retaining each affected file and obligation independently.
1415                evidence
1416                    .limitations
1417                    .entry((id.clone(), file.clone(), obligation.clone()))
1418                    .or_insert(RuntimeLimitation {
1419                        id,
1420                        reason,
1421                        file,
1422                        obligation,
1423                    });
1424            }
1425            // Expanded through the layout before it is applied.
1426            Record::Runs { .. } => return Err(invalid("slot runs were not expanded")),
1427            Record::Exit { .. } => {}
1428        }
1429        Ok(())
1430    };
1431    for (line_number, record) in parsed.records {
1432        apply(line_number, record)?;
1433    }
1434    Ok(())
1435}
1436
1437fn observations<'a>(
1438    evidence: &'a mut Evidence,
1439    contexts: &BTreeMap<u64, Identity>,
1440    process_worker: Option<&str>,
1441    context: u64,
1442    file: &str,
1443    line: usize,
1444) -> Result<&'a mut Observations, PythonEvidenceError> {
1445    if context == 0 {
1446        return Ok(evidence
1447            .background
1448            .entry(process_worker.unwrap_or("main").to_owned())
1449            .or_default());
1450    }
1451    let identity = contexts
1452        .get(&context)
1453        .ok_or(PythonEvidenceError::UnknownContext {
1454            file: file.into(),
1455            line,
1456            context,
1457        })?;
1458    Ok(evidence.per_identity.entry(identity.clone()).or_default())
1459}
1460
1461/// Python is observed through probes compiled into each measured module as it
1462/// is imported, from the obligations the plan derived from the source.
1463pub fn python_coverage_model() -> CoverageModelDeclaration {
1464    CoverageModelDeclaration {
1465        language: "python".into(),
1466        variant: "python-owned-probes".into(),
1467        name: PYTHON_FRONTEND_VERSION.into(),
1468        completeness_meaning: "Every statement, function, decision vector, loop, short-circuit, match and exception-flow obligation Supercov derived from the source was observed through probes compiled into the code at import, with exact test identity; the declared runtime limitations remain separate.".into(),
1469        measured: vec![
1470            "executable statements, each by a probe placed before it".into(),
1471            "function and lambda entry".into(),
1472            "boolean decision vectors with masking MC/DC, each condition's value taken as the program evaluated it".into(),
1473            "for-loop and comprehension zero-versus-entered iteration".into(),
1474            "logical and/or short-circuit alternatives".into(),
1475            "match case selection and guards".into(),
1476            "try completion, handler selection and exception propagation".into(),
1477            "pytest and unittest worker, test, retry and setup/call/teardown phase identity".into(),
1478            "evidence a test recorded before its first assertion, linked to that assertion when the test passes".into(),
1479        ],
1480        not_measured: vec![
1481            "causal linkage to individual actions, or to any assertion after a test's first".into(),
1482            "code compiled from strings at runtime".into(),
1483            "measured modules imported before Supercov installed, or compiled past the import system; the run names each as a limitation".into(),
1484            "causal test context for raw _thread or native-extension-created threads".into(),
1485            "child coverage outside subprocess.Popen and multiprocessing adapters".into(),
1486            "all input values, semantic partitions, paths, or concurrency interleavings".into(),
1487            "mutation score or assertion fault-detection strength".into(),
1488        ],
1489    }
1490}
1491
1492fn phase_id(run: &str, identity: &Identity) -> String {
1493    stable_id(
1494        "python-phase",
1495        &[
1496            run,
1497            &identity.worker,
1498            &identity.test,
1499            &identity.retry.to_string(),
1500            &identity.phase,
1501        ],
1502    )
1503}
1504
1505fn scope(run: &str, worker: &str, test: &str, retry: usize) -> ExecutionScope {
1506    ExecutionScope {
1507        version: 1,
1508        run_id: run.into(),
1509        worker_id: worker.into(),
1510        test_id: test.into(),
1511        test_key: stable_id("python-test", &[worker, test]),
1512        retry,
1513        attempt_id: stable_id("python-attempt", &[run, worker, test, &retry.to_string()]),
1514    }
1515}
1516
1517struct ManifestIndex<'a> {
1518    points: crate::interned::FastSet<&'a str>,
1519    alternatives: crate::interned::FastSet<&'a str>,
1520    decisions: crate::interned::FastMap<&'a str, &'a DecisionMeta>,
1521    lines: BTreeMap<&'a str, (&'a str, usize)>,
1522    files: BTreeMap<&'a str, Vec<&'a str>>,
1523}
1524
1525impl<'a> ManifestIndex<'a> {
1526    fn new(manifest: &'a CoverageManifest) -> Self {
1527        let mut lines = BTreeMap::new();
1528        for point in &manifest.points {
1529            lines.insert(point.id.as_str(), (point.file.as_str(), point.line));
1530        }
1531        for decision in &manifest.decisions {
1532            lines.insert(
1533                decision.id.as_str(),
1534                (decision.file.as_str(), decision.line),
1535            );
1536        }
1537        for branch in &manifest.branches {
1538            lines.insert(branch.id.as_str(), (branch.file.as_str(), branch.line));
1539        }
1540        let mut files = BTreeMap::<&str, Vec<&str>>::new();
1541        for (id, (file, _)) in &lines {
1542            files.entry(*file).or_default().push(*id);
1543        }
1544        Self {
1545            points: manifest
1546                .points
1547                .iter()
1548                .map(|point| point.id.as_str())
1549                .collect(),
1550            alternatives: manifest
1551                .branches
1552                .iter()
1553                .flat_map(|branch| branch.alternatives.iter().map(|alt| alt.id.as_str()))
1554                .collect(),
1555            decisions: manifest
1556                .decisions
1557                .iter()
1558                .map(|decision| (decision.id.as_str(), decision))
1559                .collect(),
1560            lines,
1561            files,
1562        }
1563    }
1564}
1565
1566fn snapshot(
1567    index: &ManifestIndex<'_>,
1568    observations: &Observations,
1569    slot_ids: &[String],
1570    phase: &str,
1571) -> Result<RuntimeSnapshot, PythonEvidenceError> {
1572    let mut positions = observations.slot_hits.clone();
1573    positions.sort_unstable();
1574    positions.dedup();
1575    let mut hits = BTreeSet::new();
1576    for id in observations.hits.iter().map(String::as_str).chain(
1577        positions
1578            .iter()
1579            .map(|position| slot_ids[*position as usize].as_str()),
1580    ) {
1581        if !index.points.contains(id) && !index.alternatives.contains(id) {
1582            return Err(PythonEvidenceError::UnknownObligation(id.to_owned()));
1583        }
1584        hits.insert(id);
1585    }
1586    let hits = hits.into_iter().map(str::to_owned).collect::<BTreeSet<_>>();
1587    let mut decisions = Vec::new();
1588    for (id, vectors) in &observations.vectors {
1589        let Some(meta) = index.decisions.get(id.as_str()) else {
1590            return Err(PythonEvidenceError::UnknownObligation(id.clone()));
1591        };
1592        let mut observed = Vec::new();
1593        for (values, outcome) in vectors {
1594            if values.len() != meta.conditions.len() {
1595                return Err(PythonEvidenceError::InvalidVector {
1596                    id: id.clone(),
1597                    expected: meta.conditions.len(),
1598                    actual: values.len(),
1599                });
1600            }
1601            observed.push(McdcVector {
1602                values: values.clone(),
1603                outcome: *outcome,
1604            });
1605        }
1606        decisions.push(DecisionSnapshot {
1607            meta: (*meta).clone(),
1608            vectors: observed,
1609        });
1610    }
1611    // One snapshot per phase: everything in it is that phase's, which the
1612    // snapshot says once rather than with an event per hit and vector.
1613    Ok(RuntimeSnapshot {
1614        decisions,
1615        hits: hits.into_iter().collect(),
1616        events: Vec::new(),
1617        logicals: Vec::new(),
1618        phase_id: Some(phase.into()),
1619    })
1620}
1621
1622/// One phase, however many times its runner reported it. pytest-subtests
1623/// reports every subtest under the same node id and `call` phase, so an
1624/// attempt carries several outcomes for the identity a phase id is derived
1625/// from; each became a phase of its own with the same id, and the run was
1626/// refused whole. The outcomes fold the way an attempt's do -- a failure
1627/// anywhere is the phase's, then a skip -- because that is what the phase
1628/// ended up doing, and the coverage recorded under that identity is one
1629/// phase's however the runner chose to narrate it.
1630fn fold_outcome(
1631    outcomes: &mut Vec<(String, String, bool)>,
1632    phase: String,
1633    outcome: String,
1634    xfail: bool,
1635) {
1636    let Some(reported) = outcomes.iter_mut().find(|(name, _, _)| *name == phase) else {
1637        outcomes.push((phase, outcome, xfail));
1638        return;
1639    };
1640    if outcome_severity(&outcome) > outcome_severity(&reported.1) {
1641        reported.1 = outcome;
1642    }
1643    reported.2 |= xfail;
1644}
1645
1646/// The order `attempt_status` reads outcomes in, as one value.
1647fn outcome_severity(outcome: &str) -> u8 {
1648    match outcome {
1649        "failed" | "rerun" | "error" => 2,
1650        "skipped" => 1,
1651        _ => 0,
1652    }
1653}
1654
1655fn attempt_status(outcomes: &[(String, String, bool)]) -> String {
1656    if outcomes
1657        .iter()
1658        .any(|(_, outcome, _)| matches!(outcome.as_str(), "failed" | "rerun" | "error"))
1659    {
1660        "failed"
1661    } else if outcomes.iter().any(|(_, outcome, _)| outcome == "skipped") {
1662        "skipped"
1663    } else {
1664        "passed"
1665    }
1666    .into()
1667}
1668
1669#[derive(Debug, Clone, PartialEq)]
1670pub struct PythonFrontendRun {
1671    pub declaration: FrontendRunDeclaration,
1672    pub request: CoverageReportRequest,
1673    pub tests: usize,
1674    pub interpreters: usize,
1675    pub python_versions: Vec<String>,
1676}
1677
1678impl PythonFrontendRun {
1679    pub fn archive_entries(&self) -> Result<Vec<EvidenceArchiveEntry>, serde_json::Error> {
1680        let model = PersistedCoverageModel::from_declaration(
1681            self.request
1682                .coverage_model
1683                .as_ref()
1684                .expect("Python frontend always declares a coverage model"),
1685        )
1686        .expect("Python coverage model is contract-valid");
1687        let mut entries = vec![
1688            EvidenceArchiveEntry {
1689                path: "coverage-model.json".into(),
1690                contents: serde_json::to_vec(&model)?,
1691            },
1692            EvidenceArchiveEntry {
1693                path: "frontend.json".into(),
1694                contents: serde_json::to_vec(&self.declaration)?,
1695            },
1696            EvidenceArchiveEntry {
1697                path: "manifest.json".into(),
1698                contents: serde_json::to_vec(&self.request.manifest)?,
1699            },
1700        ];
1701        for (index, result) in self.request.raw_results.iter().enumerate() {
1702            entries.push(EvidenceArchiveEntry {
1703                path: format!("results/{index:08}/mcdc.json"),
1704                contents: serde_json::to_vec(result)?,
1705            });
1706        }
1707        Ok(entries)
1708    }
1709}
1710
1711/// Join the runtime's evidence directory with the ahead-of-run manifest into
1712/// a protocol-conformant frontend run.
1713pub fn build_python_frontend_run(
1714    manifest: &CoverageManifest,
1715    evidence_directory: &Path,
1716    run_id: &str,
1717    generated_at: &str,
1718    test_exit_code: i32,
1719    root: &Path,
1720) -> Result<PythonFrontendRun, PythonEvidenceError> {
1721    let mut evidence = read_evidence_directory(evidence_directory, run_id)?;
1722    // A server the suite runs in its own process -- a live server a browser
1723    // or the test itself calls -- numbers the requests no test named: each
1724    // is the suite's test running then in that process, if only one was.
1725    let order = crate::served_tests::InProcess::new(&evidence.phase_starts);
1726    let by_time = credit_requests_by_time(&mut evidence, |_, transport, _, seq| {
1727        order.owner(transport, seq).cloned()
1728    });
1729    let mut run = build_from_evidence(
1730        evidence,
1731        manifest,
1732        run_id,
1733        generated_at,
1734        test_exit_code,
1735        root,
1736    )?;
1737    mark_by_time(&mut run, &by_time);
1738    Ok(run)
1739}
1740
1741pub use crate::served_tests::ServedTest;
1742
1743/// The Python run of a server a browser suite drove: the server's evidence,
1744/// with each browser test's outcome as its runner reported it, under the
1745/// names that runner gave them.
1746pub fn build_python_server_run(
1747    manifest: &CoverageManifest,
1748    evidence_directory: &Path,
1749    run_id: &str,
1750    generated_at: &str,
1751    test_exit_code: i32,
1752    root: &Path,
1753    served: &[ServedTest],
1754) -> Result<PythonFrontendRun, PythonEvidenceError> {
1755    let mut evidence = read_evidence_directory(evidence_directory, run_id)?;
1756    let mut by_attempt = BTreeMap::new();
1757    for test in served {
1758        let key = (test.worker.clone(), test.test_id.clone(), test.retry);
1759        let outcome = match test.status.as_str() {
1760            "passed" => "passed",
1761            "skipped" => "skipped",
1762            _ => "failed",
1763        };
1764        evidence.outcomes.entry(key.clone()).or_default().push((
1765            "call".into(),
1766            outcome.into(),
1767            test.expected_status == "failed",
1768        ));
1769        evidence
1770            .runners
1771            .insert(key.clone(), test.provenance.runner.clone());
1772        by_attempt.insert(key, test);
1773    }
1774    let by_time = credit_requests_by_time(&mut evidence, |evidence, transport, at, _| {
1775        crate::served_tests::owner(served, evidence.served_by.get(transport), at)
1776            .map(|test| (test.worker.clone(), test.test_id.clone(), test.retry))
1777    });
1778    let mut run = build_from_evidence(
1779        evidence,
1780        manifest,
1781        run_id,
1782        generated_at,
1783        test_exit_code,
1784        root,
1785    )?;
1786    for raw in &mut run.request.raw_results {
1787        let Some(test) = raw.scope.as_ref().and_then(|scope| {
1788            by_attempt.get(&(
1789                scope.worker_id.clone(),
1790                raw.test.clone(),
1791                raw.retry.unwrap_or(0),
1792            ))
1793        }) else {
1794            continue;
1795        };
1796        raw.test = test.name.clone();
1797        raw.title = Some(test.title.clone());
1798        raw.test_file = Some(test.file.clone());
1799        raw.status = Some(test.status.clone());
1800        raw.expected_status = Some(test.expected_status.clone());
1801        raw.provenance = test.provenance.clone();
1802        if by_time.contains(&(test.worker.clone(), test.test_id.clone(), test.retry)) {
1803            raw.attribution = crate::coverage_report::ATTRIBUTION_BY_TIME.into();
1804        }
1805    }
1806    run.tests = run
1807        .request
1808        .raw_results
1809        .iter()
1810        .filter(|raw| raw.role == "test")
1811        .map(|raw| raw.test.as_str())
1812        .collect::<BTreeSet<_>>()
1813        .len();
1814    Ok(run)
1815}
1816
1817/// Say of each attempt credited with a request by time that it was.
1818fn mark_by_time(run: &mut PythonFrontendRun, by_time: &BTreeSet<(String, String, usize)>) {
1819    for raw in &mut run.request.raw_results {
1820        let Some(scope) = &raw.scope else { continue };
1821        let key = (
1822            scope.worker_id.clone(),
1823            scope.test_id.clone(),
1824            raw.retry.unwrap_or(0),
1825        );
1826        if by_time.contains(&key) && raw.attribution == crate::coverage_report::ATTRIBUTION_EXACT {
1827            raw.attribution = crate::coverage_report::ATTRIBUTION_BY_TIME.into();
1828        }
1829    }
1830}
1831
1832/// Give each request that named no test to the test attempt `owner` names
1833/// for it -- from its transport, when it came and where in its process's
1834/// records -- or, where none, to its process's background. Returns the
1835/// attempts credited so.
1836fn credit_requests_by_time(
1837    evidence: &mut Evidence,
1838    owner: impl Fn(&Evidence, &str, i64, usize) -> Option<(String, String, usize)>,
1839) -> BTreeSet<(String, String, usize)> {
1840    let requests = std::mem::take(&mut evidence.requests);
1841    let owners = requests
1842        .iter()
1843        .map(|(_, at, seq, transport, _)| owner(evidence, transport, *at, *seq))
1844        .collect::<Vec<_>>();
1845    let mut credited = BTreeSet::new();
1846    for ((identity, _, _, _, process_worker), owner) in requests.into_iter().zip(owners) {
1847        let Some(observations) = evidence.per_identity.remove(&identity) else {
1848            continue;
1849        };
1850        let into = match owner {
1851            Some((worker, test, retry)) => {
1852                credited.insert((worker.clone(), test.clone(), retry));
1853                evidence
1854                    .per_identity
1855                    .entry(Identity {
1856                        worker,
1857                        test,
1858                        retry,
1859                        phase: "call".into(),
1860                    })
1861                    .or_default()
1862            }
1863            None => evidence.background.entry(process_worker).or_default(),
1864        };
1865        into.hits.extend(observations.hits);
1866        into.slot_hits.extend(observations.slot_hits);
1867        for (id, vectors) in observations.vectors {
1868            into.vectors.entry(id).or_default().extend(vectors);
1869        }
1870    }
1871    credited
1872}
1873
1874fn build_from_evidence(
1875    evidence: Evidence,
1876    manifest: &CoverageManifest,
1877    run_id: &str,
1878    generated_at: &str,
1879    test_exit_code: i32,
1880    root: &Path,
1881) -> Result<PythonFrontendRun, PythonEvidenceError> {
1882    if evidence.interpreters == 0 {
1883        return Err(PythonEvidenceError::NoInterpreter);
1884    }
1885    if evidence.outcomes.is_empty() {
1886        return Err(PythonEvidenceError::NoTests);
1887    }
1888    let Evidence {
1889        interpreters,
1890        python_versions,
1891        per_identity,
1892        background,
1893        outcomes,
1894        runners,
1895        test_files,
1896        limitations,
1897        slot_ids,
1898        ..
1899    } = evidence;
1900    let mut manifest = manifest.clone();
1901    let index = ManifestIndex::new(&manifest);
1902
1903    let mut raw_results = Vec::new();
1904    let mut observed_runners = BTreeSet::new();
1905    let mut identities_by_attempt =
1906        BTreeMap::<(String, String, usize), Vec<(&Identity, &Observations)>>::new();
1907    for (identity, observations) in &per_identity {
1908        identities_by_attempt
1909            .entry((
1910                identity.worker.clone(),
1911                identity.test.clone(),
1912                identity.retry,
1913            ))
1914            .or_default()
1915            .push((identity, observations));
1916    }
1917    // Each phase's snapshot stands alone: built side by side, taken in the
1918    // order below, and any error raised where the phase is reached.
1919    let phases_observed = per_identity.iter().collect::<Vec<_>>();
1920    let mut snapshots = phases_observed
1921        .iter()
1922        .map(|(identity, _)| *identity)
1923        .zip(parallel_map(
1924            &phases_observed,
1925            |(identity, observations)| {
1926                snapshot(&index, observations, &slot_ids, &phase_id(run_id, identity))
1927            },
1928        ))
1929        .collect::<BTreeMap<_, _>>();
1930    let mut take = |identity: &Identity| {
1931        snapshots
1932            .remove(identity)
1933            .expect("each phase's snapshot is taken once")
1934    };
1935    for ((worker, test, retry), mut outcomes) in outcomes {
1936        let runner = runners
1937            .get(&(worker.clone(), test.clone(), retry))
1938            .cloned()
1939            .unwrap_or_else(default_runner);
1940        let attempt_identities = identities_by_attempt
1941            .remove(&(worker.clone(), test.clone(), retry))
1942            .unwrap_or_default();
1943        observed_runners.insert(runner.clone());
1944        outcomes.sort_by_key(|(phase, _, _)| match phase.as_str() {
1945            "setup" => 0,
1946            "call" => 1,
1947            _ => 2,
1948        });
1949        let mut phases = Vec::new();
1950        let mut runtime = Vec::new();
1951        let mut observed_phases = BTreeSet::new();
1952        for (position, (phase_name, outcome, xfail)) in outcomes.iter().enumerate() {
1953            observed_phases.insert(phase_name.clone());
1954            let identity = Identity {
1955                worker: worker.clone(),
1956                test: test.clone(),
1957                retry,
1958                phase: phase_name.clone(),
1959            };
1960            let id = phase_id(run_id, &identity);
1961            phases.push(CoveragePhase {
1962                id: id.clone(),
1963                kind: match phase_name.as_str() {
1964                    "call" => "test",
1965                    value => value,
1966                }
1967                .into(),
1968                operation: format!("{runner} {phase_name}"),
1969                source: Some(test.clone()),
1970                caused_by_phase_id: None,
1971                started_at_ms: position as i64 * 2 + 1,
1972                ended_at_ms: Some(position as i64 * 2 + 2),
1973                status: Some(match outcome.as_str() {
1974                    "rerun" | "error" => "failed".into(),
1975                    value => value.into(),
1976                }),
1977                error: None,
1978            });
1979            if let Some((observed, _)) = attempt_identities
1980                .iter()
1981                .find(|(candidate, _)| candidate.phase == phase_name.as_str())
1982            {
1983                runtime.push(take(observed)?);
1984            }
1985            if phase_name != "call" {
1986                continue;
1987            }
1988            // What the test recorded before its first assertion is that
1989            // assertion's evidence, linked when the phase passed outright:
1990            // a failed, skipped or expected-to-fail phase witnessed nothing.
1991            if let Some((identity, _)) = attempt_identities
1992                .iter()
1993                .find(|(candidate, _)| candidate.phase == "assertion")
1994            {
1995                observed_phases.insert("assertion".to_owned());
1996                let id = phase_id(run_id, identity);
1997                phases.push(CoveragePhase {
1998                    id: id.clone(),
1999                    kind: "assertion".into(),
2000                    operation: format!("{runner} assertion"),
2001                    source: Some(test.clone()),
2002                    caused_by_phase_id: None,
2003                    started_at_ms: position as i64 * 2 + 1,
2004                    ended_at_ms: Some(position as i64 * 2 + 2),
2005                    status: Some(
2006                        if outcome == "passed" && !*xfail {
2007                            "passed"
2008                        } else {
2009                            "failed"
2010                        }
2011                        .into(),
2012                    ),
2013                    error: None,
2014                });
2015                runtime.push(take(identity)?);
2016            }
2017        }
2018        // A phase the runtime entered but pytest never reported (the worker
2019        // died inside it) is a failed phase with its evidence kept.
2020        for (identity, _) in attempt_identities {
2021            if !observed_phases.contains(&identity.phase) {
2022                let id = phase_id(run_id, identity);
2023                phases.push(CoveragePhase {
2024                    id: id.clone(),
2025                    kind: match identity.phase.as_str() {
2026                        "call" => "test",
2027                        value => value,
2028                    }
2029                    .into(),
2030                    operation: format!("{runner} {}", identity.phase),
2031                    source: Some(test.clone()),
2032                    caused_by_phase_id: None,
2033                    started_at_ms: phases.len() as i64 * 2 + 1,
2034                    ended_at_ms: None,
2035                    status: Some("failed".into()),
2036                    error: Some("the phase started but the runner reported no outcome".into()),
2037                });
2038                runtime.push(take(identity)?);
2039            }
2040        }
2041        let status = if phases.iter().any(|phase| phase.error.is_some()) {
2042            "failed".into()
2043        } else {
2044            attempt_status(&outcomes)
2045        };
2046        let test_file = test_files
2047            .get(&(worker.clone(), test.clone(), retry))
2048            .and_then(|path| relative(root, path))
2049            .or_else(|| test.split("::").next().map(str::to_owned));
2050        raw_results.push(RawTestResult {
2051            test_id: Some(test.clone()),
2052            scope: Some(scope(run_id, &worker, &test, retry)),
2053            test: test.clone(),
2054            // What the runner named, as the project names it. A dotted
2055            // module path or a pytest node id is not a path, so the old
2056            // derivation stays only as a fallback.
2057            test_file: test_file.clone(),
2058            title: test.rsplit("::").next().map(str::to_owned),
2059            retry: Some(retry),
2060            status: Some(status),
2061            expected_status: Some(
2062                if outcomes.iter().any(|(_, _, xfail)| *xfail) {
2063                    "failed"
2064                } else {
2065                    "passed"
2066                }
2067                .into(),
2068            ),
2069            flaky: false,
2070            provenance: TestProvenance {
2071                runner: runner.clone(),
2072                kind: crate::test_kind::from_path(test_file.as_deref()).into(),
2073                project: None,
2074                source: PYTHON_FRONTEND_VERSION.into(),
2075            },
2076            role: "test".into(),
2077            attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
2078            phases,
2079            runtime,
2080            browser: Vec::new(),
2081            server: Vec::new(),
2082        });
2083    }
2084    // Phases with observations whose test never produced any outcome at all
2085    // (for example a worker killed during its first phase).
2086    let default_observed = observed_runners
2087        .iter()
2088        .next()
2089        .cloned()
2090        .unwrap_or_else(default_runner);
2091    for ((worker, test, retry), identities) in identities_by_attempt {
2092        let runner = default_observed.clone();
2093        let mut phases = Vec::new();
2094        let mut runtime = Vec::new();
2095        for (position, (identity, _)) in identities.iter().enumerate() {
2096            let id = phase_id(run_id, identity);
2097            phases.push(CoveragePhase {
2098                id: id.clone(),
2099                kind: match identity.phase.as_str() {
2100                    "call" => "test",
2101                    value => value,
2102                }
2103                .into(),
2104                operation: format!("{runner} {}", identity.phase),
2105                source: Some(test.clone()),
2106                caused_by_phase_id: None,
2107                started_at_ms: position as i64 * 2 + 1,
2108                ended_at_ms: None,
2109                status: Some("failed".into()),
2110                error: Some("the phase started but the runner reported no outcome".into()),
2111            });
2112            runtime.push(take(identity)?);
2113        }
2114        let test_file = test_files
2115            .get(&(worker.clone(), test.clone(), retry))
2116            .and_then(|path| relative(root, path))
2117            .or_else(|| test.split("::").next().map(str::to_owned));
2118        raw_results.push(RawTestResult {
2119            test_id: Some(test.clone()),
2120            scope: Some(scope(run_id, &worker, &test, retry)),
2121            test: test.clone(),
2122            // What the runner named, as the project names it. A dotted
2123            // module path or a pytest node id is not a path, so the old
2124            // derivation stays only as a fallback.
2125            test_file: test_file.clone(),
2126            title: test.rsplit("::").next().map(str::to_owned),
2127            retry: Some(retry),
2128            status: Some("failed".into()),
2129            expected_status: Some("passed".into()),
2130            flaky: false,
2131            provenance: TestProvenance {
2132                runner: runner.clone(),
2133                kind: crate::test_kind::from_path(test_file.as_deref()).into(),
2134                project: None,
2135                source: PYTHON_FRONTEND_VERSION.into(),
2136            },
2137            role: "test".into(),
2138            attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
2139            phases,
2140            runtime,
2141            browser: Vec::new(),
2142            server: Vec::new(),
2143        });
2144    }
2145    for (worker, observations) in &background {
2146        if observations.is_empty() {
2147            continue;
2148        }
2149        let test = format!("__supercov_background__:{worker}");
2150        let identity = Identity {
2151            worker: worker.clone(),
2152            test: test.clone(),
2153            retry: 0,
2154            phase: "background".into(),
2155        };
2156        let phase = phase_id(run_id, &identity);
2157        raw_results.push(RawTestResult {
2158            test_id: Some(test.clone()),
2159            scope: Some(scope(run_id, worker, &test, 0)),
2160            test: "Python import, collection and background execution".into(),
2161            test_file: None,
2162            title: None,
2163            retry: Some(0),
2164            status: Some("unknown".into()),
2165            expected_status: None,
2166            flaky: false,
2167            provenance: TestProvenance {
2168                runner: default_observed.clone(),
2169                kind: "unit".into(),
2170                project: None,
2171                source: PYTHON_FRONTEND_VERSION.into(),
2172            },
2173            role: "background".into(),
2174            attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
2175            phases: vec![CoveragePhase {
2176                id: phase.clone(),
2177                kind: "background".into(),
2178                operation: "Python import and collection background".into(),
2179                source: None,
2180                caused_by_phase_id: None,
2181                started_at_ms: 0,
2182                ended_at_ms: Some(0),
2183                status: Some("passed".into()),
2184                error: None,
2185            }],
2186            runtime: vec![snapshot(&index, observations, &slot_ids, &phase)?],
2187            browser: Vec::new(),
2188            server: Vec::new(),
2189        });
2190    }
2191
2192    // Runtime-detected limitations: obligations the runtime could not map
2193    // become unmeasured, and every limitation ID joins the manifest so the
2194    // declaration and manifest agree.
2195    let mut limitation_ids = manifest
2196        .limitations
2197        .iter()
2198        .filter_map(|item| item.get("id").and_then(serde_json::Value::as_str))
2199        .map(str::to_owned)
2200        .collect::<BTreeSet<_>>();
2201    let mut unmeasured = manifest.unmeasured.iter().cloned().collect::<BTreeSet<_>>();
2202    let mut new_limitations = Vec::new();
2203    // What any process was seen to run, to tell a file some process ran
2204    // unobserved from a file nothing observed at all.
2205    let observed = raw_results
2206        .iter()
2207        .flat_map(|raw| raw.runtime.iter())
2208        .flat_map(|snapshot| snapshot.hits.iter().cloned())
2209        .collect::<std::collections::HashSet<String>>();
2210    for limitation in limitations.values() {
2211        if let Some(obligation) = &limitation.obligation {
2212            if !index.lines.contains_key(obligation.as_str()) {
2213                return Err(PythonEvidenceError::UnknownObligation(obligation.clone()));
2214            }
2215            unmeasured.insert(obligation.clone());
2216        } else if let Some(file) = &limitation.file {
2217            // A code-object mapping failure or missing debug ranges prevents
2218            // every obligation in that source file from being observed. Mark
2219            // the whole file unmeasured instead of presenting its denominator
2220            // as ordinary uncovered code.
2221            //
2222            // An unobserved module is narrower: one process ran the file
2223            // without probes -- most often `python path/script.py`, which
2224            // CPython compiles outside the import system -- while every
2225            // process that imported it was measured. A project whose tests
2226            // both import its scripts and run them lost 43 files and six
2227            // points of line coverage when the direct run discarded the
2228            // imports' evidence. Such a file stays measured, a lower bound
2229            // the limitation still flags, unless nothing it ran was observed.
2230            let observed_elsewhere = limitation.id == "python-probes-unobserved-module"
2231                && index
2232                    .files
2233                    .get(file.as_str())
2234                    .is_some_and(|obligations| obligations.iter().any(|id| observed.contains(*id)));
2235            if !observed_elsewhere && let Some(obligations) = index.files.get(file.as_str()) {
2236                unmeasured.extend(obligations.iter().map(|id| (*id).to_owned()));
2237            }
2238        }
2239        let id = if limitation.file.is_some() || limitation.obligation.is_some() {
2240            stable_id(
2241                &limitation.id,
2242                &[
2243                    limitation.file.as_deref().unwrap_or_default(),
2244                    limitation.obligation.as_deref().unwrap_or_default(),
2245                ],
2246            )
2247        } else {
2248            limitation.id.clone()
2249        };
2250        if limitation_ids.insert(id.clone()) {
2251            let (file, line) = limitation
2252                .obligation
2253                .as_deref()
2254                .and_then(|id| {
2255                    index
2256                        .lines
2257                        .get(id)
2258                        .map(|(file, line)| ((*file).to_owned(), *line))
2259                })
2260                .unwrap_or_else(|| {
2261                    (
2262                        limitation.file.clone().unwrap_or_else(|| {
2263                            manifest
2264                                .points
2265                                .first()
2266                                .map_or(".".into(), |point| point.file.clone())
2267                        }),
2268                        1,
2269                    )
2270                });
2271            new_limitations.push(json!({
2272                "id": id,
2273                "code": limitation.id,
2274                "kind": "semantic-safety",
2275                "file": file,
2276                "line": line,
2277                "column": 0,
2278                "source": "",
2279                "reason": limitation.reason
2280            }));
2281        }
2282    }
2283    manifest.limitations.extend(new_limitations);
2284    manifest.unmeasured = unmeasured.into_iter().collect();
2285    let structural_limitations = limitation_ids.into_iter().collect::<Vec<_>>();
2286
2287    // Retries are separate raw results so their coverage remains attempt
2288    // exact, but the public lifecycle diagnostic reports logical tests rather
2289    // than inflating the count when a flaky test is rerun.
2290    let tests = raw_results
2291        .iter()
2292        .filter(|raw| raw.role == "test")
2293        .map(|raw| raw.test.as_str())
2294        .collect::<BTreeSet<_>>()
2295        .len();
2296    Ok(PythonFrontendRun {
2297        declaration: FrontendRunDeclaration {
2298            protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
2299            frontend_id: "python".into(),
2300            frontend_version: PYTHON_FRONTEND_VERSION.into(),
2301            language: "python".into(),
2302            structural_source: StructuralSource::OwnedProbes,
2303            runners: observed_runners
2304                .iter()
2305                .map(|runner| FrontendRunnerDeclaration {
2306                    runner: runner.clone(),
2307                    execution_model: if runner == UNITTEST_RUNNER {
2308                        ExecutionModel::SerialInProcess
2309                    } else {
2310                        ExecutionModel::ParallelContextPropagated
2311                    },
2312                    attribution: FrontendAttribution {
2313                        run: AttributionPrecision::Exact,
2314                        worker: AttributionPrecision::Exact,
2315                        test: AttributionPrecision::Exact,
2316                        retry: AttributionPrecision::Exact,
2317                        phase: AttributionPrecision::Exact,
2318                        action: AttributionPrecision::Unavailable,
2319                        // A server sees a browser test's requests, never
2320                        // its assertions, which run in the browser runner.
2321                        assertion: if python_runner(runner) {
2322                            AttributionPrecision::Exact
2323                        } else {
2324                            AttributionPrecision::Unavailable
2325                        },
2326                    },
2327                    limitations: if python_runner(runner) {
2328                        vec![FrontendLimitation {
2329                            id: format!("python-{runner}-action-linkage"),
2330                            scopes: vec![FrontendLimitationScope::Action],
2331                            reason: format!("{runner} exposes no general action lifecycle"),
2332                        }]
2333                    } else {
2334                        vec![FrontendLimitation {
2335                            id: format!("python-server-{runner}-linkage"),
2336                            scopes: vec![
2337                                FrontendLimitationScope::Action,
2338                                FrontendLimitationScope::Assertion,
2339                            ],
2340                            reason: format!(
2341                                "the server sees each {runner} test's requests, not its actions or assertions"
2342                            ),
2343                        }]
2344                    },
2345                })
2346                .collect(),
2347            structural_limitations,
2348        },
2349        request: CoverageReportRequest {
2350            run_id: run_id.into(),
2351            manifest,
2352            raw_results,
2353            generated_at: generated_at.into(),
2354            coverage_model: Some(python_coverage_model()),
2355            integrity: None,
2356            test_exit_code: ExitCodeInput::Present(Some(test_exit_code)),
2357        },
2358        tests,
2359        interpreters,
2360        python_versions: python_versions.into_iter().collect(),
2361    })
2362}
2363
2364#[cfg(test)]
2365mod tests {
2366    use std::time::{SystemTime, UNIX_EPOCH};
2367
2368    use super::*;
2369    use crate::{
2370        frontend_protocol::validate_frontend_report_request,
2371        python_instrumenter::build_python_obligations,
2372    };
2373
2374    // A request that named no test is the test's that was running when it
2375    // came, if only one was: among those its process answered a request of,
2376    // where it answered any, and among every test otherwise.
2377    #[test]
2378    fn a_request_naming_no_test_is_the_only_running_tests() {
2379        let identity = |worker: &str, test: &str| Identity {
2380            worker: worker.into(),
2381            test: test.into(),
2382            retry: 0,
2383            phase: "call".into(),
2384        };
2385        let observed = |hit: &str| Observations {
2386            hits: BTreeSet::from([hit.to_owned()]),
2387            ..Observations::default()
2388        };
2389        let served = |worker: &str, test: &str, window: (i64, i64)| ServedTest {
2390            worker: worker.into(),
2391            test_id: test.into(),
2392            retry: 0,
2393            name: test.into(),
2394            title: test.into(),
2395            file: "tests/a.spec.js".into(),
2396            status: "passed".into(),
2397            expected_status: "passed".into(),
2398            provenance: TestProvenance::default(),
2399            window: Some(window),
2400        };
2401        // Two workers' tests at once; one server per worker, and one server
2402        // only a mock calls.
2403        let tests = [
2404            served("playwright:w0", "one", (100, 200)),
2405            served("playwright:w1", "two", (100, 200)),
2406            served("playwright:w1", "three", (300, 400)),
2407        ];
2408        let first = identity("supercov-request:a.mmap", "1");
2409        let late = identity("supercov-request:a.mmap", "2");
2410        let mocked = identity("supercov-request:b.mmap", "1");
2411        let alone = identity("supercov-request:b.mmap", "2");
2412        let mut evidence = Evidence {
2413            per_identity: BTreeMap::from([
2414                (first.clone(), observed("first")),
2415                (late.clone(), observed("late")),
2416                (mocked.clone(), observed("mocked")),
2417                (alone.clone(), observed("alone")),
2418            ]),
2419            requests: vec![
2420                (first, 150, 1, "a.mmap".into(), "main".into()),
2421                (late, 250, 2, "a.mmap".into(), "main".into()),
2422                (mocked, 150, 1, "b.mmap".into(), "main".into()),
2423                (alone, 350, 2, "b.mmap".into(), "main".into()),
2424            ],
2425            served_by: BTreeMap::from([(
2426                "a.mmap".to_owned(),
2427                BTreeSet::from([("playwright:w0".to_owned(), "one".to_owned(), 0)]),
2428            )]),
2429            ..Evidence::default()
2430        };
2431        let credited = credit_requests_by_time(&mut evidence, |evidence, transport, at, _| {
2432            crate::served_tests::owner(&tests, evidence.served_by.get(transport), at)
2433                .map(|test| (test.worker.clone(), test.test_id.clone(), test.retry))
2434        });
2435        assert_eq!(
2436            credited,
2437            BTreeSet::from([
2438                ("playwright:w0".to_owned(), "one".to_owned(), 0),
2439                ("playwright:w1".to_owned(), "three".to_owned(), 0),
2440            ])
2441        );
2442        let hits = |worker: &str, test: &str| {
2443            evidence
2444                .per_identity
2445                .get(&identity(worker, test))
2446                .map(|observations| observations.hits.clone())
2447                .unwrap_or_default()
2448        };
2449        // The worker's own test, though another ran meanwhile.
2450        assert_eq!(
2451            hits("playwright:w0", "one"),
2452            BTreeSet::from(["first".to_owned()])
2453        );
2454        assert_eq!(
2455            hits("playwright:w1", "three"),
2456            BTreeSet::from(["alone".to_owned()])
2457        );
2458        // Between its tests, and while two ran with no process to choose by,
2459        // no test's: the background, and no request identity remains.
2460        assert_eq!(
2461            evidence.background["main"].hits,
2462            BTreeSet::from(["late".to_owned(), "mocked".to_owned()])
2463        );
2464        assert!(
2465            evidence
2466                .per_identity
2467                .keys()
2468                .all(|identity| !identity.worker.starts_with(REQUEST_WORKER))
2469        );
2470    }
2471
2472    fn temporary(name: &str) -> std::path::PathBuf {
2473        let nonce = SystemTime::now()
2474            .duration_since(UNIX_EPOCH)
2475            .unwrap()
2476            .as_nanos();
2477        let path = std::env::temp_dir().join(format!(
2478            "supercov-python-evidence-{}-{nonce}-{name}",
2479            std::process::id()
2480        ));
2481        fs::create_dir_all(&path).unwrap();
2482        path
2483    }
2484
2485    /// One transport in memory, a frame per payload.
2486    fn transport_bytes(payloads: &[&[u8]]) -> Vec<u8> {
2487        let capacity = payloads
2488            .iter()
2489            .fold(TRANSPORT_HEADER_SIZE, |cursor, payload| {
2490                align_transport(cursor + TRANSPORT_RECORD_HEADER_SIZE + payload.len()).unwrap()
2491            });
2492        let mut bytes = vec![0_u8; capacity];
2493        bytes[..8].copy_from_slice(TRANSPORT_MAGIC);
2494        bytes[8..12].copy_from_slice(&TRANSPORT_VERSION.to_le_bytes());
2495        bytes[12..16].copy_from_slice(&(TRANSPORT_HEADER_SIZE as u32).to_le_bytes());
2496        bytes[16..24].copy_from_slice(&(capacity as u64).to_le_bytes());
2497        bytes[32..40].copy_from_slice(&1_u64.to_le_bytes());
2498        let mut cursor = TRANSPORT_HEADER_SIZE;
2499        for payload in payloads {
2500            let start = cursor + TRANSPORT_RECORD_HEADER_SIZE;
2501            bytes[start..start + payload.len()].copy_from_slice(payload);
2502            bytes[cursor + 4..cursor + 8].copy_from_slice(&(payload.len() as u32).to_le_bytes());
2503            bytes[cursor + 8..cursor + 12].copy_from_slice(&crc32fast::hash(payload).to_le_bytes());
2504            bytes[cursor] = 1;
2505            cursor = align_transport(start + payload.len()).unwrap();
2506        }
2507        bytes
2508    }
2509
2510    #[test]
2511    fn a_damaged_transport_is_refused_with_what_is_wrong() {
2512        // A killed writer, a full disk or another process writing into the
2513        // evidence directory each leave a transport like one of these. Each
2514        // is refused, never read as evidence.
2515        let exit: &[u8] = br#"{"t":"exit","at":9}"#;
2516        let valid = transport_bytes(&[exit]);
2517        let read = |bytes: &[u8]| match parse_transport("main.1.mmap", bytes, &[], None) {
2518            Ok(_) => "accepted".to_owned(),
2519            Err(error) => error.to_string(),
2520        };
2521        assert_eq!(read(&valid), "accepted");
2522        // The first frame's header starts at 64, its payload at 80 and ends at
2523        // 99, and its padding runs to 104.
2524        let set = |at: usize, value: &[u8]| {
2525            let mut bytes = valid.clone();
2526            bytes[at..at + value.len()].copy_from_slice(value);
2527            bytes
2528        };
2529        let mut truncated = valid[..99].to_vec();
2530        truncated[16..24].copy_from_slice(&99_u64.to_le_bytes());
2531        for (bytes, reason) in [
2532            (set(0, b"X"), "header or version does not match"),
2533            (
2534                set(8, &99_u32.to_le_bytes()),
2535                "header or version does not match",
2536            ),
2537            (
2538                set(16, &8_u64.to_le_bytes()),
2539                "declared capacity is outside the mapped file",
2540            ),
2541            (set(24, &3_u64.to_le_bytes()), "3"),
2542            (set(32, &0_u64.to_le_bytes()), "process id is missing"),
2543            (set(64, &[2]), "commit marker or reserved bytes are invalid"),
2544            (
2545                set(68, &0_u32.to_le_bytes()),
2546                "payload length is outside the transport bound",
2547            ),
2548            (
2549                set(68, &64_u32.to_le_bytes()),
2550                "payload extends past the mapped file",
2551            ),
2552            (truncated, "aligned frame extends past the mapped file"),
2553            (set(100, &[1]), "frame padding is not zero"),
2554            (
2555                set(72, &0_u32.to_le_bytes()),
2556                "payload checksum does not match",
2557            ),
2558            (transport_bytes(&[b"{not json"]), "key must be a string"),
2559            (
2560                transport_bytes(&[br#"{"t":"runs","ctx":1,"r":[]}"#]),
2561                "slot runs arrived without the run's slot layout",
2562            ),
2563        ] {
2564            let said = read(&bytes);
2565            assert!(said.contains(reason), "{reason}: {said}");
2566        }
2567        // A frame whose commit byte never landed ends the transport there,
2568        // as a killed writer leaves it.
2569        let mut uncommitted = transport_bytes(&[exit, exit]);
2570        uncommitted[104] = 0;
2571        assert_eq!(read(&uncommitted), "accepted");
2572    }
2573
2574    fn write_transport(path: &Path, records: &[serde_json::Value], dropped: u64) {
2575        write_transport_version(path, records, dropped, TRANSPORT_VERSION, None);
2576    }
2577
2578    /// A transport of any version, checksummed as that version does -- or,
2579    /// with `corrupt`, with one payload's checksum deliberately wrong.
2580    fn write_transport_version(
2581        path: &Path,
2582        records: &[serde_json::Value],
2583        dropped: u64,
2584        version: u32,
2585        corrupt: Option<usize>,
2586    ) {
2587        let payloads = records
2588            .iter()
2589            .map(|record| serde_json::to_vec(record).unwrap())
2590            .collect::<Vec<_>>();
2591        let capacity = payloads
2592            .iter()
2593            .fold(TRANSPORT_HEADER_SIZE, |cursor, payload| {
2594                align_transport(cursor + TRANSPORT_RECORD_HEADER_SIZE + payload.len()).unwrap()
2595            })
2596            + 64;
2597        let mut bytes = vec![0_u8; capacity];
2598        bytes[..8].copy_from_slice(TRANSPORT_MAGIC);
2599        bytes[8..12].copy_from_slice(&version.to_le_bytes());
2600        bytes[12..16].copy_from_slice(&(TRANSPORT_HEADER_SIZE as u32).to_le_bytes());
2601        bytes[16..24].copy_from_slice(&(capacity as u64).to_le_bytes());
2602        bytes[24..32].copy_from_slice(&dropped.to_le_bytes());
2603        bytes[32..40].copy_from_slice(&1_u64.to_le_bytes());
2604        let mut cursor = TRANSPORT_HEADER_SIZE;
2605        for (index, payload) in payloads.into_iter().enumerate() {
2606            let payload_start = cursor + TRANSPORT_RECORD_HEADER_SIZE;
2607            let payload_end = payload_start + payload.len();
2608            bytes[payload_start..payload_end].copy_from_slice(&payload);
2609            bytes[cursor + 4..cursor + 8].copy_from_slice(&(payload.len() as u32).to_le_bytes());
2610            // Computed here, not through the reader's own function: a test of
2611            // which checksum a version means must not share the code under
2612            // test with the transport it writes.
2613            let expected = if version == TRANSPORT_VERSION_FNV {
2614                payload.iter().fold(0x811c_9dc5_u32, |value, byte| {
2615                    (value ^ u32::from(*byte)).wrapping_mul(0x0100_0193)
2616                })
2617            } else {
2618                crc32fast::hash(&payload)
2619            };
2620            let checksum = expected ^ u32::from(corrupt == Some(index));
2621            bytes[cursor + 8..cursor + 12].copy_from_slice(&checksum.to_le_bytes());
2622            bytes[cursor] = 1;
2623            cursor = align_transport(payload_end).unwrap();
2624        }
2625        fs::write(path, bytes).unwrap();
2626    }
2627
2628    // A path is only absolute in the platform's own spelling: "/project" is a
2629    // relative path on Windows, where an absolute one needs a drive. The
2630    // runtimes report whatever the interpreter loaded, so these fixtures have
2631    // to speak the host's dialect too.
2632    fn under(first: &str, rest: &str) -> String {
2633        let mut path = PathBuf::from(if cfg!(windows) {
2634            format!("C:\\{first}")
2635        } else {
2636            format!("/{first}")
2637        });
2638        for part in rest.split('/').filter(|part| !part.is_empty()) {
2639            path.push(part);
2640        }
2641        path.to_string_lossy().into_owned()
2642    }
2643
2644    fn assertion_sources(run: &PythonFrontendRun) -> Vec<String> {
2645        run.request.raw_results[0]
2646            .phases
2647            .iter()
2648            .filter(|phase| phase.kind == "assertion")
2649            .filter_map(|phase| phase.source.clone())
2650            .collect()
2651    }
2652
2653    fn run_with_sites(
2654        name: &str,
2655        sites: &[serde_json::Value],
2656        outcome_file: Option<&str>,
2657        root: &str,
2658    ) -> PythonFrontendRun {
2659        let source = "def f(a):\n    return a\n";
2660        let obligations = build_python_obligations("m.py", source).unwrap();
2661        let mut outcome = json!({"t":"outcome","worker":"main","test":"tests/test_m.py::test_a","retry":0,"phase":"call","outcome":"passed","xfail":false});
2662        if let Some(file) = outcome_file {
2663            outcome["file"] = json!(file);
2664        }
2665        let mut lines = vec![
2666            json!({"t":"process","v":1,"run":"run-1","pid":1,"worker":"main","python":"3.14.4","executable":"python","argv":["pytest"]}),
2667            json!({"t":"phase","ctx":1,"at":5,"worker":"main","test":"tests/test_m.py::test_a","retry":0,"phase":"call"}),
2668            json!({"t":"assert","ctx":1}),
2669        ];
2670        lines.extend(sites.iter().cloned());
2671        lines.push(outcome);
2672        lines.push(json!({"t":"exit","at":9}));
2673        let directory = temporary(name);
2674        write_transport(&directory.join("main.1.mmap"), &lines, 0);
2675        let run = build_python_frontend_run(
2676            &obligations.manifest,
2677            &directory,
2678            "run-1",
2679            "now",
2680            0,
2681            Path::new(root),
2682        )
2683        .unwrap();
2684        validate_frontend_report_request(&run.declaration, &run.request).unwrap();
2685        fs::remove_dir_all(directory).unwrap();
2686        run
2687    }
2688
2689    const SLOT_DIGEST: &str = "0123456789abcdef0123456789abcdef";
2690
2691    /// Two obligations and one two-condition decision `a or b`, whose
2692    /// logical branch is decided by the vector: a 16-byte header, two
2693    /// obligation bytes, then the decision's `2 * 3^2` region bytes.
2694    fn slot_layout() -> serde_json::Value {
2695        json!({
2696            "version": 2, "digest": SLOT_DIGEST, "header": 16, "bytes": 36,
2697            "ids": ["s1", "s2"],
2698            "decisions": [{
2699                "start": 18, "width": 2, "id": "d",
2700                "outcomeTrue": "d:true", "outcomeFalse": "d:false",
2701                "logical": [{"evaluated": "or:evaluated", "shortCircuit": "or:short",
2702                             "previousLeaves": [0], "operandLeaves": [1]}]
2703            }]
2704        })
2705    }
2706
2707    /// A slot of `slot_layout` for `context`, with `set` bytes at 1.
2708    fn slot_bytes(context: u64, tag: Option<&str>, set: &[usize]) -> Vec<u8> {
2709        let mut bytes = vec![0_u8; 36];
2710        bytes[..8].copy_from_slice(&context.to_le_bytes());
2711        if let Some(tag) = tag {
2712            for index in 0..8 {
2713                bytes[8 + index] = u8::from_str_radix(&tag[index * 2..index * 2 + 2], 16).unwrap();
2714            }
2715        }
2716        for index in set {
2717            bytes[*index] = 1;
2718        }
2719        bytes
2720    }
2721
2722    /// A process killed during a test's call phase: its transport declares the
2723    /// phase and nothing after it, and its slot holds what it observed.
2724    fn killed_process(name: &str, slot: &[u8]) -> PathBuf {
2725        let directory = temporary(name);
2726        write_transport(
2727            &directory.join("main.1.token.mmap"),
2728            &[
2729                json!({"t":"process","v":1,"run":"run-1","pid":1,"worker":"main","python":"3.9.21","executable":"python","argv":["pytest"]}),
2730                json!({"t":"phase","ctx":1,"at":5,"worker":"main","test":"tests/test_m.py::test_a","retry":0,"phase":"call"}),
2731            ],
2732            0,
2733        );
2734        fs::write(directory.join("main.1.token.0.slot"), slot).unwrap();
2735        fs::write(
2736            directory.join("layout.json"),
2737            serde_json::to_vec(&slot_layout()).unwrap(),
2738        )
2739        .unwrap();
2740        directory
2741    }
2742
2743    fn call_observations(evidence: &Evidence) -> Observations {
2744        evidence
2745            .per_identity
2746            .iter()
2747            .find(|(identity, _)| identity.phase == "call")
2748            .map(|(_, observations)| Observations {
2749                // Slot hits named, as a snapshot names them.
2750                hits: observations
2751                    .hits
2752                    .iter()
2753                    .cloned()
2754                    .chain(
2755                        observations
2756                            .slot_hits
2757                            .iter()
2758                            .map(|position| evidence.slot_ids[*position as usize].clone()),
2759                    )
2760                    .collect(),
2761                slot_hits: Vec::new(),
2762                vectors: observations.vectors.clone(),
2763            })
2764            .unwrap_or_default()
2765    }
2766
2767    #[test]
2768    fn a_killed_process_slot_is_read_as_its_last_records() {
2769        // s1 ran; `a or b` evaluated a false, then b true -- digits "12", mask
2770        // 1 + 2*3 = 7 -- and came out true: region byte 18 + 2*7 + 1.
2771        let directory = killed_process(
2772            "py-slot-killed",
2773            &slot_bytes(1, Some(SLOT_DIGEST), &[16, 18 + 15]),
2774        );
2775        let evidence = read_evidence_directory(&directory, "run-1").unwrap();
2776        let call = call_observations(&evidence);
2777        assert_eq!(
2778            call.hits.iter().cloned().collect::<Vec<_>>(),
2779            ["d:true", "or:evaluated", "s1"],
2780            "the obligation, and what the vector implies"
2781        );
2782        assert_eq!(
2783            call.vectors["d"],
2784            BTreeSet::from([(vec![Some(false), Some(true)], true)])
2785        );
2786        fs::remove_dir_all(directory).unwrap();
2787    }
2788
2789    #[test]
2790    fn a_head_byte_is_read_with_the_bytes_it_stands_for() {
2791        // Byte 17 (s2) heads what bytes 16 (s1) and 18 + 2*7 + 1 (a vector
2792        // of d) stand for: the transform left those stores out because s2's
2793        // probe records them, and reading s2 reads them.
2794        let directory = temporary("py-slot-implied");
2795        write_transport(
2796            &directory.join("main.1.token.mmap"),
2797            &[
2798                json!({"t":"process","v":1,"run":"run-1","pid":1,"worker":"main","python":"3.9.21","executable":"python","argv":["pytest"]}),
2799                json!({"t":"phase","ctx":1,"at":5,"worker":"main","test":"tests/test_m.py::test_a","retry":0,"phase":"call"}),
2800                json!({"t":"runs","ctx":1,"r":[17, 1]}),
2801            ],
2802            0,
2803        );
2804        let mut layout = slot_layout();
2805        layout["implied"] = json!([[17, [16, 18 + 15]]]);
2806        fs::write(
2807            directory.join("layout.json"),
2808            serde_json::to_vec(&layout).unwrap(),
2809        )
2810        .unwrap();
2811        let evidence = read_evidence_directory(&directory, "run-1").unwrap();
2812        let call = call_observations(&evidence);
2813        assert_eq!(
2814            call.hits.iter().cloned().collect::<Vec<_>>(),
2815            ["d:true", "or:evaluated", "s1", "s2"]
2816        );
2817        assert_eq!(
2818            call.vectors["d"],
2819            BTreeSet::from([(vec![Some(false), Some(true)], true)])
2820        );
2821        // A byte may not imply a statement's own head, past the slot, or a
2822        // byte that implies others.
2823        for implied in [
2824            json!([[17, [17]]]),
2825            json!([[17, [36]]]),
2826            json!([[17, [16]], [16, [18]]]),
2827        ] {
2828            layout["implied"] = implied;
2829            fs::write(
2830                directory.join("layout.json"),
2831                serde_json::to_vec(&layout).unwrap(),
2832            )
2833            .unwrap();
2834            assert!(read_evidence_directory(&directory, "run-1").is_err());
2835        }
2836        fs::remove_dir_all(directory).unwrap();
2837    }
2838
2839    #[test]
2840    fn a_slot_gone_since_the_listing_is_skipped_and_nothing_else_is() {
2841        // Between listing the evidence and reading it, a process that outlived
2842        // the test command can close. A slot it took back is skipped at either
2843        // step; a transport, or anything else, that goes missing is still an
2844        // error rather than evidence quietly lost.
2845        let directory = temporary("py-slot-vanished");
2846        let gone = directory.join("main.1.token.0.slot");
2847        assert!(
2848            entry_metadata("main.1.token.0.slot", &gone)
2849                .unwrap()
2850                .is_none()
2851        );
2852        assert!(map_slot(&gone).unwrap().is_none());
2853        assert!(entry_metadata("main.1.token.mmap", &directory.join("main.1.token.mmap")).is_err());
2854        assert!(entry_metadata("layout.json", &directory.join("layout.json")).is_err());
2855        fs::write(&gone, slot_bytes(1, Some(SLOT_DIGEST), &[16])).unwrap();
2856        assert!(
2857            entry_metadata("main.1.token.0.slot", &gone)
2858                .unwrap()
2859                .is_some()
2860        );
2861        assert!(map_slot(&gone).unwrap().is_some());
2862        fs::remove_dir_all(directory).unwrap();
2863    }
2864
2865    #[test]
2866    fn a_slot_its_process_closed_is_not_counted() {
2867        // Close zeroes the tag after its final harvest; a daemon thread may
2868        // still have written after that, and nothing it wrote is a test's.
2869        let directory = killed_process("py-slot-closed", &slot_bytes(1, None, &[16]));
2870        let evidence = read_evidence_directory(&directory, "run-1").unwrap();
2871        assert!(call_observations(&evidence).hits.is_empty());
2872        fs::remove_dir_all(directory).unwrap();
2873    }
2874
2875    #[test]
2876    fn a_slot_is_read_only_against_its_own_layout() {
2877        let other = "fedcba9876543210fedcba9876543210";
2878        let directory = killed_process("py-slot-foreign", &slot_bytes(1, Some(other), &[16]));
2879        assert!(matches!(
2880            read_evidence_directory(&directory, "run-1"),
2881            Err(PythonEvidenceError::InvalidTransport { .. })
2882        ));
2883        fs::remove_dir_all(directory).unwrap();
2884
2885        // A byte that is neither set nor clear was not written by a probe.
2886        let directory = killed_process("py-slot-byte", &slot_bytes(1, Some(SLOT_DIGEST), &[]));
2887        let mut bytes = slot_bytes(1, Some(SLOT_DIGEST), &[]);
2888        bytes[17] = 7;
2889        fs::write(directory.join("main.1.token.0.slot"), bytes).unwrap();
2890        assert!(read_evidence_directory(&directory, "run-1").is_err());
2891        fs::remove_dir_all(directory).unwrap();
2892    }
2893
2894    #[test]
2895    fn a_slot_needs_its_transport_and_a_partial_layout_is_ignored() {
2896        let directory = killed_process("py-slot-orphan", &slot_bytes(1, Some(SLOT_DIGEST), &[16]));
2897        // A layout write cut short by the process dying is left behind.
2898        fs::write(directory.join("layout.json.42.token.partial"), b"{").unwrap();
2899        assert!(read_evidence_directory(&directory, "run-1").is_ok());
2900        fs::write(
2901            directory.join("other.9.token.0.slot"),
2902            slot_bytes(1, Some(SLOT_DIGEST), &[16]),
2903        )
2904        .unwrap();
2905        assert!(matches!(
2906            read_evidence_directory(&directory, "run-1"),
2907            Err(PythonEvidenceError::InvalidTransport { .. })
2908        ));
2909        fs::remove_dir_all(directory).unwrap();
2910    }
2911
2912    #[test]
2913    fn a_test_s_assertions_are_its_one_assertion_phase() {
2914        // The per-test assertion phase carries the evidence from the first
2915        // assertion on; there is no separate phase per assertion line.
2916        let run = run_with_sites("py-assertion-phase", &[], None, &under("project", ""));
2917        assert_eq!(
2918            assertion_sources(&run),
2919            vec!["tests/test_m.py::test_a".to_string()]
2920        );
2921    }
2922
2923    #[test]
2924    fn the_runner_names_the_test_file_in_either_path_form() {
2925        // pytest reports the file from the report's location and unittest from
2926        // the test's module; either may be absolute or relative. Both name the
2927        // same project file, and a runner that names none falls back to the
2928        // node id.
2929        let root = under("project", "");
2930        for reported in [
2931            under("project", "tests/test_m.py"),
2932            "tests/test_m.py".to_owned(),
2933            "./tests/test_m.py".to_owned(),
2934        ] {
2935            let run = run_with_sites("py-asite-file", &[], Some(reported.as_str()), &root);
2936            assert_eq!(
2937                run.request.raw_results[0].test_file.as_deref(),
2938                Some("tests/test_m.py"),
2939                "{reported} should resolve to the project path"
2940            );
2941        }
2942        let without = run_with_sites("py-asite-nofile", &[], None, &root);
2943        assert_eq!(
2944            without.request.raw_results[0].test_file.as_deref(),
2945            Some("tests/test_m.py"),
2946            "a pytest node id already begins with the file"
2947        );
2948    }
2949
2950    /// A run with every kind of phase evidence: setup, a call phase split by
2951    /// its first assertion, teardown, background, and decision vectors on
2952    /// both sides of the assertion.
2953    fn phased_run(name: &str) -> PythonFrontendRun {
2954        let source = "def f(a, b):\n    if a and b:\n        return 1\n    return 2\n";
2955        let obligations = build_python_obligations("m.py", source).unwrap();
2956        let statements = &obligations.plan.statements;
2957        let decision = &obligations.manifest.decisions[0].id;
2958        let test = "tests/test_m.py::test_a";
2959        let lines = [
2960            json!({"t":"process","v":1,"run":"run-1","pid":1,"worker":"main","python":"3.14.4","executable":"python","argv":["pytest"]}),
2961            json!({"t":"hit","ctx":0,"id":statements[0].id}),
2962            json!({"t":"phase","ctx":1,"at":5,"worker":"main","test":test,"retry":0,"phase":"setup"}),
2963            json!({"t":"hits","ctx":1,"ids":[statements[0].id, statements[1].id]}),
2964            json!({"t":"outcome","worker":"main","test":test,"retry":0,"phase":"setup","outcome":"passed","xfail":false}),
2965            json!({"t":"phase","ctx":2,"at":6,"worker":"main","test":test,"retry":0,"phase":"call"}),
2966            json!({"t":"hit","ctx":2,"id":statements[1].id}),
2967            json!({"t":"dec","ctx":2,"id":decision,"v":"22","o":1}),
2968            json!({"t":"assert","ctx":2}),
2969            json!({"t":"hit","ctx":2,"id":statements[2].id}),
2970            json!({"t":"dec","ctx":2,"id":decision,"v":"21","o":0}),
2971            json!({"t":"outcome","worker":"main","test":test,"retry":0,"phase":"call","outcome":"passed","xfail":false}),
2972            json!({"t":"phase","ctx":3,"at":7,"worker":"main","test":test,"retry":0,"phase":"teardown"}),
2973            json!({"t":"hit","ctx":3,"id":statements[2].id}),
2974            json!({"t":"outcome","worker":"main","test":test,"retry":0,"phase":"teardown","outcome":"passed","xfail":false}),
2975            json!({"t":"exit","at":9}),
2976        ];
2977        let directory = temporary(name);
2978        write_transport(&directory.join("main.1.mmap"), &lines, 0);
2979        let run = build_python_frontend_run(
2980            &obligations.manifest,
2981            &directory,
2982            "run-1",
2983            "now",
2984            0,
2985            Path::new(""),
2986        )
2987        .unwrap();
2988        validate_frontend_report_request(&run.declaration, &run.request).unwrap();
2989        fs::remove_dir_all(directory).unwrap();
2990        run
2991    }
2992
2993    #[test]
2994    fn a_snapshot_that_names_its_phase_analyses_as_the_events_it_replaces() {
2995        // Python names a snapshot's phase once instead of writing an explicit
2996        // event per hit and vector. Every count and relation the analysis
2997        // derives must be the one those events gave.
2998        use crate::coverage_report::{RuntimeEvent, analyze_coverage_results};
2999        let run = phased_run("py-phase-named");
3000        let named = &run.request;
3001        assert!(
3002            named
3003                .raw_results
3004                .iter()
3005                .flat_map(|raw| &raw.runtime)
3006                .filter(|snapshot| snapshot.phase_id.is_some())
3007                .count()
3008                >= 4,
3009            "setup, call, assertion and teardown each name their phase"
3010        );
3011        let mut events = named.clone();
3012        for snapshot in events
3013            .raw_results
3014            .iter_mut()
3015            .flat_map(|raw| raw.runtime.iter_mut())
3016        {
3017            let Some(phase) = snapshot.phase_id.take() else {
3018                continue;
3019            };
3020            let mut clock = 1;
3021            let mut event = |event_type: &str, id: &str, vector: Option<McdcVector>| {
3022                clock += 1;
3023                RuntimeEvent {
3024                    event_type: event_type.into(),
3025                    id: id.into(),
3026                    vector,
3027                    timestamp_ms: clock,
3028                    phase_id: Some(phase.clone()),
3029                    statement_id: None,
3030                    environment: "python".into(),
3031                }
3032            };
3033            let mut written = snapshot
3034                .hits
3035                .iter()
3036                .map(|hit| event("hit", hit, None))
3037                .collect::<Vec<_>>();
3038            for decision in &snapshot.decisions {
3039                for vector in &decision.vectors {
3040                    written.push(event("decision", &decision.meta.id, Some(vector.clone())));
3041                }
3042            }
3043            snapshot.events = written;
3044        }
3045        let from_names = analyze_coverage_results(named).unwrap();
3046        let from_events = analyze_coverage_results(&events).unwrap();
3047        assert!(
3048            from_names
3049                .view
3050                .phases
3051                .iter()
3052                .any(|phase| phase.explicit_events > 1),
3053            "the fixture's phases carry explicit evidence"
3054        );
3055        assert_eq!(
3056            serde_json::to_value(&from_names).unwrap(),
3057            serde_json::to_value(&from_events).unwrap()
3058        );
3059    }
3060
3061    #[test]
3062    fn analysing_the_archived_request_is_analysing_the_archive() {
3063        // Publication analyses the request it has just archived instead of
3064        // reading the archive back. The two must agree to the byte.
3065        use crate::coverage_report::{
3066            ArchiveReportRequest, ExitCodeInput, TransportStats, analyze_coverage_archive,
3067            analyze_frontend_request,
3068        };
3069        let run = phased_run("py-archived");
3070        let directory = temporary("py-archived-archive");
3071        let archive = directory.join("evidence.raw.gz");
3072        crate::evidence_archive::write_archive(run.archive_entries().unwrap(), &archive).unwrap();
3073        let integrity = json!({"schemaVersion": 2});
3074        let from_archive = analyze_coverage_archive(&ArchiveReportRequest {
3075            archive_path: archive,
3076            run_id: "run-1".into(),
3077            generated_at: "then".into(),
3078            integrity: Some(integrity.clone()),
3079            test_exit_code: ExitCodeInput::Present(Some(0)),
3080        })
3081        .unwrap();
3082        let in_memory = analyze_frontend_request(
3083            &run.declaration,
3084            &CoverageReportRequest {
3085                run_id: "run-1".into(),
3086                generated_at: "then".into(),
3087                integrity: Some(integrity),
3088                test_exit_code: ExitCodeInput::Present(Some(0)),
3089                ..run.request.clone()
3090            },
3091            TransportStats::none(),
3092        )
3093        .unwrap();
3094        fs::remove_dir_all(directory).unwrap();
3095        assert_eq!(
3096            serde_json::to_value(&in_memory).unwrap(),
3097            serde_json::to_value(&from_archive).unwrap()
3098        );
3099    }
3100
3101    #[test]
3102    fn evidence_before_the_first_assertion_links_to_it_when_the_test_passes() {
3103        // The runtime's marker says everything the call phase recorded so far
3104        // ran before an assertion. That evidence carries an assertion phase
3105        // that passed with the test; what ran after the marker, and all of a
3106        // test that failed, stays execution only. A second marker is inert.
3107        let source = "def f(a):\n    return a\n\n\ndef g(b):\n    return b\n";
3108        let obligations = build_python_obligations("m.py", source).unwrap();
3109        let before = &obligations.plan.statements[0].id;
3110        let after = &obligations.plan.statements[1].id;
3111        let run_with = |name: &str, outcome: &str| {
3112            let lines = [
3113                json!({"t":"process","v":1,"run":"run-1","pid":1,"worker":"main","python":"3.14.4","executable":"python","argv":["pytest"]}),
3114                json!({"t":"phase","ctx":1,"at":5,"worker":"main","test":"tests/test_m.py::test_a","retry":0,"phase":"call"}),
3115                json!({"t":"hit","ctx":1,"id":before}),
3116                json!({"t":"assert","ctx":1}),
3117                json!({"t":"assert","ctx":1}),
3118                json!({"t":"hit","ctx":1,"id":after}),
3119                json!({"t":"outcome","worker":"main","test":"tests/test_m.py::test_a","retry":0,"phase":"call","outcome":outcome,"xfail":false}),
3120                json!({"t":"exit","at":9}),
3121            ];
3122            let directory = temporary(name);
3123            write_transport(&directory.join("main.1.mmap"), &lines, 0);
3124            let run = build_python_frontend_run(
3125                &obligations.manifest,
3126                &directory,
3127                "run-1",
3128                "now",
3129                0,
3130                Path::new(""),
3131            )
3132            .unwrap();
3133            validate_frontend_report_request(&run.declaration, &run.request).unwrap();
3134            fs::remove_dir_all(directory).unwrap();
3135            run
3136        };
3137        let events_of = |result: &RawTestResult, phase: &str| -> BTreeSet<String> {
3138            result
3139                .runtime
3140                .iter()
3141                .filter(|snapshot| snapshot.phase_id.as_deref() == Some(phase))
3142                .flat_map(|snapshot| snapshot.hits.iter().cloned())
3143                .collect()
3144        };
3145
3146        let run = run_with("asserted-passed", "passed");
3147        let passed = &run.request.raw_results[0];
3148        assert_eq!(passed.test, "tests/test_m.py::test_a");
3149        let assertion = passed
3150            .phases
3151            .iter()
3152            .find(|phase| phase.kind == "assertion")
3153            .expect("the asserting test carries an assertion phase");
3154        assert_eq!(assertion.status.as_deref(), Some("passed"));
3155        assert_eq!(
3156            events_of(passed, &assertion.id),
3157            BTreeSet::from([before.clone()]),
3158            "only what ran before the marker is the assertion's evidence"
3159        );
3160        let test_phase = passed
3161            .phases
3162            .iter()
3163            .find(|phase| phase.kind == "test")
3164            .unwrap();
3165        assert_eq!(
3166            events_of(passed, &test_phase.id),
3167            BTreeSet::from([before.clone(), after.clone()]),
3168            "the test phase keeps everything it ran"
3169        );
3170        assert_eq!(
3171            run.declaration.runners[0].attribution.assertion,
3172            AttributionPrecision::Exact
3173        );
3174
3175        let run = run_with("asserted-failed", "failed");
3176        let failed = &run.request.raw_results[0];
3177        let assertion = failed
3178            .phases
3179            .iter()
3180            .find(|phase| phase.kind == "assertion")
3181            .unwrap();
3182        assert_eq!(
3183            assertion.status.as_deref(),
3184            Some("failed"),
3185            "a failed test's assertion witnessed nothing"
3186        );
3187    }
3188
3189    #[test]
3190    fn repeated_runtime_limitations_keep_each_affected_file_once() {
3191        let mut manifest = build_python_obligations("a.py", "x = 1\n")
3192            .unwrap()
3193            .manifest;
3194        let second = build_python_obligations("b.py", "y = 2\n")
3195            .unwrap()
3196            .manifest;
3197        manifest.points.extend(second.points);
3198        let directory = temporary("repeated-limitations");
3199        let mut records = vec![
3200            json!({"t":"process","v":1,"run":"run-1","pid":1,"worker":"main","python":"3.11","executable":"python","argv":[]}),
3201            json!({"t":"outcome","worker":"main","test":"test_a","retry":0,"phase":"call","outcome":"passed","xfail":false}),
3202        ];
3203        for _ in 0..1000 {
3204            for file in ["a.py", "b.py"] {
3205                records.push(json!({"t":"limitation","id":"python-probes-unobserved-module","file":file,"reason":"entry script"}));
3206            }
3207        }
3208        write_transport(&directory.join("main.1.mmap"), &records, 0);
3209        let evidence = read_evidence_directory(&directory, "run-1").unwrap();
3210        assert_eq!(
3211            evidence.limitations.len(),
3212            2,
3213            "deduplicate before manifest expansion"
3214        );
3215        let run =
3216            build_python_frontend_run(&manifest, &directory, "run-1", "now", 0, Path::new(""))
3217                .unwrap();
3218        validate_frontend_report_request(&run.declaration, &run.request).unwrap();
3219        let limitations = &run.request.manifest.limitations;
3220        assert_eq!(limitations.len(), 2);
3221        assert_ne!(limitations[0]["id"], limitations[1]["id"]);
3222        assert_eq!(limitations[0]["file"], "a.py");
3223        assert_eq!(limitations[1]["file"], "b.py");
3224        assert_eq!(limitations[0]["code"], "python-probes-unobserved-module");
3225        assert_eq!(run.request.manifest.unmeasured.len(), 2);
3226        fs::remove_dir_all(directory).unwrap();
3227    }
3228
3229    #[test]
3230    fn a_script_run_unobserved_stays_measured_where_its_imports_were_observed() {
3231        // issue #40: tests import `bin/anti-drift-gate.py` and also run it as
3232        // `python bin/anti-drift-gate.py`. The direct run made the whole file
3233        // unmeasured, discarding what the imports were seen to run.
3234        let imported = build_python_obligations("imported.py", "def f():\n    return 1\n").unwrap();
3235        let only_run = build_python_obligations("only_run.py", "y = 2\n").unwrap();
3236        let mut manifest = imported.manifest.clone();
3237        manifest.points.extend(only_run.manifest.points.clone());
3238        let statement = &imported.plan.statements[0];
3239        let directory = temporary("script-and-import");
3240        let records = vec![
3241            json!({"t":"process","v":1,"run":"run-1","pid":1,"worker":"main","python":"3.11","executable":"python","argv":["pytest"]}),
3242            json!({"t":"phase","ctx":1,"at":5,"worker":"main","test":"tests/test_m.py::test_a","retry":0,"phase":"call"}),
3243            json!({"t":"hit","ctx":1,"id":statement.id}),
3244            json!({"t":"outcome","worker":"main","test":"tests/test_m.py::test_a","retry":0,"phase":"call","outcome":"passed","xfail":false}),
3245            json!({"t":"limitation","id":"python-probes-unobserved-module","file":"imported.py","reason":"entry script"}),
3246            json!({"t":"limitation","id":"python-probes-unobserved-module","file":"only_run.py","reason":"entry script"}),
3247        ];
3248        write_transport(&directory.join("main.1.mmap"), &records, 0);
3249        let run =
3250            build_python_frontend_run(&manifest, &directory, "run-1", "now", 0, Path::new(""))
3251                .unwrap();
3252        let unmeasured = &run.request.manifest.unmeasured;
3253        let file_points = |file: &str| {
3254            manifest
3255                .points
3256                .iter()
3257                .filter(|point| point.file == file)
3258                .map(|point| point.id.clone())
3259                .collect::<Vec<_>>()
3260        };
3261        assert!(
3262            file_points("imported.py")
3263                .iter()
3264                .all(|id| !unmeasured.contains(id)),
3265            "an observed file keeps its measurement"
3266        );
3267        assert!(
3268            file_points("only_run.py")
3269                .iter()
3270                .all(|id| unmeasured.contains(id)),
3271            "a file nothing observed stays unmeasured"
3272        );
3273        // Both still say so: the imported file's numbers are a lower bound.
3274        let files = run
3275            .request
3276            .manifest
3277            .limitations
3278            .iter()
3279            .map(|limitation| limitation["file"].as_str().unwrap().to_owned())
3280            .collect::<BTreeSet<_>>();
3281        assert!(files.contains("imported.py") && files.contains("only_run.py"));
3282        fs::remove_dir_all(directory).unwrap();
3283    }
3284
3285    #[test]
3286    fn joins_phases_outcomes_hits_and_vectors_into_exact_results() {
3287        let source = "def f(a, b):\n    if a and b:\n        return 1\n    return 0\n";
3288        let obligations = build_python_obligations("m.py", source).unwrap();
3289        let decision = &obligations.plan.decisions[0];
3290        let statement = &obligations.plan.statements[0];
3291        let directory = temporary("join");
3292        let lines = [
3293            json!({"t":"process","v":1,"run":"run-1","pid":1,"worker":"main","python":"3.14.4","executable":"python","argv":["pytest"]}),
3294            json!({"t":"hit","ctx":0,"id":statement.id}),
3295            json!({"t":"phase","ctx":1,"at":5,"worker":"main","test":"tests/test_m.py::test_a","retry":0,"phase":"call"}),
3296            json!({"t":"dec","ctx":1,"id":decision.id,"v":"22","o":1}),
3297            json!({"t":"hit","ctx":1,"id":decision.outcome_true}),
3298            json!({"t":"outcome","worker":"main","test":"tests/test_m.py::test_a","retry":0,"phase":"setup","outcome":"passed","xfail":false}),
3299            json!({"t":"outcome","worker":"main","test":"tests/test_m.py::test_a","retry":0,"phase":"call","outcome":"passed","xfail":false}),
3300            json!({"t":"outcome","worker":"main","test":"tests/test_m.py::test_a","retry":0,"phase":"teardown","outcome":"passed","xfail":false}),
3301            json!({"t":"limitation","id":"python-decision-partially-mapped","reason":"folded","obligation":decision.id}),
3302            json!({"t":"exit","at":9}),
3303        ];
3304        write_transport(&directory.join("main.1.mmap"), &lines, 0);
3305        let run = build_python_frontend_run(
3306            &obligations.manifest,
3307            &directory,
3308            "run-1",
3309            "now",
3310            0,
3311            Path::new(""),
3312        )
3313        .unwrap();
3314        validate_frontend_report_request(&run.declaration, &run.request).unwrap();
3315        assert_eq!(run.tests, 1);
3316        assert_eq!(run.request.raw_results.len(), 2);
3317        let test = &run.request.raw_results[0];
3318        assert_eq!(test.status.as_deref(), Some("passed"));
3319        assert_eq!(test.phases.len(), 3);
3320        assert_eq!(test.runtime.len(), 1);
3321        assert_eq!(test.runtime[0].decisions.len(), 1);
3322        assert!(test.runtime[0].phase_id.is_some());
3323        assert!(test.runtime[0].events.is_empty());
3324        let background = &run.request.raw_results[1];
3325        assert_eq!(background.role, "background");
3326        assert!(run.request.manifest.unmeasured.contains(&decision.id));
3327        assert!(run.declaration.structural_limitations.contains(&stable_id(
3328            "python-decision-partially-mapped",
3329            &["", &decision.id]
3330        )));
3331        fs::remove_dir_all(directory).unwrap();
3332    }
3333
3334    #[test]
3335    fn a_damaged_record_is_refused_with_what_is_wrong() {
3336        // The runtime writes none of these. Another version of it, another
3337        // run's leftovers or a damaged write can, and each is refused rather
3338        // than read as evidence.
3339        let source = "def f(a, b):\n    if a and b:\n        return 1\n    return 0\n";
3340        let obligations = build_python_obligations("m.py", source).unwrap();
3341        let decision = obligations.plan.decisions[0].id.clone();
3342        let process = json!({"t":"process","v":1,"run":"run-1","pid":1,"worker":"main","python":"3.14.4","executable":"python","argv":["pytest"]});
3343        let phase = json!({"t":"phase","ctx":1,"at":5,"worker":"main","test":"t::a","retry":0,"phase":"call"});
3344        let outcome = json!({"t":"outcome","worker":"main","test":"t::a","retry":0,"phase":"call","outcome":"passed","xfail":false});
3345        let with = |record: &serde_json::Value, key: &str, value: serde_json::Value| {
3346            let mut changed = record.clone();
3347            changed[key] = value;
3348            changed
3349        };
3350        let read = |name: &str, records: Vec<serde_json::Value>| {
3351            let directory = temporary(name);
3352            write_transport(&directory.join("main.1.mmap"), &records, 0);
3353            let said = match build_python_frontend_run(
3354                &obligations.manifest,
3355                &directory,
3356                "run-1",
3357                "now",
3358                0,
3359                Path::new(""),
3360            ) {
3361                Ok(_) => "accepted".to_owned(),
3362                Err(error) => error.to_string(),
3363            };
3364            fs::remove_dir_all(directory).unwrap();
3365            said
3366        };
3367        let exit = json!({"t":"exit","at":9});
3368        assert_eq!(
3369            read(
3370                "record-valid",
3371                vec![
3372                    process.clone(),
3373                    phase.clone(),
3374                    outcome.clone(),
3375                    exit.clone()
3376                ]
3377            ),
3378            "accepted"
3379        );
3380        for (name, records, reason) in [
3381            (
3382                "record-version",
3383                vec![with(&process, "v", json!(2))],
3384                "unsupported Python evidence version 2",
3385            ),
3386            (
3387                "record-run",
3388                vec![with(&process, "run", json!("run-0"))],
3389                "belongs to run run-0, expected run-1",
3390            ),
3391            (
3392                "record-pid",
3393                vec![with(&process, "pid", json!(7))],
3394                "does not match the transport owner",
3395            ),
3396            (
3397                "record-python",
3398                vec![with(&process, "python", json!("3.8.1"))],
3399                "the test command ran Python 3.8.1",
3400            ),
3401            (
3402                "record-context",
3403                vec![process.clone(), with(&phase, "ctx", json!(0))],
3404                "phase context 0 is reserved",
3405            ),
3406            (
3407                "record-phase",
3408                vec![process.clone(), with(&phase, "phase", json!("body"))],
3409                "unknown pytest phase",
3410            ),
3411            (
3412                "record-identity",
3413                vec![process.clone(), with(&phase, "test", json!(" "))],
3414                "must name a worker and test",
3415            ),
3416            (
3417                "record-outcome-phase",
3418                vec![process.clone(), with(&outcome, "phase", json!("body"))],
3419                "unknown test outcome phase",
3420            ),
3421            (
3422                "record-outcome",
3423                vec![process.clone(), with(&outcome, "outcome", json!("maybe"))],
3424                "unknown test outcome",
3425            ),
3426            (
3427                "record-runner",
3428                vec![process.clone(), with(&outcome, "runner", json!("nose"))],
3429                "unknown Python test runner",
3430            ),
3431            (
3432                "record-two-runners",
3433                vec![
3434                    process.clone(),
3435                    outcome.clone(),
3436                    with(&outcome, "runner", json!("unittest")),
3437                ],
3438                "one attempt was reported by two runners",
3439            ),
3440            (
3441                "record-digits",
3442                vec![
3443                    process.clone(),
3444                    phase.clone(),
3445                    json!({"t":"dec","ctx":1,"id":decision,"v":"23","o":1}),
3446                ],
3447                "decision vector digits must be 0, 1 or 2",
3448            ),
3449            (
3450                "record-decision-outcome",
3451                vec![
3452                    process.clone(),
3453                    phase.clone(),
3454                    json!({"t":"dec","ctx":1,"id":decision,"v":"22","o":2}),
3455                ],
3456                "decision outcome must be 0 or 1",
3457            ),
3458            (
3459                "record-batch-digits",
3460                vec![
3461                    process.clone(),
3462                    phase.clone(),
3463                    json!({"t":"decs","ctx":1,"v":[[decision, "", 1]]}),
3464                ],
3465                "decision vector digits must be 0, 1 or 2",
3466            ),
3467            (
3468                "record-batch-outcome",
3469                vec![
3470                    process.clone(),
3471                    phase.clone(),
3472                    json!({"t":"decs","ctx":1,"v":[[decision, "21", 3]]}),
3473                ],
3474                "decision outcome must be 0 or 1",
3475            ),
3476            (
3477                "record-unknown-hit",
3478                vec![
3479                    process.clone(),
3480                    phase.clone(),
3481                    json!({"t":"hit","ctx":1,"id":"py:nowhere"}),
3482                    outcome.clone(),
3483                    exit.clone(),
3484                ],
3485                "unknown obligation: py:nowhere",
3486            ),
3487            (
3488                "record-unknown-decision",
3489                vec![
3490                    process.clone(),
3491                    phase.clone(),
3492                    json!({"t":"dec","ctx":1,"id":"py:nodecision","v":"22","o":1}),
3493                    outcome.clone(),
3494                    exit.clone(),
3495                ],
3496                "unknown obligation: py:nodecision",
3497            ),
3498            (
3499                "record-short-vector",
3500                vec![
3501                    process.clone(),
3502                    phase.clone(),
3503                    json!({"t":"dec","ctx":1,"id":decision,"v":"2","o":1}),
3504                    outcome.clone(),
3505                    exit.clone(),
3506                ],
3507                "reported 1 condition values, expected 2",
3508            ),
3509        ] {
3510            let said = read(name, records);
3511            assert!(said.contains(reason), "{name}: {said}");
3512        }
3513    }
3514
3515    // Version 2 checksums with CRC-32, version 1 with FNV-1a. A runtime older
3516    // than this reader -- evidence kept from a failed run, say -- still reads,
3517    // by its own checksum; a payload that does not match its checksum, and a
3518    // version this reader does not know, are refused rather than guessed at.
3519    #[test]
3520    fn a_transport_reads_by_its_own_versions_checksum() {
3521        let obligations = build_python_obligations("m.py", "x = 1\n").unwrap();
3522        let process = json!({"t":"process","v":1,"run":"run-1","pid":1,"worker":"main","python":"3.14.4","executable":"p","argv":[]});
3523        let read = |name: &str, version: u32, corrupt: Option<usize>| {
3524            let directory = temporary(name);
3525            write_transport_version(
3526                &directory.join("main.1.mmap"),
3527                std::slice::from_ref(&process),
3528                0,
3529                version,
3530                corrupt,
3531            );
3532            let outcome = build_python_frontend_run(
3533                &obligations.manifest,
3534                &directory,
3535                "run-1",
3536                "now",
3537                0,
3538                Path::new(""),
3539            );
3540            fs::remove_dir_all(directory).unwrap();
3541            outcome.map(|_| ()).map_err(|error| error.to_string())
3542        };
3543        let current = read("checksum-v2", TRANSPORT_VERSION, None);
3544        let older = read("checksum-v1", TRANSPORT_VERSION_FNV, None);
3545        assert_eq!(
3546            older, current,
3547            "a version 1 transport reads as a version 2 one does"
3548        );
3549        assert!(
3550            !current
3551                .as_ref()
3552                .err()
3553                .is_some_and(|error| error.contains("checksum")),
3554            "{current:?}"
3555        );
3556        let corrupted = read("checksum-bad", TRANSPORT_VERSION, Some(0)).unwrap_err();
3557        assert!(corrupted.contains("checksum does not match"), "{corrupted}");
3558        let unknown = read("checksum-v3", TRANSPORT_VERSION + 1, None).unwrap_err();
3559        assert!(unknown.contains("version"), "{unknown}");
3560    }
3561
3562    #[test]
3563    fn mixed_pytest_and_unittest_runs_have_distinct_runner_limitations() {
3564        let obligations = build_python_obligations("m.py", "def f():\n    return 1\n").unwrap();
3565        let directory = temporary("mixed-runners");
3566        let mut records = vec![json!({"t":"process","v":1,"run":"run-1","pid":1,
3567            "worker":"main","python":"3.11.11","executable":"python","argv":["pytest"]})];
3568        for (index, runner) in [PYTEST_RUNNER, UNITTEST_RUNNER].iter().enumerate() {
3569            let test = format!("tests/test_m.py::test_{runner}");
3570            records.push(
3571                json!({"t":"phase","ctx":index+1,"at":index+5,"worker":"main",
3572                "test":test,"retry":0,"phase":"call"}),
3573            );
3574            records.push(json!({"t":"hit","ctx":index+1,"id":obligations.plan.statements[0].id}));
3575            records.push(json!({"t":"outcome","worker":"main","test":test,"retry":0,
3576                "phase":"call","outcome":"passed","xfail":false,"runner":runner}));
3577        }
3578        write_transport(&directory.join("main.1.mmap"), &records, 0);
3579        let run = build_python_frontend_run(
3580            &obligations.manifest,
3581            &directory,
3582            "run-1",
3583            "now",
3584            0,
3585            Path::new(""),
3586        )
3587        .unwrap();
3588        assert_eq!(run.tests, 2);
3589        assert_eq!(run.declaration.runners.len(), 2);
3590        validate_frontend_report_request(&run.declaration, &run.request).unwrap();
3591        for runner in &run.declaration.runners {
3592            assert_eq!(runner.limitations.len(), 1);
3593            assert_eq!(runner.attribution.action, AttributionPrecision::Unavailable);
3594            assert_eq!(
3595                runner.limitations[0].scopes,
3596                [FrontendLimitationScope::Action]
3597            );
3598        }
3599        fs::remove_dir_all(directory).unwrap();
3600    }
3601
3602    #[test]
3603    fn a_phase_its_runner_reported_more_than_once_is_one_phase() {
3604        // pytest-subtests reports every subtest under the same node id and the
3605        // same `call` phase, so an attempt carries several outcomes for the
3606        // identity a phase id is derived from. Each became its own phase with
3607        // the same id, and the run was refused whole -- a suite of 6872 tests
3608        // lost two and three-quarter hours of measurement to it (#40). The
3609        // outcomes fold the way an attempt's do: a failure anywhere is the
3610        // phase's.
3611        let source = "def f(a):\n    return a\n";
3612        let obligations = build_python_obligations("m.py", source).unwrap();
3613        let directory = temporary("subtests");
3614        let subtest = |outcome: &str| {
3615            json!({"t":"outcome","worker":"main","test":"tests/test_m.py::test_many","retry":0,
3616                   "phase":"call","outcome":outcome,"xfail":false})
3617        };
3618        let lines = [
3619            json!({"t":"process","v":1,"run":"run-1","pid":1,"worker":"main","python":"3.14.4","executable":"python","argv":["pytest"]}),
3620            json!({"t":"phase","ctx":1,"at":5,"worker":"main","test":"tests/test_m.py::test_many","retry":0,"phase":"call"}),
3621            json!({"t":"hit","ctx":1,"id":obligations.plan.statements[0].id}),
3622            json!({"t":"outcome","worker":"main","test":"tests/test_m.py::test_many","retry":0,"phase":"setup","outcome":"passed","xfail":false}),
3623            subtest("passed"),
3624            subtest("passed"),
3625            subtest("failed"),
3626            subtest("passed"),
3627            json!({"t":"outcome","worker":"main","test":"tests/test_m.py::test_many","retry":0,"phase":"teardown","outcome":"passed","xfail":false}),
3628            json!({"t":"exit","at":9}),
3629        ];
3630        write_transport(&directory.join("main.1.mmap"), &lines, 0);
3631        let run = build_python_frontend_run(
3632            &obligations.manifest,
3633            &directory,
3634            "run-1",
3635            "now",
3636            1,
3637            Path::new(""),
3638        )
3639        .unwrap();
3640        // The protocol refuses a repeated phase id, which is what threw the
3641        // run away; nothing may reach it twice.
3642        validate_frontend_report_request(&run.declaration, &run.request).unwrap();
3643        let test = &run.request.raw_results[0];
3644        assert_eq!(
3645            test.phases.len(),
3646            3,
3647            "setup, one call, teardown: {:?}",
3648            test.phases
3649        );
3650        assert_eq!(
3651            test.phases
3652                .iter()
3653                .filter(|phase| phase.kind == "test")
3654                .count(),
3655            1,
3656            "the subtests share the call phase their coverage was recorded under"
3657        );
3658        assert_eq!(
3659            test.status.as_deref(),
3660            Some("failed"),
3661            "a subtest that failed fails its test"
3662        );
3663        // And the coverage recorded under that identity survives the fold.
3664        assert_eq!(test.runtime.len(), 1);
3665        assert!(!test.runtime[0].hits.is_empty());
3666        fs::remove_dir_all(directory).unwrap();
3667    }
3668
3669    #[test]
3670    fn fails_closed_without_an_interpreter_or_tests() {
3671        let obligations = build_python_obligations("m.py", "x = 1\n").unwrap();
3672        let directory = temporary("empty");
3673        assert!(matches!(
3674            build_python_frontend_run(
3675                &obligations.manifest,
3676                &directory,
3677                "run-1",
3678                "now",
3679                0,
3680                Path::new("")
3681            ),
3682            Err(PythonEvidenceError::NoInterpreter)
3683        ));
3684        let path = directory.join("main.1.mmap");
3685        write_transport(
3686            &path,
3687            &[
3688                json!({"t":"process","v":1,"run":"run-1","pid":1,"worker":"main","python":"3.14.4","executable":"p","argv":[]}),
3689            ],
3690            0,
3691        );
3692        assert!(matches!(
3693            build_python_frontend_run(
3694                &obligations.manifest,
3695                &directory,
3696                "run-1",
3697                "now",
3698                0,
3699                Path::new("")
3700            ),
3701            Err(PythonEvidenceError::NoTests)
3702        ));
3703        // A killed writer may have copied part of its next payload without
3704        // publishing the commit byte. The reader must stop at that frame,
3705        // even when the tail would be invalid JSON if treated as committed.
3706        let mut torn = fs::read(&path).unwrap();
3707        let process_length = transport_u32(&torn, TRANSPORT_HEADER_SIZE + 4).unwrap() as usize;
3708        let torn_cursor =
3709            align_transport(TRANSPORT_HEADER_SIZE + TRANSPORT_RECORD_HEADER_SIZE + process_length)
3710                .unwrap();
3711        torn[torn_cursor + 4..torn_cursor + 8].copy_from_slice(&5_u32.to_le_bytes());
3712        torn[torn_cursor + TRANSPORT_RECORD_HEADER_SIZE
3713            ..torn_cursor + TRANSPORT_RECORD_HEADER_SIZE + 5]
3714            .copy_from_slice(b"{nope");
3715        fs::write(&path, torn).unwrap();
3716        assert!(matches!(
3717            build_python_frontend_run(
3718                &obligations.manifest,
3719                &directory,
3720                "run-1",
3721                "now",
3722                0,
3723                Path::new("")
3724            ),
3725            Err(PythonEvidenceError::NoTests)
3726        ));
3727        write_transport(
3728            &path,
3729            &[
3730                json!({"t":"process","v":1,"run":"run-1","pid":1,"worker":"main","python":"3.8.20","executable":"p","argv":[]}),
3731            ],
3732            0,
3733        );
3734        assert!(matches!(
3735            build_python_frontend_run(
3736                &obligations.manifest,
3737                &directory,
3738                "run-1",
3739                "now",
3740                0,
3741                Path::new("")
3742            ),
3743            Err(PythonEvidenceError::UnsupportedPython(_))
3744        ));
3745        write_transport(
3746            &path,
3747            &[
3748                json!({"t":"process","v":1,"run":"run-1","pid":1,"worker":"main","python":"3.14.4","executable":"p","argv":[]}),
3749            ],
3750            2,
3751        );
3752        assert!(matches!(
3753            build_python_frontend_run(
3754                &obligations.manifest,
3755                &directory,
3756                "run-1",
3757                "now",
3758                0,
3759                Path::new("")
3760            ),
3761            Err(PythonEvidenceError::DroppedRecords { count: 2, .. })
3762        ));
3763        fs::remove_dir_all(directory).unwrap();
3764    }
3765}