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