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