Skip to main content

supercov_engine/
rust_test_runner.rs

1//! Stable Cargo/libtest execution for the owned Rust frontend.
2//!
3//! Source preparation happens in an isolated workspace. For `cargo test`,
4//! Cargo builds each test artifact once and every libtest case then runs in a
5//! process of its own with an evidence directory of its own, so attribution
6//! is exact by construction; doctests run through `rust_owned_doctests`, with
7//! this program standing in for rustdoc. For `cargo nextest run`, nextest
8//! keeps its own scheduling and retries and this program serves as its target
9//! runner (`rust_owned_nextest`), recording each attempt it launches.
10
11use std::{
12    collections::{BTreeMap, BTreeSet},
13    ffi::OsString,
14    fs,
15    io::Write,
16    path::{Component, Path, PathBuf},
17    process::{Command, Output},
18    sync::{
19        Mutex,
20        atomic::{AtomicUsize, Ordering},
21    },
22    time::Instant,
23};
24
25use serde::Deserialize;
26use supercov_contracts::{
27    AttributionPrecision, ExecutionModel, FrontendAttribution, FrontendLimitation,
28    FrontendLimitationScope, FrontendRunDeclaration, FrontendRunnerDeclaration,
29    LANGUAGE_FRONTEND_PROTOCOL_VERSION, StructuralSource,
30};
31
32use crate::{
33    coverage_analysis::McdcVector,
34    coverage_report::{
35        CoverageManifest, CoverageModelDeclaration, CoveragePhase, CoverageReportRequest,
36        DecisionMeta, DecisionSnapshot, ExecutionScope, ExitCodeInput, PersistedCoverageModel,
37        RawTestResult, RuntimeEvent, RuntimeSnapshot, TestProvenance,
38    },
39    evidence_archive::EvidenceArchiveEntry,
40    rust_project::PreparedRustProject,
41    rust_runtime::{RustProbeObservation, read_rust_probe_directory},
42    rust_test_context::preflight_rust_test_contexts,
43};
44
45#[derive(Debug, Clone, PartialEq)]
46pub struct RustFrontendRun {
47    pub declaration: FrontendRunDeclaration,
48    pub request: CoverageReportRequest,
49    pub exit_code: i32,
50    pub artifacts: usize,
51    pub artifact_files: Vec<PathBuf>,
52    pub build_ms: f64,
53    pub execution_ms: f64,
54}
55
56impl RustFrontendRun {
57    pub fn archive_entries(&self) -> Result<Vec<EvidenceArchiveEntry>, serde_json::Error> {
58        let model = PersistedCoverageModel::from_declaration(
59            self.request
60                .coverage_model
61                .as_ref()
62                .expect("Rust frontend always declares a coverage model"),
63        )
64        .expect("Rust coverage model is contract-valid");
65        let mut entries = vec![
66            EvidenceArchiveEntry {
67                path: "coverage-model.json".into(),
68                contents: serde_json::to_vec(&model)?,
69            },
70            EvidenceArchiveEntry {
71                path: "frontend.json".into(),
72                contents: serde_json::to_vec(&self.declaration)?,
73            },
74            EvidenceArchiveEntry {
75                path: "manifest.json".into(),
76                contents: serde_json::to_vec(&self.request.manifest)?,
77            },
78        ];
79        for (index, result) in self.request.raw_results.iter().enumerate() {
80            entries.push(EvidenceArchiveEntry {
81                path: format!("results/{index:08}/mcdc.json"),
82                contents: serde_json::to_vec(result)?,
83            });
84        }
85        Ok(entries)
86    }
87}
88
89#[derive(Debug)]
90pub enum RustTestRunnerError {
91    UnsupportedCommand(String),
92    Launch(String),
93    CargoFailed(String),
94    CargoJson(String),
95    UnsafeArtifact(String),
96    ListFailed(String),
97    Probe(String),
98    Context(String),
99    UnknownProbe(String),
100    InvalidVector {
101        id: String,
102        expected: usize,
103        actual: usize,
104    },
105    Json(serde_json::Error),
106    Io(String),
107}
108
109impl std::fmt::Display for RustTestRunnerError {
110    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        match self {
112            Self::UnsupportedCommand(reason) => formatter.write_str(reason),
113            Self::Launch(reason) => {
114                write!(formatter, "could not launch Rust test process: {reason}")
115            }
116            Self::CargoFailed(reason) => write!(formatter, "Cargo test build failed: {reason}"),
117            Self::CargoJson(reason) => write!(formatter, "invalid Cargo JSON output: {reason}"),
118            Self::UnsafeArtifact(path) => {
119                write!(formatter, "Cargo emitted an unsafe test artifact: {path}")
120            }
121            Self::ListFailed(reason) => {
122                write!(formatter, "could not enumerate Rust tests: {reason}")
123            }
124            Self::Probe(reason) => write!(formatter, "invalid Rust probe evidence: {reason}"),
125            Self::Context(reason) => write!(formatter, "invalid Rust test context: {reason}"),
126            Self::UnknownProbe(id) => write!(
127                formatter,
128                "Rust runtime emitted an unknown obligation: {id}"
129            ),
130            Self::InvalidVector {
131                id,
132                expected,
133                actual,
134            } => write!(
135                formatter,
136                "Rust decision {id} emitted vector width {actual}; expected {expected}"
137            ),
138            Self::Json(error) => write!(formatter, "could not encode Rust evidence: {error}"),
139            Self::Io(reason) => formatter.write_str(reason),
140        }
141    }
142}
143
144impl std::error::Error for RustTestRunnerError {}
145
146impl From<serde_json::Error> for RustTestRunnerError {
147    fn from(value: serde_json::Error) -> Self {
148        Self::Json(value)
149    }
150}
151
152#[derive(Debug, Deserialize)]
153struct CargoMessage {
154    reason: String,
155    #[serde(default)]
156    target: Option<CargoArtifactTarget>,
157    #[serde(default)]
158    profile: Option<CargoArtifactProfile>,
159    executable: Option<PathBuf>,
160    manifest_path: Option<PathBuf>,
161}
162
163#[derive(Debug, Deserialize)]
164struct CargoArtifactTarget {
165    name: String,
166    kind: Vec<String>,
167    src_path: PathBuf,
168}
169
170#[derive(Debug, Deserialize)]
171struct CargoArtifactProfile {
172    test: bool,
173}
174
175#[derive(Debug, Clone)]
176struct TestArtifact {
177    executable: PathBuf,
178    name: String,
179    kind: String,
180    source: String,
181    /// Where Cargo runs the binary: the directory of its package's manifest.
182    /// tokio's `basic_fs` reads `Cargo.toml` from there.
183    package_directory: PathBuf,
184}
185
186#[derive(Debug)]
187struct ProcessTask {
188    ordinal: usize,
189    artifact_index: usize,
190    test_index: usize,
191    artifact: TestArtifact,
192    test: String,
193    context_id: u64,
194    directory: PathBuf,
195}
196
197#[derive(Debug)]
198struct ProcessOutcome {
199    task: ProcessTask,
200    output: Output,
201}
202
203fn shell_words(value: &str) -> Result<Vec<String>, RustTestRunnerError> {
204    let mut words = Vec::new();
205    let mut current = String::new();
206    let mut quote = None;
207    let mut escaped = false;
208    for character in value.chars() {
209        if escaped {
210            current.push(character);
211            escaped = false;
212        } else if character == '\\' && quote != Some('\'') {
213            escaped = true;
214        } else if matches!(character, '\'' | '"') {
215            if quote == Some(character) {
216                quote = None;
217            } else if quote.is_none() {
218                quote = Some(character);
219            } else {
220                current.push(character);
221            }
222        } else if character.is_whitespace() && quote.is_none() {
223            if !current.is_empty() {
224                words.push(std::mem::take(&mut current));
225            }
226        } else {
227            current.push(character);
228        }
229    }
230    if escaped || quote.is_some() {
231        return Err(RustTestRunnerError::UnsupportedCommand(
232            "the expanded Cargo command contains an incomplete quote or escape".into(),
233        ));
234    }
235    if !current.is_empty() {
236        words.push(current);
237    }
238    Ok(words)
239}
240
241fn executable_name(value: &str) -> &str {
242    Path::new(value)
243        .file_name()
244        .and_then(|name| name.to_str())
245        .unwrap_or(value)
246        .trim_end_matches(".exe")
247        .trim_end_matches(".cmd")
248}
249
250#[derive(Debug, Clone, PartialEq, Eq)]
251pub(crate) struct CargoTestInvocation {
252    pub program: String,
253    pub kind: RustCargoCommandKind,
254    pub arguments: Vec<String>,
255    pub runner_arguments: Vec<String>,
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub(crate) enum RustCargoCommandKind {
260    CargoTest,
261    NextestRun,
262}
263
264impl CargoTestInvocation {
265    pub(crate) fn command_position(&self) -> Option<usize> {
266        match self.kind {
267            RustCargoCommandKind::CargoTest => self
268                .arguments
269                .iter()
270                .position(|argument| argument == "test"),
271            RustCargoCommandKind::NextestRun => self
272                .arguments
273                .windows(2)
274                .position(|pair| pair == ["nextest", "run"]),
275        }
276    }
277}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
280pub(crate) struct RustLibtestSelection {
281    pub list_arguments: Vec<String>,
282}
283
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub(crate) struct RustCargoExecutionSelection {
286    pub run_libtests: bool,
287    pub run_doctests: bool,
288    pub doctest_arguments: Vec<String>,
289}
290
291#[derive(Debug, Clone, PartialEq, Eq)]
292pub(crate) struct NextestListInvocation {
293    pub arguments: Vec<String>,
294    pub runner_arguments: Vec<String>,
295}
296
297pub(crate) fn nextest_version_arguments(
298    invocation: &CargoTestInvocation,
299) -> Result<Vec<String>, RustTestRunnerError> {
300    if invocation.kind != RustCargoCommandKind::NextestRun {
301        return Err(RustTestRunnerError::UnsupportedCommand(
302            "a nextest version handshake requires `cargo nextest run`".into(),
303        ));
304    }
305    let command = invocation.command_position().ok_or_else(|| {
306        RustTestRunnerError::UnsupportedCommand(
307            "the expanded Cargo invocation lost its nextest run subcommand".into(),
308        )
309    })?;
310    let mut arguments = invocation.arguments[..command].to_vec();
311    arguments.extend(["nextest".into(), "--version".into()]);
312    Ok(arguments)
313}
314
315fn nextest_run_only_option(argument: &str) -> Option<bool> {
316    let name = argument.split_once('=').map_or(argument, |(name, _)| name);
317    match name {
318        "-j"
319        | "--jobs"
320        | "--test-threads"
321        | "--retries"
322        | "--flaky-result"
323        | "--max-fail"
324        | "--no-tests"
325        | "--failure-output"
326        | "--success-output"
327        | "--status-level"
328        | "--final-status-level"
329        | "--show-progress"
330        | "--max-progress-running"
331        | "--message-format"
332        | "--message-format-version" => Some(!argument.contains('=')),
333        "--fail-fast"
334        | "--ff"
335        | "--no-fail-fast"
336        | "--nff"
337        | "--no-capture"
338        | "--nocapture"
339        | "--no-output-indent"
340        | "--hide-progress-bar"
341        | "--no-input-handler" => Some(false),
342        _ if argument.starts_with("-j") && argument.len() > 2 => Some(false),
343        _ => None,
344    }
345}
346
347fn nextest_unsupported_run_option(argument: &str) -> Option<bool> {
348    let name = argument.split_once('=').map_or(argument, |(name, _)| name);
349    match name {
350        "-R"
351        | "--rerun"
352        | "--debugger"
353        | "--tracer"
354        | "--stress-count"
355        | "--stress-duration"
356        | "--archive-file"
357        | "--archive-format"
358        | "--extract-to"
359        | "--cargo-metadata"
360        | "--workspace-remap"
361        | "--binaries-metadata"
362        | "--target-dir-remap"
363        | "--build-dir-remap" => Some(!argument.contains('=')),
364        "--no-run" | "--extract-overwrite" | "--persist-extract-tempdir" => Some(false),
365        _ => None,
366    }
367}
368
369fn nextest_shared_option(argument: &str) -> Option<bool> {
370    let name = argument.split_once('=').map_or(argument, |(name, _)| name);
371    match name {
372        "--color"
373        | "-p"
374        | "--package"
375        | "--exclude"
376        | "--bin"
377        | "--example"
378        | "--test"
379        | "--bench"
380        | "-F"
381        | "--features"
382        | "--build-jobs"
383        | "--cargo-profile"
384        | "--target"
385        | "--target-dir"
386        | "--cargo-message-format"
387        | "--config"
388        | "--timings"
389        | "-Z"
390        | "--run-ignored"
391        | "--partition"
392        | "--platform-filter"
393        | "-E"
394        | "--filterset"
395        | "--filter-expr"
396        | "--manifest-path"
397        | "--config-file"
398        | "--user-config-file"
399        | "--tool-config-file"
400        | "-P"
401        | "--profile" => Some(!argument.contains('=')),
402        "--no-pager"
403        | "-v"
404        | "--verbose"
405        | "--workspace"
406        | "--all"
407        | "--lib"
408        | "--bins"
409        | "--examples"
410        | "--tests"
411        | "--benches"
412        | "--all-targets"
413        | "--all-features"
414        | "--no-default-features"
415        | "-r"
416        | "--release"
417        | "--unit-graph"
418        | "--frozen"
419        | "--locked"
420        | "--offline"
421        | "--cargo-quiet"
422        | "--cargo-verbose"
423        | "--ignore-rust-version"
424        | "--future-incompat-report"
425        | "--ignore-default-filter"
426        | "--override-version-check" => Some(false),
427        _ if argument.starts_with("-p") && argument.len() > 2 => Some(false),
428        _ if argument.starts_with("-F") && argument.len() > 2 => Some(false),
429        _ if argument.starts_with("-E") && argument.len() > 2 => Some(false),
430        _ if argument.starts_with("-P") && argument.len() > 2 => Some(false),
431        _ if argument.starts_with("-Z") && argument.len() > 2 => Some(false),
432        _ if argument.len() > 2
433            && argument.starts_with('-')
434            && argument[1..].bytes().all(|byte| byte == b'v') =>
435        {
436            Some(false)
437        }
438        _ if argument.starts_with("--timings=") => Some(false),
439        _ => None,
440    }
441}
442
443/// Reprojects a pinned `nextest run` invocation into the stable machine-readable
444/// `nextest list` contract. Selection/build/configuration arguments are kept
445/// byte-for-byte. Runner and presentation arguments are removed because they
446/// do not affect the selected-test catalog. Options whose selection semantics
447/// require an external recording or a different execution mode fail closed.
448pub(crate) fn nextest_list_invocation(
449    invocation: &CargoTestInvocation,
450) -> Result<NextestListInvocation, RustTestRunnerError> {
451    if invocation.kind != RustCargoCommandKind::NextestRun {
452        return Err(RustTestRunnerError::UnsupportedCommand(
453            "a nextest list projection requires `cargo nextest run`".into(),
454        ));
455    }
456    let command = invocation.command_position().ok_or_else(|| {
457        RustTestRunnerError::UnsupportedCommand(
458            "the expanded Cargo invocation lost its nextest run subcommand".into(),
459        )
460    })?;
461    let mut arguments = invocation.arguments[..command].to_vec();
462    arguments.extend(["nextest".into(), "list".into()]);
463    let mut index = command + 2;
464    while index < invocation.arguments.len() {
465        let argument = &invocation.arguments[index];
466        if argument == "--" {
467            arguments.extend(invocation.arguments[index..].iter().cloned());
468            break;
469        }
470        if let Some(takes_value) = nextest_unsupported_run_option(argument) {
471            if takes_value && invocation.arguments.get(index + 1).is_none() {
472                return Err(RustTestRunnerError::UnsupportedCommand(format!(
473                    "nextest option {argument} has no value"
474                )));
475            }
476            return Err(RustTestRunnerError::UnsupportedCommand(format!(
477                "nextest option {argument} cannot yet be assigned exact selected-test identity"
478            )));
479        }
480        if let Some(takes_value) = nextest_run_only_option(argument) {
481            if takes_value {
482                index += 1;
483                if index == invocation.arguments.len() {
484                    return Err(RustTestRunnerError::UnsupportedCommand(format!(
485                        "nextest option {argument} has no value"
486                    )));
487                }
488            }
489        } else if let Some(takes_value) = nextest_shared_option(argument) {
490            arguments.push(argument.clone());
491            if takes_value {
492                index += 1;
493                let value = invocation.arguments.get(index).ok_or_else(|| {
494                    RustTestRunnerError::UnsupportedCommand(format!(
495                        "nextest option {argument} has no value"
496                    ))
497                })?;
498                arguments.push(value.clone());
499            }
500        } else if argument.starts_with('-') {
501            return Err(RustTestRunnerError::UnsupportedCommand(format!(
502                "the pinned nextest run contract does not recognize option {argument}"
503            )));
504        } else {
505            arguments.push(argument.clone());
506        }
507        index += 1;
508    }
509    arguments.extend(["--message-format".into(), "json".into()]);
510    Ok(NextestListInvocation {
511        arguments,
512        runner_arguments: invocation.runner_arguments.clone(),
513    })
514}
515
516pub(crate) fn cargo_invocation(
517    root: &Path,
518    command: &[String],
519) -> Result<CargoTestInvocation, RustTestRunnerError> {
520    // A process argv is already tokenized. Joining and shell-parsing a direct
521    // Cargo command destroys quotes that are payload (not shell syntax), most
522    // notably TOML strings passed to Cargo's --config. Only opaque wrapper or
523    // package-script commands need textual expansion and shell tokenization.
524    let words = if command.iter().any(|word| executable_name(word) == "cargo") {
525        command.to_vec()
526    } else {
527        let expanded = crate::project_discovery::expanded_command(root, command);
528        shell_words(&expanded)?
529    };
530    let cargo = words
531        .iter()
532        .position(|word| executable_name(word) == "cargo")
533        .ok_or_else(|| RustTestRunnerError::UnsupportedCommand(
534            "Rust was detected, but the expanded command does not expose a stable Cargo invocation".into(),
535        ))?;
536    let cargo_test = words[cargo + 1..]
537        .iter()
538        .position(|word| word == "test")
539        .map(|position| cargo + 1 + position);
540    let nextest = words[cargo + 1..]
541        .windows(2)
542        .position(|pair| pair == ["nextest", "run"])
543        .map(|position| cargo + 1 + position);
544    let (kind, command) = match (cargo_test, nextest) {
545        (Some(test), None) => (RustCargoCommandKind::CargoTest, test),
546        (None, Some(nextest)) => (RustCargoCommandKind::NextestRun, nextest),
547        (Some(_), Some(_)) => {
548            return Err(RustTestRunnerError::UnsupportedCommand(
549                "the Cargo invocation ambiguously contains both test and nextest run".into(),
550            ));
551        }
552        (None, None) => {
553            return Err(RustTestRunnerError::UnsupportedCommand(
554                "the owned Rust runner currently requires `cargo test` or `cargo nextest run`; cross remains explicitly unsupported"
555                    .into(),
556            ));
557        }
558    };
559    if words[cargo + 1..command]
560        .iter()
561        .any(|word| matches!(word.as_str(), "&&" | "||" | ";" | "|"))
562    {
563        return Err(RustTestRunnerError::UnsupportedCommand(
564            "the Cargo invocation contains a shell boundary before `test`".into(),
565        ));
566    }
567    let command_end = command
568        + if kind == RustCargoCommandKind::NextestRun {
569            1
570        } else {
571            0
572        };
573    let mut arguments = words[cargo + 1..=command_end].to_vec();
574    let mut runner_arguments = Vec::new();
575    let mut after_separator = false;
576    for argument in &words[command_end + 1..] {
577        if argument == "--" && !after_separator {
578            after_separator = true;
579            continue;
580        }
581        if matches!(argument.as_str(), "&&" | "||" | ";" | "|") {
582            return Err(RustTestRunnerError::UnsupportedCommand(
583                "the Cargo test command contains an unsupported shell boundary".into(),
584            ));
585        }
586        if after_separator {
587            runner_arguments.push(argument.clone());
588        } else {
589            arguments.push(argument.clone());
590        }
591    }
592    Ok(CargoTestInvocation {
593        program: words[cargo].clone(),
594        kind,
595        arguments,
596        runner_arguments,
597    })
598}
599
600fn cargo_option_takes_value(argument: &str) -> Option<bool> {
601    let name = argument.split_once('=').map_or(argument, |(name, _)| name);
602    match name {
603        "-p" | "--package" | "--exclude" | "--bin" | "--example" | "--test" | "--bench" | "-F"
604        | "--features" | "-j" | "--jobs" | "--profile" | "--target" | "--target-dir"
605        | "--message-format" | "--color" | "--config" | "-Z" | "--manifest-path" => {
606            Some(!argument.contains('='))
607        }
608        "--no-run"
609        | "--no-fail-fast"
610        | "--future-incompat-report"
611        | "-q"
612        | "--quiet"
613        | "-v"
614        | "--verbose"
615        | "--workspace"
616        | "--all"
617        | "--lib"
618        | "--bins"
619        | "--examples"
620        | "--tests"
621        | "--benches"
622        | "--all-targets"
623        | "--doc"
624        | "--all-features"
625        | "--no-default-features"
626        | "-r"
627        | "--release"
628        | "--timings"
629        | "--ignore-rust-version"
630        | "--locked"
631        | "--offline"
632        | "--frozen" => Some(false),
633        _ if argument.starts_with("-vv") => Some(false),
634        _ if argument.starts_with("-p") && argument.len() > 2 => Some(false),
635        _ if argument.starts_with("-F") && argument.len() > 2 => Some(false),
636        _ if argument.starts_with("-j") && argument.len() > 2 => Some(false),
637        _ => None,
638    }
639}
640
641pub(crate) fn rust_libtest_selection(
642    invocation: &CargoTestInvocation,
643) -> Result<RustLibtestSelection, RustTestRunnerError> {
644    if invocation.kind != RustCargoCommandKind::CargoTest {
645        return Err(RustTestRunnerError::UnsupportedCommand(
646            "libtest selection cannot be reconstructed from a nextest command".into(),
647        ));
648    }
649    let test = invocation
650        .arguments
651        .iter()
652        .position(|argument| argument == "test")
653        .ok_or_else(|| {
654            RustTestRunnerError::UnsupportedCommand(
655                "the expanded Cargo invocation lost its test subcommand".into(),
656            )
657        })?;
658    let mut cargo_filter = None;
659    let mut index = test + 1;
660    while index < invocation.arguments.len() {
661        let argument = &invocation.arguments[index];
662        if argument.starts_with('-') {
663            let takes_value = cargo_option_takes_value(argument).ok_or_else(|| {
664                RustTestRunnerError::UnsupportedCommand(format!(
665                    "the pinned Cargo test contract does not recognize option {argument}"
666                ))
667            })?;
668            if takes_value {
669                index += 1;
670                if index == invocation.arguments.len() {
671                    return Err(RustTestRunnerError::UnsupportedCommand(format!(
672                        "Cargo option {argument} has no value"
673                    )));
674                }
675            }
676        } else if cargo_filter.replace(argument.clone()).is_some() {
677            return Err(RustTestRunnerError::UnsupportedCommand(
678                "Cargo test has more than one pre-separator TESTNAME".into(),
679            ));
680        }
681        index += 1;
682    }
683
684    let mut list_arguments = cargo_filter.into_iter().collect::<Vec<_>>();
685    let mut test_threads = None;
686    let mut index = 0;
687    while index < invocation.runner_arguments.len() {
688        let argument = &invocation.runner_arguments[index];
689        match argument.as_str() {
690            "--ignored" | "--include-ignored" | "--exclude-should-panic" | "--test" | "--bench" => {
691                list_arguments.push(argument.clone());
692            }
693            "--exact" => list_arguments.push(argument.clone()),
694            "--skip" => {
695                let value = invocation.runner_arguments.get(index + 1).ok_or_else(|| {
696                    RustTestRunnerError::UnsupportedCommand(
697                        "libtest --skip has no filter value".into(),
698                    )
699                })?;
700                list_arguments.extend([argument.clone(), value.clone()]);
701                index += 1;
702            }
703            _ if argument.starts_with("--skip=") && argument.len() > "--skip=".len() => {
704                list_arguments.push(argument.clone());
705            }
706            "--test-threads" => {
707                let value = invocation.runner_arguments.get(index + 1).ok_or_else(|| {
708                    RustTestRunnerError::UnsupportedCommand(
709                        "libtest --test-threads has no value".into(),
710                    )
711                })?;
712                let parsed = parse_libtest_threads(value)?;
713                if test_threads.replace(parsed).is_some() {
714                    return Err(RustTestRunnerError::UnsupportedCommand(
715                        "libtest --test-threads was provided more than once".into(),
716                    ));
717                }
718                index += 1;
719            }
720            _ if argument.starts_with("--test-threads=") => {
721                let value = &argument["--test-threads=".len()..];
722                let parsed = parse_libtest_threads(value)?;
723                if test_threads.replace(parsed).is_some() {
724                    return Err(RustTestRunnerError::UnsupportedCommand(
725                        "libtest --test-threads was provided more than once".into(),
726                    ));
727                }
728            }
729            "-Z" => {
730                let value = invocation.runner_arguments.get(index + 1).ok_or_else(|| {
731                    RustTestRunnerError::UnsupportedCommand(
732                        "libtest -Z has no feature value".into(),
733                    )
734                })?;
735                // `exclude-should-panic` and the other unstable selection
736                // flags must be parsed under the same libtest feature gate
737                // during discovery. The user's exact pair is also preserved
738                // unchanged for the real artifact execution.
739                list_arguments.extend([argument.clone(), value.clone()]);
740                index += 1;
741            }
742            _ if argument.starts_with("-Z") && argument.len() > 2 => {
743                list_arguments.push(argument.clone());
744            }
745            "--logfile" | "--color" | "--format" | "--shuffle-seed" => {
746                if invocation.runner_arguments.get(index + 1).is_none() {
747                    return Err(RustTestRunnerError::UnsupportedCommand(format!(
748                        "libtest {argument} has no value"
749                    )));
750                }
751                // Presentation-only values must not leak into the synthetic
752                // terse listing. The original argument and value are passed
753                // byte-for-byte to the one stock artifact execution.
754                index += 1;
755            }
756            _ if ["--logfile=", "--color=", "--format=", "--shuffle-seed="]
757                .iter()
758                .any(|prefix| argument.starts_with(prefix) && argument.len() > prefix.len()) => {}
759            "--force-run-in-process"
760            | "--fail-fast"
761            | "--no-capture"
762            | "--nocapture"
763            | "-q"
764            | "--quiet"
765            | "--show-output"
766            | "--report-time"
767            | "--ensure-time"
768            | "--shuffle" => {
769                // These affect only scheduling, execution or presentation.
770                // They are intentionally absent from discovery and retained
771                // unchanged in the real artifact argv.
772            }
773            "--list" | "-h" | "--help" => {
774                return Err(RustTestRunnerError::UnsupportedCommand(format!(
775                    "libtest {argument} does not execute a test suite; exact non-execution mode support is not implemented"
776                )));
777            }
778            _ if !argument.starts_with('-') => list_arguments.push(argument.clone()),
779            _ => {
780                return Err(RustTestRunnerError::UnsupportedCommand(format!(
781                    "the pinned Rust 1.95 libtest discovery contract does not recognize option {argument}"
782                )));
783            }
784        }
785        index += 1;
786    }
787    Ok(RustLibtestSelection { list_arguments })
788}
789
790fn parse_libtest_threads(value: &str) -> Result<usize, RustTestRunnerError> {
791    match value.parse::<usize>() {
792        Ok(0) => Err(RustTestRunnerError::UnsupportedCommand(
793            "argument for --test-threads must not be 0".into(),
794        )),
795        Ok(value) => Ok(value),
796        Err(error) => Err(RustTestRunnerError::UnsupportedCommand(format!(
797            "argument for --test-threads must be a number > 0 (error: {error})"
798        ))),
799    }
800}
801
802pub(crate) fn rust_cargo_execution_selection(
803    invocation: &CargoTestInvocation,
804) -> Result<RustCargoExecutionSelection, RustTestRunnerError> {
805    if invocation.kind == RustCargoCommandKind::NextestRun {
806        return Ok(RustCargoExecutionSelection {
807            run_libtests: true,
808            run_doctests: false,
809            doctest_arguments: Vec::new(),
810        });
811    }
812    let test = invocation
813        .arguments
814        .iter()
815        .position(|argument| argument == "test")
816        .ok_or_else(|| {
817            RustTestRunnerError::UnsupportedCommand(
818                "the expanded Cargo invocation lost its test subcommand".into(),
819            )
820        })?;
821    let mut doc = false;
822    let mut other_target = false;
823    let mut index = test + 1;
824    while index < invocation.arguments.len() {
825        let argument = &invocation.arguments[index];
826        let name = argument
827            .split_once('=')
828            .map_or(argument.as_str(), |(name, _)| name);
829        match name {
830            "--doc" => doc = true,
831            "--lib" | "--bins" | "--bin" | "--examples" | "--example" | "--tests" | "--test"
832            | "--benches" | "--bench" | "--all-targets" => other_target = true,
833            _ => {}
834        }
835        if argument.starts_with('-') {
836            let takes_value = cargo_option_takes_value(argument).ok_or_else(|| {
837                RustTestRunnerError::UnsupportedCommand(format!(
838                    "the pinned Cargo test contract does not recognize option {argument}"
839                ))
840            })?;
841            if takes_value {
842                index += 1;
843                if index == invocation.arguments.len() {
844                    return Err(RustTestRunnerError::UnsupportedCommand(format!(
845                        "Cargo option {argument} has no value"
846                    )));
847                }
848            }
849        }
850        index += 1;
851    }
852    if doc && other_target {
853        return Err(RustTestRunnerError::UnsupportedCommand(
854            "Cargo --doc cannot be combined with another explicit target selection".into(),
855        ));
856    }
857    let run_doctests = doc || !other_target;
858    let run_libtests = !doc;
859    let mut doctest_arguments = invocation.arguments.clone();
860    if run_doctests && !doc {
861        doctest_arguments.insert(test + 1, "--doc".into());
862    }
863    if !invocation.runner_arguments.is_empty() {
864        doctest_arguments.push("--".into());
865        doctest_arguments.extend(invocation.runner_arguments.iter().cloned());
866    }
867    Ok(RustCargoExecutionSelection {
868        run_libtests,
869        run_doctests,
870        doctest_arguments,
871    })
872}
873
874pub(crate) fn relative_source(root: &Path, path: &Path) -> Result<String, RustTestRunnerError> {
875    let relative = path
876        .strip_prefix(root)
877        .map_err(|_| RustTestRunnerError::UnsafeArtifact(path.display().to_string()))?;
878    if relative.as_os_str().is_empty()
879        || relative
880            .components()
881            .any(|part| !matches!(part, Component::Normal(_)))
882    {
883        return Err(RustTestRunnerError::UnsafeArtifact(
884            path.display().to_string(),
885        ));
886    }
887    Ok(relative.to_string_lossy().replace('\\', "/"))
888}
889
890fn build_test_artifacts(
891    project: &PreparedRustProject,
892    command: &[String],
893) -> Result<Vec<TestArtifact>, RustTestRunnerError> {
894    let mut invocation = cargo_invocation(&project.workspace_root, command)?;
895    invocation
896        .arguments
897        .extend(["--no-run".into(), "--message-format=json".into()]);
898    // The instrumented workspace is ephemeral and its sources are generated,
899    // so the HOST crate's lint policy must not reject them: serde builds with
900    // `#![deny(warnings)]`, and http's `if ({ frame ... })` decision wrapping
901    // trips `unused_parens` into a hard error under it. Capping lints to warn
902    // changes nothing about the user's own `cargo test` runs. The user's
903    // RUSTFLAGS are preserved ahead of the cap.
904    let output = Command::new(&invocation.program)
905        .args(invocation.arguments)
906        .current_dir(&project.workspace_root)
907        .env("CARGO_TARGET_DIR", &project.target_directory)
908        .env("RUSTFLAGS", capped_rustflags())
909        .output()
910        .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
911    if !output.status.success() {
912        // With --message-format=json the compiler's diagnostics travel on
913        // stdout as JSON and Cargo's own summary on stderr; show both, or a
914        // failed build says only which crate failed.
915        let rendered = output
916            .stdout
917            .split(|byte| *byte == b'\n')
918            .filter_map(|line| serde_json::from_slice::<serde_json::Value>(line).ok())
919            .filter(|message| {
920                message["reason"] == "compiler-message" && message["message"]["level"] == "error"
921            })
922            .filter_map(|message| message["message"]["rendered"].as_str().map(str::to_owned))
923            .collect::<String>();
924        return Err(RustTestRunnerError::CargoFailed(format!(
925            "{rendered}{}",
926            String::from_utf8_lossy(&output.stderr).trim()
927        )));
928    }
929    let canonical_target = fs::canonicalize(&project.target_directory)
930        .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
931    let mut artifacts = Vec::new();
932    for line in output
933        .stdout
934        .split(|byte| *byte == b'\n')
935        .filter(|line| !line.is_empty())
936    {
937        let message: CargoMessage = serde_json::from_slice(line)
938            .map_err(|error| RustTestRunnerError::CargoJson(error.to_string()))?;
939        if message.reason != "compiler-artifact"
940            || !message.profile.as_ref().is_some_and(|profile| profile.test)
941        {
942            continue;
943        }
944        let (Some(executable), Some(target)) = (message.executable, message.target) else {
945            continue;
946        };
947        let executable = fs::canonicalize(&executable)
948            .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
949        if !executable.starts_with(&canonical_target)
950            || !fs::metadata(&executable).is_ok_and(|metadata| metadata.is_file())
951        {
952            return Err(RustTestRunnerError::UnsafeArtifact(
953                executable.display().to_string(),
954            ));
955        }
956        let source = fs::canonicalize(target.src_path)
957            .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
958        let package_directory = message
959            .manifest_path
960            .as_deref()
961            .and_then(Path::parent)
962            .map_or_else(|| project.workspace_root.clone(), Path::to_path_buf);
963        artifacts.push(TestArtifact {
964            executable,
965            package_directory,
966            name: target.name,
967            kind: if target.kind.iter().any(|kind| kind == "test") {
968                "integration".into()
969            } else {
970                "unit".into()
971            },
972            source: relative_source(&project.workspace_root, &source)?,
973        });
974    }
975    artifacts.sort_by(|left, right| left.executable.cmp(&right.executable));
976    artifacts.dedup_by(|left, right| left.executable == right.executable);
977    if artifacts.is_empty() {
978        return Err(RustTestRunnerError::CargoJson(
979            "Cargo emitted no libtest artifacts".into(),
980        ));
981    }
982    Ok(artifacts)
983}
984
985/// The tests an artifact would run for this invocation. `filters` are the
986/// user's libtest arguments that select tests -- a name, `--skip`, `--exact`,
987/// `--ignored` -- and libtest applies them to `--list` exactly as it applies
988/// them to a run, so the enumeration is the set plain Cargo would execute.
989fn list_tests(
990    executable: &Path,
991    filters: &[String],
992    environment: &[(&'static str, OsString)],
993) -> Result<Vec<String>, RustTestRunnerError> {
994    let output = Command::new(executable)
995        .args(["--list", "--format", "terse"])
996        .args(filters)
997        .envs(environment.iter().map(|(key, value)| (key, value)))
998        .output()
999        .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
1000    if !output.status.success() {
1001        // A libtest binary that dies on a signal writes nothing to stderr, so
1002        // reporting stderr alone produces an empty, undiagnosable message.
1003        // Name the artifact and how it ended, and fall back to stdout.
1004        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
1005        let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned();
1006        let detail = if !stderr.is_empty() {
1007            stderr
1008        } else if !stdout.is_empty() {
1009            format!("no stderr; stdout was {stdout}")
1010        } else {
1011            "no output on either stream".to_owned()
1012        };
1013        return Err(RustTestRunnerError::ListFailed(format!(
1014            "{} exited with {} when asked to --list: {detail}",
1015            executable.display(),
1016            output.status
1017        )));
1018    }
1019    let mut tests = String::from_utf8_lossy(&output.stdout)
1020        .lines()
1021        .filter_map(|line| line.strip_suffix(": test"))
1022        .map(str::to_owned)
1023        .collect::<Vec<_>>();
1024    tests.sort();
1025    tests.dedup();
1026    Ok(tests)
1027}
1028
1029/// What one test process recorded: the coverage snapshot and the assertion
1030/// phases the evidence witnesses.
1031pub(crate) struct RustEvidence {
1032    pub(crate) snapshot: RuntimeSnapshot,
1033    pub(crate) phases: Vec<CoveragePhase>,
1034}
1035
1036/// One record a thread wrote, waiting to learn whether an assertion follows.
1037struct PendingRecord {
1038    event_type: &'static str,
1039    id: String,
1040    vector: Option<McdcVector>,
1041    sequence: i64,
1042}
1043
1044/// `attempt` names what this evidence belongs to, so its phase IDs are
1045/// unique across the run: every test runs in a process of its own and numbers
1046/// its records from one.
1047pub(crate) fn snapshot(
1048    manifest: &CoverageManifest,
1049    directory: &Path,
1050    attempt: &str,
1051) -> Result<RustEvidence, RustTestRunnerError> {
1052    let points = manifest
1053        .points
1054        .iter()
1055        .map(|point| (point.id.as_str(), point))
1056        .collect::<BTreeMap<_, _>>();
1057    let alternatives = manifest
1058        .branches
1059        .iter()
1060        .flat_map(|branch| {
1061            branch
1062                .alternatives
1063                .iter()
1064                .map(|alternative| alternative.id.as_str())
1065        })
1066        .collect::<BTreeSet<_>>();
1067    let decisions = manifest
1068        .decisions
1069        .iter()
1070        .map(|decision| (decision.id.as_str(), decision))
1071        .collect::<BTreeMap<_, _>>();
1072    let mut hits = BTreeSet::new();
1073    let mut vectors = BTreeMap::<String, BTreeSet<(Vec<Option<bool>>, bool)>>::new();
1074    // Records a thread wrote since its last assertion. An assertion witnesses
1075    // its own thread's records and no other's: a test that spawns threads
1076    // interleaves them in one file.
1077    let mut pending = BTreeMap::<u64, Vec<PendingRecord>>::new();
1078    let mut events = Vec::new();
1079    let mut phases = Vec::new();
1080    // Records carry no clock. The order they were written in is what matters,
1081    // and a sequence number preserves exactly that.
1082    let mut sequence = 0_i64;
1083    // Evidence files are named by the instrumentation that wrote them. A
1084    // test may build and run a program instrumented on its own -- a fixture
1085    // prepared inside the instrumented workspace -- and that program
1086    // inherits the evidence directory; its obligations are not this run's.
1087    let token = crate::rust_project::manifest_token(manifest);
1088    for (name, observations) in read_rust_probe_directory(directory)
1089        .map_err(|error| RustTestRunnerError::Probe(error.to_string()))?
1090    {
1091        if !name.starts_with(&token) {
1092            continue;
1093        }
1094        for entry in observations {
1095            sequence += 1;
1096            let thread = entry.thread;
1097            match entry.observation {
1098                RustProbeObservation::Hit { id } => {
1099                    if !points.contains_key(id.as_str()) && !alternatives.contains(id.as_str()) {
1100                        return Err(RustTestRunnerError::UnknownProbe(id));
1101                    }
1102                    hits.insert(id.clone());
1103                    pending.entry(thread).or_default().push(PendingRecord {
1104                        event_type: "hit",
1105                        id,
1106                        vector: None,
1107                        sequence,
1108                    });
1109                }
1110                RustProbeObservation::Decision {
1111                    id,
1112                    values,
1113                    outcome,
1114                } => {
1115                    let Some(meta) = decisions.get(id.as_str()) else {
1116                        return Err(RustTestRunnerError::UnknownProbe(id));
1117                    };
1118                    if values.len() != meta.conditions.len() {
1119                        return Err(RustTestRunnerError::InvalidVector {
1120                            id,
1121                            expected: meta.conditions.len(),
1122                            actual: values.len(),
1123                        });
1124                    }
1125                    hits.insert(format!(
1126                        "{}:outcome:{}",
1127                        meta.id,
1128                        if outcome { "true" } else { "false" }
1129                    ));
1130                    vectors
1131                        .entry(meta.id.clone())
1132                        .or_default()
1133                        .insert((values.clone(), outcome));
1134                    pending.entry(thread).or_default().push(PendingRecord {
1135                        event_type: "decision",
1136                        id,
1137                        vector: Some(McdcVector { values, outcome }),
1138                        sequence,
1139                    });
1140                }
1141                RustProbeObservation::Assertion { id } => {
1142                    // The marker names the statement that asserts, which is
1143                    // a point of this manifest.
1144                    let Some(point) = points.get(id.as_str()) else {
1145                        return Err(RustTestRunnerError::UnknownProbe(id));
1146                    };
1147                    let witnessed = pending.remove(&thread).unwrap_or_default();
1148                    let phase_id = format!("{attempt}:assertion:{sequence}");
1149                    phases.push(CoveragePhase {
1150                        id: phase_id.clone(),
1151                        kind: "assertion".into(),
1152                        operation: format!(
1153                            "Rust assertion at {}:{}:{}",
1154                            point.file, point.line, point.column
1155                        ),
1156                        source: Some(point.source.clone()),
1157                        caused_by_phase_id: None,
1158                        started_at_ms: witnessed.first().map_or(sequence, |record| record.sequence),
1159                        ended_at_ms: Some(sequence),
1160                        // Reaching the marker is the proof: a failing
1161                        // assertion panics before it.
1162                        status: Some("passed".into()),
1163                        error: None,
1164                    });
1165                    for record in witnessed {
1166                        events.push(RuntimeEvent {
1167                            event_type: record.event_type.into(),
1168                            id: record.id,
1169                            vector: record.vector,
1170                            timestamp_ms: record.sequence,
1171                            phase_id: Some(phase_id.clone()),
1172                            statement_id: None,
1173                            environment: "server".into(),
1174                        });
1175                    }
1176                }
1177            }
1178        }
1179    }
1180    let mut decision_snapshots = Vec::new();
1181    for (id, observed) in vectors {
1182        let meta: DecisionMeta = (*decisions[id.as_str()]).clone();
1183        decision_snapshots.push(DecisionSnapshot {
1184            meta,
1185            vectors: observed
1186                .into_iter()
1187                .map(|(values, outcome)| McdcVector { values, outcome })
1188                .collect(),
1189        });
1190    }
1191    Ok(RustEvidence {
1192        snapshot: RuntimeSnapshot {
1193            decisions: decision_snapshots,
1194            hits: hits.into_iter().collect(),
1195            events,
1196            logicals: Vec::new(),
1197            phase_id: None,
1198        },
1199        phases,
1200    })
1201}
1202
1203fn rust_coverage_model() -> CoverageModelDeclaration {
1204    CoverageModelDeclaration {
1205        language: "rust".into(),
1206        variant: "rust-owned-probes-v1".into(),
1207        name: "supercov-rust-owned-v1".into(),
1208        completeness_meaning: "Every semantics-proven Rust obligation in the owned source denominator was observed; explicit manifest limitations identify unmeasured Rust surfaces.".into(),
1209        measured: vec![
1210            "owned Rust statements and function entries".into(),
1211            "owned atomic condition vectors and decision outcomes".into(),
1212            "exact process-per-libtest attribution".into(),
1213            "exact process-per-doctest attribution".into(),
1214            "evidence a passing assertion of the same thread witnessed".into(),
1215        ],
1216        not_measured: vec![
1217            "macro-expanded and generated Rust code".into(),
1218            "const-evaluated code and unsupported structural branch probes".into(),
1219            "causal linkage to individual actions".into(),
1220            "all input values, semantic partitions, paths, or concurrency interleavings".into(),
1221            "mutation score or assertion fault-detection strength".into(),
1222        ],
1223    }
1224}
1225
1226/// The user's RUSTFLAGS, then a cap so the HOST crate's lint policy cannot
1227/// reject generated sources: serde builds with `#![deny(warnings)]`, and
1228/// http's `if ({ frame ... })` decision wrapping trips `unused_parens` into a
1229/// hard error under it. The user's own `cargo test` runs are unaffected.
1230/// The stack an instrumented test thread gets. Probes add stack to every
1231/// frame -- a decision frame, a `?` operand copied through its probe, a
1232/// wrapper's temporaries -- and a test that recurses to a depth chosen against
1233/// the default 2 MiB (serde_json's recursion-limit test) overflows under
1234/// instrumentation. libtest sizes test threads from `RUST_MIN_STACK`, so an
1235/// instrumented process gets 16 MiB unless the user chose a size. The
1236/// reservation is virtual; only touched pages cost memory.
1237pub(crate) fn instrumented_stack_environment() -> Vec<(&'static str, &'static str)> {
1238    if std::env::var_os("RUST_MIN_STACK").is_some() {
1239        Vec::new()
1240    } else {
1241        vec![("RUST_MIN_STACK", "16777216")]
1242    }
1243}
1244
1245/// One path the selected toolchain reports through `rustc --print`.
1246fn rustc_print_path(request: &str) -> Result<PathBuf, RustTestRunnerError> {
1247    let output = Command::new("rustc")
1248        .args(["--print", request])
1249        .output()
1250        .map_err(|error| {
1251            RustTestRunnerError::Launch(format!("rustc --print {request}: {error}"))
1252        })?;
1253    if !output.status.success() {
1254        return Err(RustTestRunnerError::Launch(format!(
1255            "rustc --print {request} exited with {}",
1256            output.status
1257        )));
1258    }
1259    Ok(PathBuf::from(
1260        String::from_utf8_lossy(&output.stdout).trim(),
1261    ))
1262}
1263
1264/// The selected toolchain's sysroot, as `rustc --print sysroot` reports it.
1265pub(crate) fn rustc_sysroot() -> Result<PathBuf, RustTestRunnerError> {
1266    rustc_print_path("sysroot")
1267}
1268
1269/// The directory holding the toolchain's own dynamic libraries
1270/// (`<sysroot>/lib/rustlib/<host>/lib`), where libstd's dylib lives; the
1271/// plain `<sysroot>/lib` no longer holds it.
1272pub(crate) fn rustc_target_libdir() -> Result<PathBuf, RustTestRunnerError> {
1273    rustc_print_path("target-libdir")
1274}
1275
1276/// The dynamic library search path Cargo gives a test binary it runs: the
1277/// artifact's own directories and the toolchain's target library directory.
1278/// A proc-macro crate's test harness links libstd dynamically -- async-trait's
1279/// and serde_derive's cannot even list their tests without this -- and any
1280/// dylib dependency is found the same way. The user's own path follows.
1281pub(crate) fn dynamic_library_environment(
1282    target_libdir: &Path,
1283    executable: &Path,
1284) -> Vec<(&'static str, OsString)> {
1285    let variable = if cfg!(target_os = "macos") {
1286        "DYLD_FALLBACK_LIBRARY_PATH"
1287    } else if cfg!(windows) {
1288        "PATH"
1289    } else {
1290        "LD_LIBRARY_PATH"
1291    };
1292    let mut entries = Vec::new();
1293    if let Some(deps) = executable.parent() {
1294        entries.push(deps.to_path_buf());
1295        if let Some(profile) = deps.parent() {
1296            entries.push(profile.to_path_buf());
1297        }
1298    }
1299    entries.push(target_libdir.to_path_buf());
1300    if let Some(existing) = std::env::var_os(variable) {
1301        entries.extend(std::env::split_paths(&existing));
1302    }
1303    match std::env::join_paths(entries) {
1304        Ok(value) => vec![(variable, value)],
1305        Err(_) => Vec::new(),
1306    }
1307}
1308
1309pub(crate) fn capped_rustflags() -> String {
1310    let mut rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
1311    if !rustflags.is_empty() {
1312        rustflags.push(' ');
1313    }
1314    rustflags.push_str("--cap-lints=warn");
1315    rustflags
1316}
1317
1318pub(crate) fn io_error(error: impl std::fmt::Display) -> RustTestRunnerError {
1319    RustTestRunnerError::Io(error.to_string())
1320}
1321
1322/// A libtest case that exited cleanly but ran nothing, or ran its one test as
1323/// ignored, was skipped rather than passed.
1324pub(crate) fn libtest_skipped(exit: i32, stdout: &str) -> bool {
1325    exit == 0 && (stdout.contains("running 0 tests") || stdout.contains("; 1 ignored;"))
1326}
1327
1328/// Limitation IDs are unique across a declaration, so each runner names its
1329/// own: the libtest runner keeps the original IDs, rustdoc's carry its name.
1330fn rust_runner_limitations(runner: &str) -> Vec<FrontendLimitation> {
1331    let prefix = if runner == "rust-libtest" {
1332        "rust".to_owned()
1333    } else {
1334        runner.to_owned()
1335    };
1336    vec![
1337        FrontendLimitation {
1338            id: format!("{prefix}-action-linkage-unavailable"),
1339            scopes: vec![FrontendLimitationScope::Action],
1340            reason: "Rust test frameworks expose no general action lifecycle".into(),
1341        },
1342        FrontendLimitation {
1343            id: format!("{prefix}-assertion-linkage-unavailable"),
1344            scopes: vec![FrontendLimitationScope::Assertion],
1345            reason: "assertion macros do not expose a stable per-assertion success lifecycle"
1346                .into(),
1347        },
1348    ]
1349}
1350
1351fn rust_runner_declaration(runner: &str) -> FrontendRunnerDeclaration {
1352    FrontendRunnerDeclaration {
1353        runner: runner.into(),
1354        execution_model: ExecutionModel::ProcessPerTest,
1355        attribution: FrontendAttribution {
1356            run: AttributionPrecision::Exact,
1357            worker: AttributionPrecision::Exact,
1358            test: AttributionPrecision::Exact,
1359            retry: AttributionPrecision::Exact,
1360            phase: AttributionPrecision::Exact,
1361            action: AttributionPrecision::Unavailable,
1362            assertion: AttributionPrecision::Unavailable,
1363        },
1364        limitations: rust_runner_limitations(runner),
1365    }
1366}
1367
1368/// Obligations in files the build never read, declined rather than counted.
1369/// Returns the manifest to report against and the files it declined.
1370///
1371/// The obligations stay in the manifest: the evidence files are named by a
1372/// token derived from its obligation IDs, so removing them would orphan every
1373/// record the run wrote. `unmeasured` is what the report reads to take an
1374/// obligation out of the covered/uncovered denominator without pretending it
1375/// was never there.
1376fn decline_uncompiled_sources(
1377    project: &PreparedRustProject,
1378) -> (CoverageManifest, BTreeSet<String>) {
1379    let mut manifest = project.manifest.clone();
1380    let Some(compiled) = crate::rust_project::compiled_source_files(
1381        &project.workspace_root,
1382        &project.target_directory,
1383    ) else {
1384        return (manifest, BTreeSet::new());
1385    };
1386    let declined = project
1387        .source_files
1388        .iter()
1389        .filter(|file| !compiled.contains(*file))
1390        .cloned()
1391        .collect::<BTreeSet<_>>();
1392    // Depinfo that accounts for no crate root at all is depinfo this run did
1393    // not produce; declining on it would empty the denominator.
1394    if declined.is_empty()
1395        || project
1396            .crate_roots
1397            .iter()
1398            .all(|root| !compiled.contains(root))
1399    {
1400        return (manifest, BTreeSet::new());
1401    }
1402    let mut unmeasured = manifest.unmeasured.iter().cloned().collect::<BTreeSet<_>>();
1403    unmeasured.extend(
1404        manifest
1405            .points
1406            .iter()
1407            .filter(|point| declined.contains(&point.file))
1408            .map(|point| point.id.clone()),
1409    );
1410    unmeasured.extend(
1411        manifest
1412            .decisions
1413            .iter()
1414            .filter(|decision| declined.contains(&decision.file))
1415            .map(|decision| decision.id.clone()),
1416    );
1417    unmeasured.extend(
1418        manifest
1419            .branches
1420            .iter()
1421            .filter(|branch| declined.contains(&branch.file))
1422            .map(|branch| branch.id.clone()),
1423    );
1424    manifest.unmeasured = unmeasured.into_iter().collect();
1425    // A boundary declared inside a file that was never compiled says nothing.
1426    manifest.limitations.retain(|limitation| {
1427        limitation
1428            .get("file")
1429            .and_then(|file| file.as_str())
1430            .is_none_or(|file| !declined.contains(file))
1431    });
1432    (manifest, declined)
1433}
1434
1435/// The manifest's structural limitation IDs, as the declaration references them.
1436fn structural_limitations(manifest: &CoverageManifest) -> Vec<String> {
1437    manifest
1438        .limitations
1439        .iter()
1440        .filter_map(|item| {
1441            item.get("id")
1442                .and_then(|value| value.as_str())
1443                .map(str::to_owned)
1444        })
1445        .collect()
1446}
1447
1448pub fn run_prepared_rust_tests(
1449    project: &PreparedRustProject,
1450    command: &[String],
1451    run_id: &str,
1452    generated_at: &str,
1453    diagnostics: &mut dyn Write,
1454) -> Result<RustFrontendRun, RustTestRunnerError> {
1455    let invocation = cargo_invocation(&project.workspace_root, command)?;
1456    let selection = rust_cargo_execution_selection(&invocation)?;
1457    let build_started = Instant::now();
1458    // `cargo test --doc` alone builds nothing here: Cargo refuses `--no-run`
1459    // with `--doc`, and the doctest phase below builds what it runs.
1460    // nextest builds for itself; Cargo builds the libtest artifacts here.
1461    let artifacts = if selection.run_libtests && invocation.kind == RustCargoCommandKind::CargoTest
1462    {
1463        build_test_artifacts(project, command)?
1464    } else {
1465        Vec::new()
1466    };
1467    let build_ms = build_started.elapsed().as_secs_f64() * 1000.0;
1468    // The denominator is what the compiler built, not what the module tree
1469    // resolves: a module behind a `#[cfg]` that is off is resolved, never
1470    // compiled, and could never be covered.
1471    let (manifest, declined) = decline_uncompiled_sources(project);
1472    if !declined.is_empty() {
1473        writeln!(
1474            diagnostics,
1475            "[supercov] {} source file(s) this build did not compile are outside the denominator: {}{}",
1476            declined.len(),
1477            declined
1478                .iter()
1479                .take(3)
1480                .cloned()
1481                .collect::<Vec<_>>()
1482                .join(", "),
1483            if declined.len() > 3 { ", ..." } else { "" }
1484        )
1485        .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1486    }
1487    let project = &PreparedRustProject {
1488        manifest,
1489        ..project.clone()
1490    };
1491    // A signal that reaches the runner alone must not leave the test
1492    // processes running: on 2026-09-07 an orphaned tokio test spun on two
1493    // cores for an hour. The guard covers every child spawned below.
1494    let _signal_guard = crate::child_signal_guard::ChildSignalGuard::install()
1495        .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
1496    let evidence_root = project
1497        .workspace_root
1498        .join(".supercov/rust-evidence")
1499        .join(run_id);
1500    fs::create_dir_all(&evidence_root)
1501        .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1502    let mut results = Vec::new();
1503    let mut overall_exit = 0;
1504    let execution_started = Instant::now();
1505    if invocation.kind == RustCargoCommandKind::NextestRun {
1506        // nextest builds, schedules and retries on its own; this program is
1507        // its target runner and records every attempt it launches.
1508        let outcome = crate::rust_owned_nextest::run_nextest(
1509            project,
1510            &invocation,
1511            &evidence_root.join("nextest"),
1512            run_id,
1513            diagnostics,
1514        )?;
1515        let artifact_count = outcome.artifact_files.len();
1516        return Ok(RustFrontendRun {
1517            declaration: FrontendRunDeclaration {
1518                protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
1519                frontend_id: "rust".into(),
1520                frontend_version: "rust-owned-v1".into(),
1521                language: "rust".into(),
1522                structural_source: StructuralSource::OwnedProbes,
1523                runners: vec![rust_runner_declaration("nextest")],
1524                structural_limitations: structural_limitations(&project.manifest),
1525            },
1526            request: CoverageReportRequest {
1527                run_id: run_id.into(),
1528                manifest: project.manifest.clone(),
1529                raw_results: outcome.results,
1530                generated_at: generated_at.into(),
1531                coverage_model: Some(rust_coverage_model()),
1532                integrity: None,
1533                test_exit_code: ExitCodeInput::Present(Some(outcome.exit_code)),
1534            },
1535            exit_code: outcome.exit_code,
1536            artifacts: artifact_count,
1537            artifact_files: outcome.artifact_files,
1538            build_ms,
1539            execution_ms: execution_started.elapsed().as_secs_f64() * 1000.0,
1540        });
1541    }
1542    // The user's libtest filters, validated against the discovery contract.
1543    // This was computed and never used: every artifact was listed bare, and
1544    // Supercov ran tests plain Cargo had been told to skip. It is read here,
1545    // after the nextest branch: a nextest command carries no libtest
1546    // selection to reconstruct, and nextest applies the user's filters itself.
1547    let libtest_selection = rust_libtest_selection(&invocation)?;
1548    // An ignored test the user asked for with `--ignored` is listed, and must
1549    // then run: `--exact name` alone would report it ignored again.
1550    let ignored_mode = libtest_selection
1551        .list_arguments
1552        .iter()
1553        .filter(|argument| {
1554            matches!(
1555                argument.as_str(),
1556                "--ignored" | "--include-ignored" | "--exclude-should-panic"
1557            )
1558        })
1559        .cloned()
1560        .collect::<Vec<_>>();
1561    let mut tasks = Vec::new();
1562    let target_libdir = if artifacts.is_empty() {
1563        PathBuf::new()
1564    } else {
1565        rustc_target_libdir()?
1566    };
1567    for (artifact_index, artifact) in artifacts.iter().enumerate() {
1568        let tests = list_tests(
1569            &artifact.executable,
1570            &libtest_selection.list_arguments,
1571            &dynamic_library_environment(&target_libdir, &artifact.executable),
1572        )?;
1573        let contexts = preflight_rust_test_contexts(tests.clone())
1574            .map_err(|error| RustTestRunnerError::Context(error.to_string()))?;
1575        for (test_index, test) in tests.into_iter().enumerate() {
1576            let directory = evidence_root.join(format!("{artifact_index:04}-{test_index:08}"));
1577            fs::create_dir(&directory)
1578                .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1579            tasks.push(ProcessTask {
1580                ordinal: tasks.len(),
1581                artifact_index,
1582                test_index,
1583                artifact: artifact.clone(),
1584                context_id: contexts[&test],
1585                test,
1586                directory,
1587            });
1588        }
1589    }
1590    let workers = std::thread::available_parallelism()
1591        .map(usize::from)
1592        .unwrap_or(1)
1593        .min(tasks.len().max(1));
1594    let next = AtomicUsize::new(0);
1595    let outcomes = Mutex::new(Vec::<Result<ProcessOutcome, String>>::with_capacity(
1596        tasks.len(),
1597    ));
1598    std::thread::scope(|scope| {
1599        for _ in 0..workers {
1600            scope.spawn(|| {
1601                loop {
1602                    let index = next.fetch_add(1, Ordering::Relaxed);
1603                    let Some(task) = tasks.get(index) else { break };
1604                    let result = crate::child_signal_guard::output(
1605                        Command::new(&task.artifact.executable)
1606                            // No --nocapture: libtest's in-memory capture is what
1607                            // plain `cargo test` gives users, and it re-emits a
1608                            // failing test's output, which `.output()` still
1609                            // receives. Streaming instead turns every print in a
1610                            // hot loop into an unbuffered stderr syscall: bytes'
1611                            // advance_bytes_mut_remaining_capacity prints per
1612                            // iteration, and --nocapture alone cost 6.9s of its
1613                            // 14.4s (baseline 8.0s streamed vs 1.0s captured).
1614                            .args(["--exact", &task.test])
1615                            .args(&ignored_mode)
1616                            .current_dir(&task.artifact.package_directory)
1617                            .envs(instrumented_stack_environment())
1618                            .envs(dynamic_library_environment(
1619                                &target_libdir,
1620                                &task.artifact.executable,
1621                            ))
1622                            .env("SUPERCOV_RUST_EVIDENCE_DIR", &task.directory)
1623                            .env(
1624                                crate::rust_probe_transport::RUST_CONTEXT_ENV,
1625                                format!("{:016x}", task.context_id),
1626                            ),
1627                    )
1628                    .map(|output| ProcessOutcome {
1629                        task: ProcessTask {
1630                            ordinal: task.ordinal,
1631                            artifact_index: task.artifact_index,
1632                            test_index: task.test_index,
1633                            artifact: task.artifact.clone(),
1634                            test: task.test.clone(),
1635                            context_id: task.context_id,
1636                            directory: task.directory.clone(),
1637                        },
1638                        output,
1639                    })
1640                    .map_err(|error| error.to_string());
1641                    outcomes
1642                        .lock()
1643                        .expect("Rust test result lock poisoned")
1644                        .push(result);
1645                }
1646            });
1647        }
1648    });
1649    let mut outcomes = outcomes
1650        .into_inner()
1651        .map_err(|_| RustTestRunnerError::Io("Rust test result lock poisoned".into()))?
1652        .into_iter()
1653        .map(|result| result.map_err(RustTestRunnerError::Launch))
1654        .collect::<Result<Vec<_>, _>>()?;
1655    outcomes.sort_by_key(|outcome| outcome.task.ordinal);
1656    for outcome in outcomes {
1657        let ProcessTask {
1658            artifact_index,
1659            test_index,
1660            artifact,
1661            test,
1662            directory,
1663            ..
1664        } = outcome.task;
1665        // Target names are not workspace-unique: two packages may both
1666        // expose `lib` or the same integration-test target. Source path +
1667        // libtest name is stable and unique within the frozen workspace.
1668        let test_id = format!("{}::{test}", artifact.source);
1669        let worker_id = format!("artifact-{artifact_index:04}");
1670        let attempt_id = format!("{run_id}:{artifact_index:04}:{test_index:08}");
1671        let output = outcome.output;
1672        let exit = output.status.code().unwrap_or(1);
1673        let stdout = String::from_utf8_lossy(&output.stdout);
1674        let skipped = libtest_skipped(exit, &stdout);
1675        if exit != 0 {
1676            writeln!(diagnostics, "[supercov] Rust test failed: {test_id}")
1677                .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1678            diagnostics
1679                .write_all(&output.stdout)
1680                .and_then(|_| diagnostics.write_all(&output.stderr))
1681                .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1682        }
1683        if exit != 0 {
1684            overall_exit = exit;
1685        }
1686        let evidence = snapshot(&project.manifest, &directory, &attempt_id)?;
1687        results.push(RawTestResult {
1688            test_id: Some(test_id.clone()),
1689            scope: Some(ExecutionScope {
1690                version: 1,
1691                run_id: run_id.into(),
1692                worker_id,
1693                test_id: test_id.clone(),
1694                test_key: format!("{}::{test}", artifact.source),
1695                retry: 0,
1696                attempt_id,
1697            }),
1698            test: test_id,
1699            test_file: Some(artifact.source.clone()),
1700            title: Some(test),
1701            retry: Some(0),
1702            status: Some(
1703                if exit != 0 {
1704                    "failed"
1705                } else if skipped {
1706                    "skipped"
1707                } else {
1708                    "passed"
1709                }
1710                .into(),
1711            ),
1712            expected_status: Some("passed".into()),
1713            flaky: false,
1714            provenance: TestProvenance {
1715                runner: "rust-libtest".into(),
1716                kind: artifact.kind,
1717                project: Some(artifact.name),
1718                source: "supercov-owned-process-per-test".into(),
1719            },
1720            role: "test".into(),
1721            attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
1722            phases: evidence.phases,
1723            runtime: vec![evidence.snapshot],
1724            browser: Vec::new(),
1725            server: Vec::new(),
1726        });
1727    }
1728    let doctest_results =
1729        if selection.run_doctests && invocation.kind == RustCargoCommandKind::CargoTest {
1730            crate::rust_owned_doctests::run_doctests(
1731                project,
1732                &invocation,
1733                &selection,
1734                &evidence_root.join("doctests"),
1735                run_id,
1736                diagnostics,
1737                &mut overall_exit,
1738            )?
1739        } else {
1740            Vec::new()
1741        };
1742    // Only observed runners may be declared. A run with no tests at all keeps
1743    // the libtest declaration, as it always has.
1744    let ran_libtests = !results.is_empty();
1745    let ran_doctests = !doctest_results.is_empty();
1746    results.extend(doctest_results);
1747    let mut runners = Vec::new();
1748    if ran_libtests || !ran_doctests {
1749        runners.push(rust_runner_declaration("rust-libtest"));
1750    }
1751    if ran_doctests {
1752        runners.push(rust_runner_declaration("rustdoc"));
1753    }
1754    let structural_limitations = structural_limitations(&project.manifest);
1755    Ok(RustFrontendRun {
1756        declaration: FrontendRunDeclaration {
1757            protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
1758            frontend_id: "rust".into(),
1759            frontend_version: "rust-owned-v1".into(),
1760            language: "rust".into(),
1761            structural_source: StructuralSource::OwnedProbes,
1762            runners,
1763            structural_limitations,
1764        },
1765        request: CoverageReportRequest {
1766            run_id: run_id.into(),
1767            manifest: project.manifest.clone(),
1768            raw_results: results,
1769            generated_at: generated_at.into(),
1770            coverage_model: Some(rust_coverage_model()),
1771            integrity: None,
1772            test_exit_code: ExitCodeInput::Present(Some(overall_exit)),
1773        },
1774        exit_code: overall_exit,
1775        artifacts: artifacts.len(),
1776        artifact_files: artifacts
1777            .iter()
1778            .map(|artifact| artifact.executable.clone())
1779            .collect(),
1780        build_ms,
1781        execution_ms: execution_started.elapsed().as_secs_f64() * 1000.0,
1782    })
1783}
1784
1785#[cfg(test)]
1786mod tests {
1787    use std::time::{SystemTime, UNIX_EPOCH};
1788
1789    use super::*;
1790    use crate::{
1791        coverage_report::{ArchiveReportRequest, analyze_coverage_archive},
1792        evidence_archive::write_archive,
1793        frontend_protocol::validate_frontend_report_request,
1794        rust_project::prepare_rust_project,
1795    };
1796
1797    #[test]
1798    fn cargo_and_libtest_selection_is_preserved_without_presentation_guessing() {
1799        let root = Path::new(".");
1800        let invocation = cargo_invocation(
1801            root,
1802            &[
1803                "cargo".into(),
1804                "test".into(),
1805                "-p".into(),
1806                "fixture".into(),
1807                "authored".into(),
1808                "--".into(),
1809                "generated".into(),
1810                "--skip".into(),
1811                "slow".into(),
1812                "--include-ignored".into(),
1813            ],
1814        )
1815        .unwrap();
1816        assert_eq!(invocation.arguments, ["test", "-p", "fixture", "authored"]);
1817        assert_eq!(
1818            invocation.runner_arguments,
1819            ["generated", "--skip", "slow", "--include-ignored"]
1820        );
1821        let selection = rust_libtest_selection(&invocation).unwrap();
1822        assert_eq!(
1823            selection.list_arguments,
1824            [
1825                "authored",
1826                "generated",
1827                "--skip",
1828                "slow",
1829                "--include-ignored"
1830            ]
1831        );
1832    }
1833
1834    #[test]
1835    fn direct_cargo_argv_preserves_toml_quotes_inside_config_values() {
1836        let config = "target.host.runner=[\"runner with spaces\",\"--fixed\"]";
1837        let invocation = cargo_invocation(
1838            Path::new("."),
1839            &[
1840                "cargo".into(),
1841                "test".into(),
1842                "--config".into(),
1843                config.into(),
1844            ],
1845        )
1846        .unwrap();
1847        assert_eq!(invocation.arguments, ["test", "--config", config]);
1848    }
1849
1850    #[test]
1851    fn nextest_run_is_detected_without_reclassifying_its_filters_or_retries() {
1852        let invocation = cargo_invocation(
1853            Path::new("."),
1854            &[
1855                "cargo".into(),
1856                "+1.95.0".into(),
1857                "nextest".into(),
1858                "run".into(),
1859                "--retries".into(),
1860                "2".into(),
1861                "-E".into(),
1862                "test(/flaky/)".into(),
1863                "--".into(),
1864                "--nocapture".into(),
1865            ],
1866        )
1867        .unwrap();
1868        assert_eq!(invocation.kind, RustCargoCommandKind::NextestRun);
1869        assert_eq!(
1870            invocation.arguments,
1871            [
1872                "+1.95.0",
1873                "nextest",
1874                "run",
1875                "--retries",
1876                "2",
1877                "-E",
1878                "test(/flaky/)",
1879            ]
1880        );
1881        assert_eq!(invocation.runner_arguments, ["--nocapture"]);
1882        let execution = rust_cargo_execution_selection(&invocation).unwrap();
1883        assert!(execution.run_libtests);
1884        assert!(!execution.run_doctests);
1885        assert!(execution.doctest_arguments.is_empty());
1886        assert!(rust_libtest_selection(&invocation).is_err());
1887        assert_eq!(
1888            nextest_list_invocation(&invocation).unwrap(),
1889            NextestListInvocation {
1890                arguments: vec![
1891                    "+1.95.0".into(),
1892                    "nextest".into(),
1893                    "list".into(),
1894                    "-E".into(),
1895                    "test(/flaky/)".into(),
1896                    "--message-format".into(),
1897                    "json".into(),
1898                ],
1899                runner_arguments: vec!["--nocapture".into()],
1900            }
1901        );
1902    }
1903
1904    #[test]
1905    fn nextest_list_projection_preserves_selection_and_rejects_external_state() {
1906        let invocation = CargoTestInvocation {
1907            program: "cargo".into(),
1908            kind: RustCargoCommandKind::NextestRun,
1909            arguments: vec![
1910                "nextest".into(),
1911                "run".into(),
1912                "--package=fixture".into(),
1913                "--partition".into(),
1914                "hash:1/2".into(),
1915                "--test-threads=8".into(),
1916                "--failure-output".into(),
1917                "final".into(),
1918                "name".into(),
1919            ],
1920            runner_arguments: vec!["--exact".into(), "full::name".into()],
1921        };
1922        assert_eq!(
1923            nextest_list_invocation(&invocation).unwrap(),
1924            NextestListInvocation {
1925                arguments: vec![
1926                    "nextest".into(),
1927                    "list".into(),
1928                    "--package=fixture".into(),
1929                    "--partition".into(),
1930                    "hash:1/2".into(),
1931                    "name".into(),
1932                    "--message-format".into(),
1933                    "json".into(),
1934                ],
1935                runner_arguments: vec!["--exact".into(), "full::name".into()],
1936            }
1937        );
1938
1939        let mut rerun = invocation;
1940        rerun.arguments.extend(["--rerun".into(), "latest".into()]);
1941        assert!(
1942            nextest_list_invocation(&rerun)
1943                .unwrap_err()
1944                .to_string()
1945                .contains("cannot yet be assigned exact selected-test identity")
1946        );
1947    }
1948
1949    #[test]
1950    fn nextest_list_projection_preserves_post_separator_libtest_selection() {
1951        let invocation = cargo_invocation(
1952            Path::new("."),
1953            &[
1954                "cargo".into(),
1955                "nextest".into(),
1956                "run".into(),
1957                "--timings".into(),
1958                "-vv".into(),
1959                "--".into(),
1960                "--include-ignored".into(),
1961                "--skip".into(),
1962                "slow".into(),
1963                "--exact".into(),
1964                "tests::selected".into(),
1965            ],
1966        )
1967        .unwrap();
1968        assert_eq!(
1969            nextest_list_invocation(&invocation).unwrap(),
1970            NextestListInvocation {
1971                arguments: vec![
1972                    "nextest".to_owned(),
1973                    "list".to_owned(),
1974                    "--timings".to_owned(),
1975                    "-vv".to_owned(),
1976                    "--message-format".to_owned(),
1977                    "json".to_owned(),
1978                ],
1979                runner_arguments: vec![
1980                    "--include-ignored".to_owned(),
1981                    "--skip".to_owned(),
1982                    "slow".to_owned(),
1983                    "--exact".to_owned(),
1984                    "tests::selected".to_owned(),
1985                ],
1986            }
1987        );
1988    }
1989
1990    #[test]
1991    fn nextest_version_handshake_preserves_the_cargo_toolchain_selector() {
1992        let invocation = CargoTestInvocation {
1993            program: "cargo".into(),
1994            kind: RustCargoCommandKind::NextestRun,
1995            arguments: vec![
1996                "+1.95.0".into(),
1997                "nextest".into(),
1998                "run".into(),
1999                "-p".into(),
2000                "fixture".into(),
2001            ],
2002            runner_arguments: Vec::new(),
2003        };
2004        assert_eq!(
2005            nextest_version_arguments(&invocation).unwrap(),
2006            ["+1.95.0", "nextest", "--version"]
2007        );
2008    }
2009
2010    #[test]
2011    fn stock_libtest_presentation_and_scheduling_options_do_not_change_discovery() {
2012        let invocation = CargoTestInvocation {
2013            program: "cargo".into(),
2014            kind: RustCargoCommandKind::CargoTest,
2015            arguments: vec!["test".into(), "cargo-filter".into()],
2016            runner_arguments: [
2017                "runner-filter",
2018                "--nocapture",
2019                "--show-output",
2020                "--format=json",
2021                "--color",
2022                "never",
2023                "--test-threads=4",
2024                "--fail-fast",
2025                "--shuffle-seed",
2026                "17",
2027                "-Zunstable-options",
2028                "--exclude-should-panic",
2029            ]
2030            .into_iter()
2031            .map(str::to_owned)
2032            .collect(),
2033        };
2034        let selection = rust_libtest_selection(&invocation).unwrap();
2035        assert_eq!(
2036            selection.list_arguments,
2037            [
2038                "cargo-filter",
2039                "runner-filter",
2040                "-Zunstable-options",
2041                "--exclude-should-panic"
2042            ]
2043        );
2044    }
2045
2046    #[test]
2047    fn cargo_test_options_are_not_mistaken_for_the_test_name_filter() {
2048        let invocation = CargoTestInvocation {
2049            program: "cargo".into(),
2050            kind: RustCargoCommandKind::CargoTest,
2051            arguments: vec![
2052                "test".into(),
2053                "--manifest-path".into(),
2054                "nested/Cargo.toml".into(),
2055                "--features=one,two".into(),
2056                "needle".into(),
2057            ],
2058            runner_arguments: vec!["--ignored".into(), "other".into()],
2059        };
2060        let selection = rust_libtest_selection(&invocation).unwrap();
2061        assert_eq!(selection.list_arguments, ["needle", "--ignored", "other"]);
2062    }
2063
2064    #[test]
2065    fn libtest_thread_count_is_preserved_as_runner_scheduling() {
2066        for arguments in [vec!["--test-threads", "1"], vec!["--test-threads=8"]] {
2067            let invocation = CargoTestInvocation {
2068                program: "cargo".into(),
2069                kind: RustCargoCommandKind::CargoTest,
2070                arguments: vec!["test".into()],
2071                runner_arguments: arguments.into_iter().map(str::to_owned).collect(),
2072            };
2073            let selection = rust_libtest_selection(&invocation).unwrap();
2074            assert!(selection.list_arguments.is_empty());
2075        }
2076    }
2077
2078    #[test]
2079    fn invalid_or_duplicate_libtest_thread_counts_fail_closed() {
2080        for arguments in [
2081            vec!["--test-threads"],
2082            vec!["--test-threads=0"],
2083            vec!["--test-threads=abc"],
2084            vec!["--test-threads", "1", "--test-threads=2"],
2085        ] {
2086            let invocation = CargoTestInvocation {
2087                program: "cargo".into(),
2088                kind: RustCargoCommandKind::CargoTest,
2089                arguments: vec!["test".into()],
2090                runner_arguments: arguments.into_iter().map(str::to_owned).collect(),
2091            };
2092            assert!(rust_libtest_selection(&invocation).is_err());
2093        }
2094    }
2095
2096    #[test]
2097    fn cargo_target_selection_reproduces_when_cargo_runs_doctests() {
2098        let invocation = CargoTestInvocation {
2099            program: "cargo".into(),
2100            kind: RustCargoCommandKind::CargoTest,
2101            arguments: vec![
2102                "test".into(),
2103                "-p".into(),
2104                "fixture".into(),
2105                "needle".into(),
2106            ],
2107            runner_arguments: vec!["--include-ignored".into()],
2108        };
2109        let selection = rust_cargo_execution_selection(&invocation).unwrap();
2110        assert!(selection.run_libtests);
2111        assert!(selection.run_doctests);
2112        assert_eq!(
2113            selection.doctest_arguments,
2114            [
2115                "test",
2116                "--doc",
2117                "-p",
2118                "fixture",
2119                "needle",
2120                "--",
2121                "--include-ignored"
2122            ]
2123        );
2124
2125        let mut explicit_doc = invocation.clone();
2126        explicit_doc.arguments.insert(1, "--doc".into());
2127        let selection = rust_cargo_execution_selection(&explicit_doc).unwrap();
2128        assert!(!selection.run_libtests);
2129        assert!(selection.run_doctests);
2130
2131        for target in ["--lib", "--tests", "--all-targets", "--example=demo"] {
2132            let mut selected = invocation.clone();
2133            selected.arguments.insert(1, target.into());
2134            let selection = rust_cargo_execution_selection(&selected).unwrap();
2135            assert!(selection.run_libtests);
2136            assert!(!selection.run_doctests);
2137        }
2138    }
2139
2140    #[test]
2141    fn a_module_the_build_never_compiles_leaves_the_denominator() {
2142        // `#[cfg(feature = ...)]` with the feature off resolves as a module
2143        // and is never compiled, so nothing in it can ever be covered.
2144        // memchr carries thirteen such files for other architectures.
2145        let nonce = SystemTime::now()
2146            .duration_since(UNIX_EPOCH)
2147            .unwrap()
2148            .as_nanos();
2149        let root = std::env::temp_dir().join(format!(
2150            "supercov-rust-runner-cfg-{}-{nonce}",
2151            std::process::id()
2152        ));
2153        fs::create_dir_all(root.join("src")).unwrap();
2154        fs::write(
2155            root.join("Cargo.toml"),
2156            "[package]\nname='fixture'\nversion='0.0.0'\nedition='2024'\n\n[features]\nextra=[]\n",
2157        )
2158        .unwrap();
2159        fs::write(
2160            root.join("src/lib.rs"),
2161            r#"
2162#[cfg(feature = "extra")]
2163mod extra;
2164
2165pub fn kept(value: i32) -> i32 {
2166    if value > 0 { value } else { -value }
2167}
2168#[cfg(test)]
2169mod tests {
2170    #[test] fn positive() { assert_eq!(super::kept(2), 2); }
2171    #[test] fn negative() { assert_eq!(super::kept(-2), 2); }
2172}
2173"#,
2174        )
2175        .unwrap();
2176        fs::write(
2177            root.join("src/extra.rs"),
2178            "pub fn never_built(value: i32) -> i32 {\n    if value > 0 { 1 } else { 0 }\n}\n",
2179        )
2180        .unwrap();
2181        let project = prepare_rust_project(&root, None).unwrap();
2182        // Discovery still reaches it: the module tree is what rustc resolves.
2183        assert!(
2184            project
2185                .source_files
2186                .iter()
2187                .any(|file| file == "src/extra.rs"),
2188            "{:?}",
2189            project.source_files
2190        );
2191        let run = run_prepared_rust_tests(
2192            &project,
2193            &["cargo".into(), "test".into(), "--lib".into()],
2194            "rust-fixture-cfg",
2195            "2026-08-26T00:00:00.000Z",
2196            &mut Vec::new(),
2197        )
2198        .unwrap();
2199        assert_eq!(run.exit_code, 0);
2200        // Its obligations stay in the manifest, so the evidence token still
2201        // matches and the file is still addressable, but they are declined.
2202        let manifest = &run.request.manifest;
2203        assert!(
2204            manifest
2205                .points
2206                .iter()
2207                .any(|point| point.file == "src/extra.rs"),
2208            "the obligations must stay in the manifest"
2209        );
2210        let declined = manifest.unmeasured.iter().collect::<BTreeSet<_>>();
2211        for point in &manifest.points {
2212            assert_eq!(
2213                declined.contains(&point.id),
2214                point.file == "src/extra.rs",
2215                "{} in {}",
2216                point.id,
2217                point.file
2218            );
2219        }
2220        for decision in &manifest.decisions {
2221            assert_eq!(
2222                declined.contains(&decision.id),
2223                decision.file == "src/extra.rs"
2224            );
2225        }
2226        // The tests cover everything the build compiled.
2227        let report =
2228            crate::frontend_protocol::analyze_frontend_results(&run.declaration, &run.request)
2229                .unwrap();
2230        assert_eq!(
2231            report.view.summary.lines.percentage, 100.0,
2232            "declined obligations must not read as uncovered"
2233        );
2234        fs::remove_dir_all(&root).ok();
2235    }
2236
2237    #[test]
2238    fn assertion_occurrences_are_retained_without_automatic_asserted_credit() {
2239        // Records are appended in execution order, so what a thread wrote
2240        // before it passed an assertion was in scope for that check. Without
2241        // this every Rust line read "execution only", however thoroughly the
2242        // tests checked it.
2243        let nonce = SystemTime::now()
2244            .duration_since(UNIX_EPOCH)
2245            .unwrap()
2246            .as_nanos();
2247        let root = std::env::temp_dir().join(format!(
2248            "supercov-rust-runner-assert-{}-{nonce}",
2249            std::process::id()
2250        ));
2251        fs::create_dir_all(root.join("src")).unwrap();
2252        fs::write(
2253            root.join("Cargo.toml"),
2254            "[package]\nname='fixture'\nversion='0.0.0'\nedition='2024'\n",
2255        )
2256        .unwrap();
2257        fs::write(
2258            root.join("src/lib.rs"),
2259            r#"
2260pub fn checked(value: i32) -> i32 {
2261    value * 2
2262}
2263pub fn unchecked(value: i32) -> i32 {
2264    value + 1
2265}
2266#[cfg(test)]
2267mod tests {
2268    #[test]
2269    fn asserts() {
2270        let doubled = super::checked(2);
2271        assert_eq!(doubled, 4);
2272    }
2273    #[test]
2274    fn asserts_nothing() {
2275        let _ = super::unchecked(1);
2276    }
2277}
2278"#,
2279        )
2280        .unwrap();
2281        let project = prepare_rust_project(&root, None).unwrap();
2282        let run = run_prepared_rust_tests(
2283            &project,
2284            &["cargo".into(), "test".into(), "--lib".into()],
2285            "rust-fixture-assert",
2286            "2026-08-26T00:00:00.000Z",
2287            &mut Vec::new(),
2288        )
2289        .unwrap();
2290        assert_eq!(run.exit_code, 0);
2291
2292        // The asserting test carries an assertion phase that passed; the
2293        // other carries none.
2294        let phases = |test: &str| {
2295            run.request
2296                .raw_results
2297                .iter()
2298                .find(|result| result.test.ends_with(test))
2299                .unwrap_or_else(|| panic!("no test {test}"))
2300                .phases
2301                .clone()
2302        };
2303        let asserting = phases("asserts");
2304        assert!(
2305            !asserting.is_empty(),
2306            "the asserting test recorded no phase"
2307        );
2308        assert!(
2309            asserting
2310                .iter()
2311                .all(|phase| phase.kind == "assertion" && phase.status.as_deref() == Some("passed"))
2312        );
2313        assert!(
2314            phases("asserts_nothing").is_empty(),
2315            "a test that checks nothing witnesses nothing"
2316        );
2317
2318        let report =
2319            crate::frontend_protocol::analyze_frontend_results(&run.declaration, &run.request)
2320                .unwrap();
2321        let level = |line: usize| {
2322            report
2323                .view
2324                .lines
2325                .iter()
2326                .find(|entry| entry.file == "src/lib.rs" && entry.line == line)
2327                .map(|entry| entry.confidence.level.clone())
2328                .unwrap_or_else(|| panic!("no line {line}"))
2329        };
2330        // Both executed. Assertion meaning is supplied later by assertions.json.
2331        assert_eq!(level(3), "executed");
2332        assert_eq!(level(6), "executed");
2333        fs::remove_dir_all(&root).ok();
2334    }
2335
2336    #[test]
2337    fn libtest_filters_select_the_tests_plain_cargo_would_run() {
2338        // The user's filters were parsed and never applied: every artifact
2339        // was listed bare, so `--skip` and a name filter changed nothing and
2340        // Supercov ran a test plain Cargo did not (tokio, 1414 against 1413).
2341        let nonce = SystemTime::now()
2342            .duration_since(UNIX_EPOCH)
2343            .unwrap()
2344            .as_nanos();
2345        let root = std::env::temp_dir().join(format!(
2346            "supercov-rust-runner-filters-{}-{nonce}",
2347            std::process::id()
2348        ));
2349        fs::create_dir_all(root.join("src")).unwrap();
2350        fs::write(
2351            root.join("Cargo.toml"),
2352            "[package]\nname='fixture'\nversion='0.0.0'\nedition='2024'\n",
2353        )
2354        .unwrap();
2355        fs::write(
2356            root.join("src/lib.rs"),
2357            r#"
2358pub fn value(flag: bool) -> i32 { if flag { 1 } else { 0 } }
2359#[cfg(test)]
2360mod tests {
2361    #[test] fn alpha() { assert_eq!(super::value(true), 1); }
2362    #[test] fn beta() { assert_eq!(super::value(false), 0); }
2363    #[test] #[ignore] fn gamma_slow() { assert_eq!(super::value(true), 1); }
2364}
2365"#,
2366        )
2367        .unwrap();
2368        let project = prepare_rust_project(&root, None).unwrap();
2369        let names = |run: &str, command: &[&str]| {
2370            let run = run_prepared_rust_tests(
2371                &project,
2372                &command
2373                    .iter()
2374                    .map(|word| (*word).into())
2375                    .collect::<Vec<String>>(),
2376                run,
2377                "2026-08-26T00:00:00.000Z",
2378                &mut Vec::new(),
2379            )
2380            .unwrap();
2381            assert_eq!(run.exit_code, 0, "{:?}", run.request.raw_results);
2382            let mut names = run
2383                .request
2384                .raw_results
2385                .iter()
2386                .map(|result| {
2387                    (
2388                        result.test.rsplit("::").next().unwrap().to_owned(),
2389                        result.status.clone().unwrap(),
2390                    )
2391                })
2392                .collect::<Vec<_>>();
2393            names.sort();
2394            names
2395        };
2396        // Everything: the ignored test is listed and reported skipped.
2397        assert_eq!(
2398            names("rust-fixture-filters-1", &["cargo", "test", "--lib"]),
2399            [
2400                ("alpha".to_owned(), "passed".to_owned()),
2401                ("beta".to_owned(), "passed".to_owned()),
2402                ("gamma_slow".to_owned(), "skipped".to_owned()),
2403            ]
2404        );
2405        // A name filter and `--skip` narrow the enumeration as libtest does.
2406        assert_eq!(
2407            names(
2408                "rust-fixture-filters-2",
2409                &["cargo", "test", "--lib", "alpha"]
2410            ),
2411            [("alpha".to_owned(), "passed".to_owned())]
2412        );
2413        assert_eq!(
2414            names(
2415                "rust-fixture-filters-3",
2416                &["cargo", "test", "--lib", "--", "--skip", "beta"]
2417            ),
2418            [
2419                ("alpha".to_owned(), "passed".to_owned()),
2420                ("gamma_slow".to_owned(), "skipped".to_owned()),
2421            ]
2422        );
2423        // `--ignored` lists the ignored test, and then it has to RUN.
2424        assert_eq!(
2425            names(
2426                "rust-fixture-filters-4",
2427                &["cargo", "test", "--lib", "--", "--ignored"]
2428            ),
2429            [("gamma_slow".to_owned(), "passed".to_owned())]
2430        );
2431        fs::remove_dir_all(&root).ok();
2432    }
2433
2434    #[test]
2435    fn libtest_processes_run_in_their_package_directory() {
2436        // Cargo runs a test binary in its package's directory, and tokio's
2437        // `basic_fs` reads `Cargo.toml` from there; the workspace root has a
2438        // different manifest.
2439        let nonce = SystemTime::now()
2440            .duration_since(UNIX_EPOCH)
2441            .unwrap()
2442            .as_nanos();
2443        let root = std::env::temp_dir().join(format!(
2444            "supercov-rust-runner-cwd-{}-{nonce}",
2445            std::process::id()
2446        ));
2447        fs::create_dir_all(root.join("member/src")).unwrap();
2448        fs::write(
2449            root.join("Cargo.toml"),
2450            "[workspace]\nmembers = ['member']\nresolver = '2'\n",
2451        )
2452        .unwrap();
2453        fs::write(
2454            root.join("member/Cargo.toml"),
2455            "[package]\nname='member'\nversion='0.0.0'\nedition='2024'\n",
2456        )
2457        .unwrap();
2458        fs::write(
2459            root.join("member/src/lib.rs"),
2460            r#"
2461pub fn manifest() -> String {
2462    std::fs::read_to_string("Cargo.toml").unwrap()
2463}
2464#[cfg(test)]
2465mod tests {
2466    #[test] fn reads_own_manifest() { assert!(super::manifest().contains("name='member'")); }
2467}
2468"#,
2469        )
2470        .unwrap();
2471        let project = prepare_rust_project(&root, None).unwrap();
2472        let run = run_prepared_rust_tests(
2473            &project,
2474            &["cargo".into(), "test".into(), "--lib".into()],
2475            "rust-fixture-cwd",
2476            "2026-08-26T00:00:00.000Z",
2477            &mut Vec::new(),
2478        )
2479        .unwrap();
2480        assert_eq!(run.exit_code, 0, "{:?}", run.request.raw_results);
2481        assert_eq!(run.request.raw_results.len(), 1);
2482        assert_eq!(run.request.raw_results[0].status.as_deref(), Some("passed"));
2483        fs::remove_dir_all(&root).ok();
2484    }
2485
2486    #[test]
2487    fn cargo_libtest_runs_produce_queryable_owned_evidence() {
2488        let nonce = SystemTime::now()
2489            .duration_since(UNIX_EPOCH)
2490            .unwrap()
2491            .as_nanos();
2492        let root = std::env::temp_dir().join(format!(
2493            "supercov-rust-runner-{}-{nonce}",
2494            std::process::id()
2495        ));
2496        fs::create_dir_all(root.join("src")).unwrap();
2497        fs::write(
2498            root.join("Cargo.toml"),
2499            "[package]\nname='fixture'\nversion='0.0.0'\nedition='2024'\n",
2500        )
2501        .unwrap();
2502        fs::write(
2503            root.join("src/lib.rs"),
2504            r#"
2505pub fn choose(left: bool, right: bool) -> i32 {
2506    if left && right { 1 } else { 0 }
2507}
2508pub fn pick(value: i32) -> &'static str {
2509    match value {
2510        0 => "zero",
2511        1 => "one",
2512        _ => "many",
2513    }
2514}
2515pub fn total(values: &[i32]) -> i32 {
2516    let mut sum = 0;
2517    for value in values {
2518        sum += value;
2519    }
2520    sum
2521}
2522pub fn first_even(values: &[i32]) -> Option<i32> {
2523    let mut index = 0;
2524    while index < values.len() {
2525        if values[index] % 2 == 0 {
2526            return Some(values[index]);
2527        }
2528        index += 1;
2529    }
2530    None
2531}
2532pub fn parse_twice(text: &str) -> Option<i32> {
2533    let value: i32 = text.parse().ok()?;
2534    Some(value * 2)
2535}
2536pub fn describe(value: Option<i32>, flag: bool) -> &'static str {
2537    if let Some(inner) = value && inner > 0 && flag {
2538        "positive"
2539    } else {
2540        "other"
2541    }
2542}
2543pub fn depth(n: u32) -> Result<u32, String> {
2544    if n == 0 {
2545        Ok(0)
2546    } else {
2547        let below = depth(n - 1)?;
2548        Ok(below + 1)
2549    }
2550}
2551#[cfg(test)]
2552mod tests {
2553    #[test] fn false_path() { assert_eq!(super::choose(false, true), 0); }
2554    #[test] fn true_path() { assert_eq!(super::choose(true, true), 1); }
2555    #[test] #[ignore] fn ignored_path() { unreachable!(); }
2556    #[test] fn pick_zero() { assert_eq!(super::pick(0), "zero"); }
2557    #[test] fn pick_many() { assert_eq!(super::pick(7), "many"); }
2558    #[test] fn total_empty() { assert_eq!(super::total(&[]), 0); }
2559    #[test] fn total_some() { assert_eq!(super::total(&[1, 2]), 3); }
2560    #[test] fn first_even_empty() { assert_eq!(super::first_even(&[]), None); }
2561    #[test] fn first_even_found() { assert_eq!(super::first_even(&[1, 4]), Some(4)); }
2562    #[test] fn parse_ok() { assert_eq!(super::parse_twice("4"), Some(8)); }
2563    #[test] fn parse_bad() { assert_eq!(super::parse_twice("x"), None); }
2564    #[test] fn chain_taken() { assert_eq!(super::describe(Some(1), true), "positive"); }
2565    #[test] fn chain_pattern_fails() { assert_eq!(super::describe(None, true), "other"); }
2566    #[test] fn chain_negative() { assert_eq!(super::describe(Some(-1), true), "other"); }
2567    #[test] fn chain_flag_fails() { assert_eq!(super::describe(Some(1), false), "other"); }
2568    #[test] fn deep_recursion() { assert_eq!(super::depth(5_000), Ok(5_000)); }
2569}
2570"#,
2571        )
2572        .unwrap();
2573        let project = prepare_rust_project(&root, None).unwrap();
2574        // Doctests need the CLI binary standing in for rustdoc, which this
2575        // test binary cannot do; scripts/rust-public-cargo-integration.mjs
2576        // covers them end to end. `--lib` keeps this run to the libtests.
2577        let run = run_prepared_rust_tests(
2578            &project,
2579            &["cargo".into(), "test".into(), "--lib".into()],
2580            "rust-fixture",
2581            "2026-08-26T00:00:00.000Z",
2582            &mut Vec::new(),
2583        )
2584        .unwrap();
2585        assert_eq!(run.exit_code, 0);
2586        assert_eq!(run.request.raw_results.len(), 16);
2587        let statuses = run
2588            .request
2589            .raw_results
2590            .iter()
2591            .filter_map(|result| result.status.as_deref())
2592            .collect::<Vec<_>>();
2593        assert_eq!(
2594            statuses
2595                .iter()
2596                .filter(|status| **status == "skipped")
2597                .count(),
2598            1
2599        );
2600        assert_eq!(
2601            statuses
2602                .iter()
2603                .filter(|status| **status == "passed")
2604                .count(),
2605            15
2606        );
2607
2608        // The let chain's condition vectors, with the pattern's outcome
2609        // derived: [let Some(inner) = value, inner > 0, flag].
2610        let chain_vectors = |test: &str| {
2611            let result = run
2612                .request
2613                .raw_results
2614                .iter()
2615                .find(|result| result.test.ends_with(test))
2616                .unwrap_or_else(|| panic!("no test {test}"));
2617            let snapshot = result
2618                .runtime
2619                .iter()
2620                .flat_map(|snapshot| &snapshot.decisions)
2621                .find(|decision| decision.meta.source.starts_with("let Some(inner) = value"))
2622                .unwrap_or_else(|| panic!("{test} recorded no chain decision"));
2623            snapshot
2624                .vectors
2625                .iter()
2626                .map(|vector| (vector.values.clone(), vector.outcome))
2627                .collect::<Vec<_>>()
2628        };
2629        assert_eq!(
2630            chain_vectors("chain_taken"),
2631            [(vec![Some(true), Some(true), Some(true)], true)]
2632        );
2633        assert_eq!(
2634            chain_vectors("chain_pattern_fails"),
2635            [(vec![Some(false), None, None], false)]
2636        );
2637        assert_eq!(
2638            chain_vectors("chain_negative"),
2639            [(vec![Some(true), Some(false), None], false)]
2640        );
2641        assert_eq!(
2642            chain_vectors("chain_flag_fails"),
2643            [(vec![Some(true), Some(true), Some(false)], false)]
2644        );
2645        validate_frontend_report_request(&run.declaration, &run.request).unwrap();
2646        let archive = root.join("evidence.raw.gz");
2647        write_archive(run.archive_entries().unwrap(), &archive).unwrap();
2648        let report = analyze_coverage_archive(&ArchiveReportRequest {
2649            archive_path: archive,
2650            run_id: "rust-fixture".into(),
2651            generated_at: "2026-08-26T00:00:00.000Z".into(),
2652            integrity: None,
2653            test_exit_code: ExitCodeInput::Present(Some(0)),
2654        })
2655        .unwrap();
2656        assert_eq!(report.view.tests.len(), 16);
2657        assert!(report.view.summary.lines.covered > 0);
2658        assert!(report.view.summary.decisions > 0);
2659
2660        // Loops, the try operator and the logical operator, each with both
2661        // outcomes attributed to the test that produced it.
2662        let single = |kind: &str| {
2663            let mut found = report
2664                .view
2665                .branches
2666                .iter()
2667                .filter(|branch| branch.meta.kind == kind);
2668            let branch = found.next().unwrap_or_else(|| panic!("no {kind} branch"));
2669            assert!(found.next().is_none(), "more than one {kind} branch");
2670            branch
2671        };
2672        let tests_of = |branch: &crate::coverage_report::BranchResult, label: &str| {
2673            branch
2674                .alternatives
2675                .iter()
2676                .find(|alternative| alternative.label == label)
2677                .unwrap_or_else(|| panic!("{} has no alternative {label}", branch.meta.kind))
2678                .tests
2679                .clone()
2680        };
2681        let for_loop = single("for-loop");
2682        assert_eq!(
2683            tests_of(for_loop, "zero iterations"),
2684            ["src/lib.rs::tests::total_empty"]
2685        );
2686        assert_eq!(
2687            tests_of(for_loop, "entered"),
2688            ["src/lib.rs::tests::total_some"]
2689        );
2690        let while_loop = single("while-loop");
2691        assert_eq!(
2692            tests_of(while_loop, "zero iterations"),
2693            ["src/lib.rs::tests::first_even_empty"]
2694        );
2695        assert_eq!(
2696            tests_of(while_loop, "entered"),
2697            ["src/lib.rs::tests::first_even_found"]
2698        );
2699        // `depth` adds a second `?`; the one in `parse_twice` is the one
2700        // exercised both ways.
2701        let try_operator = report
2702            .view
2703            .branches
2704            .iter()
2705            .find(|branch| {
2706                branch.meta.kind == "try-operator" && branch.meta.source.contains("parse().ok()")
2707            })
2708            .expect("parse_twice's try operator");
2709        assert_eq!(
2710            tests_of(try_operator, "continued"),
2711            ["src/lib.rs::tests::parse_ok"]
2712        );
2713        assert_eq!(
2714            tests_of(try_operator, "early return"),
2715            ["src/lib.rs::tests::parse_bad"]
2716        );
2717        let mut logical = report
2718            .view
2719            .branches
2720            .iter()
2721            .filter(|branch| branch.meta.kind == "logical-and")
2722            .collect::<Vec<_>>();
2723        logical.sort_by_key(|branch| (branch.meta.line, branch.meta.column));
2724        // `left && right` in choose, then the chain's two operators.
2725        assert_eq!(logical.len(), 3);
2726        assert_eq!(
2727            tests_of(logical[0], "short-circuited"),
2728            ["src/lib.rs::tests::false_path"]
2729        );
2730        assert_eq!(
2731            tests_of(logical[0], "right operand evaluated"),
2732            ["src/lib.rs::tests::true_path"]
2733        );
2734        assert_eq!(
2735            tests_of(logical[1], "short-circuited"),
2736            ["src/lib.rs::tests::chain_pattern_fails"]
2737        );
2738        assert_eq!(
2739            tests_of(logical[1], "right operand evaluated"),
2740            [
2741                "src/lib.rs::tests::chain_flag_fails",
2742                "src/lib.rs::tests::chain_negative",
2743                "src/lib.rs::tests::chain_taken",
2744            ]
2745        );
2746        assert_eq!(
2747            tests_of(logical[2], "short-circuited"),
2748            [
2749                "src/lib.rs::tests::chain_negative",
2750                "src/lib.rs::tests::chain_pattern_fails",
2751            ]
2752        );
2753        assert_eq!(
2754            tests_of(logical[2], "right operand evaluated"),
2755            [
2756                "src/lib.rs::tests::chain_flag_fails",
2757                "src/lib.rs::tests::chain_taken",
2758            ]
2759        );
2760        assert!(for_loop.covered && while_loop.covered && try_operator.covered);
2761        assert!(logical.iter().all(|branch| branch.covered));
2762
2763        // The match in `pick`: pick(0) selects the first arm; pick(7) passes
2764        // the first two over and selects the last. Nothing selects `1`.
2765        let mut arms = report
2766            .view
2767            .branches
2768            .iter()
2769            .filter(|branch| branch.meta.kind == "match-arm")
2770            .collect::<Vec<_>>();
2771        arms.sort_by_key(|branch| branch.meta.line);
2772        assert_eq!(arms.len(), 3);
2773        let alternative = |arm: usize, label: &str| {
2774            arms[arm]
2775                .alternatives
2776                .iter()
2777                .find(|alternative| alternative.label == label)
2778                .unwrap_or_else(|| panic!("arm {arm} has no alternative {label}"))
2779        };
2780        assert_eq!(
2781            alternative(0, "selected").tests,
2782            ["src/lib.rs::tests::pick_zero"]
2783        );
2784        assert_eq!(
2785            alternative(0, "not selected").tests,
2786            ["src/lib.rs::tests::pick_many"]
2787        );
2788        assert!(!alternative(1, "selected").covered);
2789        assert_eq!(
2790            alternative(1, "not selected").tests,
2791            ["src/lib.rs::tests::pick_many"]
2792        );
2793        assert_eq!(arms[2].alternatives.len(), 1);
2794        assert_eq!(
2795            alternative(2, "selected").tests,
2796            ["src/lib.rs::tests::pick_many"]
2797        );
2798        assert!(arms[0].covered && !arms[1].covered && arms[2].covered);
2799
2800        fs::remove_dir_all(root).unwrap();
2801    }
2802}