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