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