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. Cargo builds each test
4//! artifact once. Ordinary libtest artifacts execute once with the selected
5//! toolchain's exact Supercov companion, preserving stock scheduling, capture,
6//! process-global state and presentation while authenticated lifecycle events
7//! provide exact attempt identity. Nextest and opaque custom runners retain
8//! their own intrinsic process boundaries.
9
10use std::{
11    collections::{BTreeMap, BTreeSet},
12    fs,
13    io::Write,
14    path::{Component, Path, PathBuf},
15    process::{Command, Output},
16    sync::{
17        Mutex,
18        atomic::{AtomicUsize, Ordering},
19    },
20    time::Instant,
21};
22
23use serde::Deserialize;
24use supercov_contracts::{
25    AttributionPrecision, ExecutionModel, FrontendAttribution, FrontendLimitation,
26    FrontendLimitationScope, FrontendRunDeclaration, FrontendRunnerDeclaration,
27    LANGUAGE_FRONTEND_PROTOCOL_VERSION, StructuralSource,
28};
29
30use crate::{
31    coverage_analysis::McdcVector,
32    coverage_report::{
33        CoverageManifest, CoverageModelDeclaration, CoverageReportRequest, DecisionMeta,
34        DecisionSnapshot, ExecutionScope, ExitCodeInput, PersistedCoverageModel, RawTestResult,
35        RuntimeSnapshot, TestProvenance,
36    },
37    evidence_archive::EvidenceArchiveEntry,
38    rust_project::PreparedRustProject,
39    rust_runtime::{RustProbeObservation, read_rust_probe_directory},
40    rust_test_context::preflight_rust_test_contexts,
41};
42
43#[derive(Debug, Clone, PartialEq)]
44pub struct RustFrontendRun {
45    pub declaration: FrontendRunDeclaration,
46    pub request: CoverageReportRequest,
47    pub exit_code: i32,
48    pub artifacts: usize,
49    pub artifact_files: Vec<PathBuf>,
50    pub build_ms: f64,
51    pub execution_ms: f64,
52}
53
54impl RustFrontendRun {
55    pub fn archive_entries(&self) -> Result<Vec<EvidenceArchiveEntry>, serde_json::Error> {
56        let model = PersistedCoverageModel::from_declaration(
57            self.request
58                .coverage_model
59                .as_ref()
60                .expect("Rust frontend always declares a coverage model"),
61        )
62        .expect("Rust coverage model is contract-valid");
63        let mut entries = vec![
64            EvidenceArchiveEntry {
65                path: "coverage-model.json".into(),
66                contents: serde_json::to_vec(&model)?,
67            },
68            EvidenceArchiveEntry {
69                path: "frontend.json".into(),
70                contents: serde_json::to_vec(&self.declaration)?,
71            },
72            EvidenceArchiveEntry {
73                path: "manifest.json".into(),
74                contents: serde_json::to_vec(&self.request.manifest)?,
75            },
76        ];
77        for (index, result) in self.request.raw_results.iter().enumerate() {
78            entries.push(EvidenceArchiveEntry {
79                path: format!("results/{index:08}/mcdc.json"),
80                contents: serde_json::to_vec(result)?,
81            });
82        }
83        Ok(entries)
84    }
85}
86
87#[derive(Debug)]
88pub enum RustTestRunnerError {
89    UnsupportedCommand(String),
90    Launch(String),
91    CargoFailed(String),
92    CargoJson(String),
93    UnsafeArtifact(String),
94    ListFailed(String),
95    Probe(String),
96    Context(String),
97    UnknownProbe(String),
98    InvalidVector {
99        id: String,
100        expected: usize,
101        actual: usize,
102    },
103    Json(serde_json::Error),
104    Io(String),
105}
106
107impl std::fmt::Display for RustTestRunnerError {
108    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        match self {
110            Self::UnsupportedCommand(reason) => formatter.write_str(reason),
111            Self::Launch(reason) => {
112                write!(formatter, "could not launch Rust test process: {reason}")
113            }
114            Self::CargoFailed(reason) => write!(formatter, "Cargo test build failed: {reason}"),
115            Self::CargoJson(reason) => write!(formatter, "invalid Cargo JSON output: {reason}"),
116            Self::UnsafeArtifact(path) => {
117                write!(formatter, "Cargo emitted an unsafe test artifact: {path}")
118            }
119            Self::ListFailed(reason) => {
120                write!(formatter, "could not enumerate Rust tests: {reason}")
121            }
122            Self::Probe(reason) => write!(formatter, "invalid Rust probe evidence: {reason}"),
123            Self::Context(reason) => write!(formatter, "invalid Rust test context: {reason}"),
124            Self::UnknownProbe(id) => write!(
125                formatter,
126                "Rust runtime emitted an unknown obligation: {id}"
127            ),
128            Self::InvalidVector {
129                id,
130                expected,
131                actual,
132            } => write!(
133                formatter,
134                "Rust decision {id} emitted vector width {actual}; expected {expected}"
135            ),
136            Self::Json(error) => write!(formatter, "could not encode Rust evidence: {error}"),
137            Self::Io(reason) => formatter.write_str(reason),
138        }
139    }
140}
141
142impl std::error::Error for RustTestRunnerError {}
143
144impl From<serde_json::Error> for RustTestRunnerError {
145    fn from(value: serde_json::Error) -> Self {
146        Self::Json(value)
147    }
148}
149
150#[derive(Debug, Deserialize)]
151struct CargoMessage {
152    reason: String,
153    #[serde(default)]
154    target: Option<CargoArtifactTarget>,
155    #[serde(default)]
156    profile: Option<CargoArtifactProfile>,
157    executable: Option<PathBuf>,
158}
159
160#[derive(Debug, Deserialize)]
161struct CargoArtifactTarget {
162    name: String,
163    kind: Vec<String>,
164    src_path: PathBuf,
165}
166
167#[derive(Debug, Deserialize)]
168struct CargoArtifactProfile {
169    test: bool,
170}
171
172#[derive(Debug, Clone)]
173struct TestArtifact {
174    executable: PathBuf,
175    name: String,
176    kind: String,
177    source: String,
178}
179
180#[derive(Debug)]
181struct ProcessTask {
182    ordinal: usize,
183    artifact_index: usize,
184    test_index: usize,
185    artifact: TestArtifact,
186    test: String,
187    context_id: u64,
188    directory: PathBuf,
189}
190
191#[derive(Debug)]
192struct ProcessOutcome {
193    task: ProcessTask,
194    output: Output,
195}
196
197fn shell_words(value: &str) -> Result<Vec<String>, RustTestRunnerError> {
198    let mut words = Vec::new();
199    let mut current = String::new();
200    let mut quote = None;
201    let mut escaped = false;
202    for character in value.chars() {
203        if escaped {
204            current.push(character);
205            escaped = false;
206        } else if character == '\\' && quote != Some('\'') {
207            escaped = true;
208        } else if matches!(character, '\'' | '"') {
209            if quote == Some(character) {
210                quote = None;
211            } else if quote.is_none() {
212                quote = Some(character);
213            } else {
214                current.push(character);
215            }
216        } else if character.is_whitespace() && quote.is_none() {
217            if !current.is_empty() {
218                words.push(std::mem::take(&mut current));
219            }
220        } else {
221            current.push(character);
222        }
223    }
224    if escaped || quote.is_some() {
225        return Err(RustTestRunnerError::UnsupportedCommand(
226            "the expanded Cargo command contains an incomplete quote or escape".into(),
227        ));
228    }
229    if !current.is_empty() {
230        words.push(current);
231    }
232    Ok(words)
233}
234
235fn executable_name(value: &str) -> &str {
236    Path::new(value)
237        .file_name()
238        .and_then(|name| name.to_str())
239        .unwrap_or(value)
240        .trim_end_matches(".exe")
241        .trim_end_matches(".cmd")
242}
243
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub(crate) struct CargoTestInvocation {
246    pub program: String,
247    pub kind: RustCargoCommandKind,
248    pub arguments: Vec<String>,
249    pub runner_arguments: Vec<String>,
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub(crate) enum RustCargoCommandKind {
254    CargoTest,
255    NextestRun,
256}
257
258impl CargoTestInvocation {
259    pub(crate) fn command_position(&self) -> Option<usize> {
260        match self.kind {
261            RustCargoCommandKind::CargoTest => self
262                .arguments
263                .iter()
264                .position(|argument| argument == "test"),
265            RustCargoCommandKind::NextestRun => self
266                .arguments
267                .windows(2)
268                .position(|pair| pair == ["nextest", "run"]),
269        }
270    }
271}
272
273#[derive(Debug, Clone, PartialEq, Eq)]
274pub(crate) struct RustLibtestSelection {
275    pub list_arguments: Vec<String>,
276}
277
278#[derive(Debug, Clone, PartialEq, Eq)]
279pub(crate) struct RustCargoExecutionSelection {
280    pub run_libtests: bool,
281    pub run_doctests: bool,
282    pub doctest_arguments: Vec<String>,
283}
284
285#[derive(Debug, Clone, PartialEq, Eq)]
286pub(crate) struct NextestListInvocation {
287    pub arguments: Vec<String>,
288    pub runner_arguments: Vec<String>,
289}
290
291pub(crate) fn nextest_version_arguments(
292    invocation: &CargoTestInvocation,
293) -> Result<Vec<String>, RustTestRunnerError> {
294    if invocation.kind != RustCargoCommandKind::NextestRun {
295        return Err(RustTestRunnerError::UnsupportedCommand(
296            "a nextest version handshake requires `cargo nextest run`".into(),
297        ));
298    }
299    let command = invocation.command_position().ok_or_else(|| {
300        RustTestRunnerError::UnsupportedCommand(
301            "the expanded Cargo invocation lost its nextest run subcommand".into(),
302        )
303    })?;
304    let mut arguments = invocation.arguments[..command].to_vec();
305    arguments.extend(["nextest".into(), "--version".into()]);
306    Ok(arguments)
307}
308
309fn nextest_run_only_option(argument: &str) -> Option<bool> {
310    let name = argument.split_once('=').map_or(argument, |(name, _)| name);
311    match name {
312        "-j"
313        | "--jobs"
314        | "--test-threads"
315        | "--retries"
316        | "--flaky-result"
317        | "--max-fail"
318        | "--no-tests"
319        | "--failure-output"
320        | "--success-output"
321        | "--status-level"
322        | "--final-status-level"
323        | "--show-progress"
324        | "--max-progress-running"
325        | "--message-format"
326        | "--message-format-version" => Some(!argument.contains('=')),
327        "--fail-fast"
328        | "--ff"
329        | "--no-fail-fast"
330        | "--nff"
331        | "--no-capture"
332        | "--nocapture"
333        | "--no-output-indent"
334        | "--hide-progress-bar"
335        | "--no-input-handler" => Some(false),
336        _ if argument.starts_with("-j") && argument.len() > 2 => Some(false),
337        _ => None,
338    }
339}
340
341fn nextest_unsupported_run_option(argument: &str) -> Option<bool> {
342    let name = argument.split_once('=').map_or(argument, |(name, _)| name);
343    match name {
344        "-R"
345        | "--rerun"
346        | "--debugger"
347        | "--tracer"
348        | "--stress-count"
349        | "--stress-duration"
350        | "--archive-file"
351        | "--archive-format"
352        | "--extract-to"
353        | "--cargo-metadata"
354        | "--workspace-remap"
355        | "--binaries-metadata"
356        | "--target-dir-remap"
357        | "--build-dir-remap" => Some(!argument.contains('=')),
358        "--no-run" | "--extract-overwrite" | "--persist-extract-tempdir" => Some(false),
359        _ => None,
360    }
361}
362
363fn nextest_shared_option(argument: &str) -> Option<bool> {
364    let name = argument.split_once('=').map_or(argument, |(name, _)| name);
365    match name {
366        "--color"
367        | "-p"
368        | "--package"
369        | "--exclude"
370        | "--bin"
371        | "--example"
372        | "--test"
373        | "--bench"
374        | "-F"
375        | "--features"
376        | "--build-jobs"
377        | "--cargo-profile"
378        | "--target"
379        | "--target-dir"
380        | "--cargo-message-format"
381        | "--config"
382        | "--timings"
383        | "-Z"
384        | "--run-ignored"
385        | "--partition"
386        | "--platform-filter"
387        | "-E"
388        | "--filterset"
389        | "--filter-expr"
390        | "--manifest-path"
391        | "--config-file"
392        | "--user-config-file"
393        | "--tool-config-file"
394        | "-P"
395        | "--profile" => Some(!argument.contains('=')),
396        "--no-pager"
397        | "-v"
398        | "--verbose"
399        | "--workspace"
400        | "--all"
401        | "--lib"
402        | "--bins"
403        | "--examples"
404        | "--tests"
405        | "--benches"
406        | "--all-targets"
407        | "--all-features"
408        | "--no-default-features"
409        | "-r"
410        | "--release"
411        | "--unit-graph"
412        | "--frozen"
413        | "--locked"
414        | "--offline"
415        | "--cargo-quiet"
416        | "--cargo-verbose"
417        | "--ignore-rust-version"
418        | "--future-incompat-report"
419        | "--ignore-default-filter"
420        | "--override-version-check" => Some(false),
421        _ if argument.starts_with("-p") && argument.len() > 2 => Some(false),
422        _ if argument.starts_with("-F") && argument.len() > 2 => Some(false),
423        _ if argument.starts_with("-E") && argument.len() > 2 => Some(false),
424        _ if argument.starts_with("-P") && argument.len() > 2 => Some(false),
425        _ if argument.starts_with("-Z") && argument.len() > 2 => Some(false),
426        _ if argument.len() > 2
427            && argument.starts_with('-')
428            && argument[1..].bytes().all(|byte| byte == b'v') =>
429        {
430            Some(false)
431        }
432        _ if argument.starts_with("--timings=") => Some(false),
433        _ => None,
434    }
435}
436
437/// Reprojects a pinned `nextest run` invocation into the stable machine-readable
438/// `nextest list` contract. Selection/build/configuration arguments are kept
439/// byte-for-byte. Runner and presentation arguments are removed because they
440/// do not affect the selected-test catalog. Options whose selection semantics
441/// require an external recording or a different execution mode fail closed.
442pub(crate) fn nextest_list_invocation(
443    invocation: &CargoTestInvocation,
444) -> Result<NextestListInvocation, RustTestRunnerError> {
445    if invocation.kind != RustCargoCommandKind::NextestRun {
446        return Err(RustTestRunnerError::UnsupportedCommand(
447            "a nextest list projection requires `cargo nextest run`".into(),
448        ));
449    }
450    let command = invocation.command_position().ok_or_else(|| {
451        RustTestRunnerError::UnsupportedCommand(
452            "the expanded Cargo invocation lost its nextest run subcommand".into(),
453        )
454    })?;
455    let mut arguments = invocation.arguments[..command].to_vec();
456    arguments.extend(["nextest".into(), "list".into()]);
457    let mut index = command + 2;
458    while index < invocation.arguments.len() {
459        let argument = &invocation.arguments[index];
460        if argument == "--" {
461            arguments.extend(invocation.arguments[index..].iter().cloned());
462            break;
463        }
464        if let Some(takes_value) = nextest_unsupported_run_option(argument) {
465            if takes_value && invocation.arguments.get(index + 1).is_none() {
466                return Err(RustTestRunnerError::UnsupportedCommand(format!(
467                    "nextest option {argument} has no value"
468                )));
469            }
470            return Err(RustTestRunnerError::UnsupportedCommand(format!(
471                "nextest option {argument} cannot yet be assigned exact selected-test identity"
472            )));
473        }
474        if let Some(takes_value) = nextest_run_only_option(argument) {
475            if takes_value {
476                index += 1;
477                if index == invocation.arguments.len() {
478                    return Err(RustTestRunnerError::UnsupportedCommand(format!(
479                        "nextest option {argument} has no value"
480                    )));
481                }
482            }
483        } else if let Some(takes_value) = nextest_shared_option(argument) {
484            arguments.push(argument.clone());
485            if takes_value {
486                index += 1;
487                let value = invocation.arguments.get(index).ok_or_else(|| {
488                    RustTestRunnerError::UnsupportedCommand(format!(
489                        "nextest option {argument} has no value"
490                    ))
491                })?;
492                arguments.push(value.clone());
493            }
494        } else if argument.starts_with('-') {
495            return Err(RustTestRunnerError::UnsupportedCommand(format!(
496                "the pinned nextest run contract does not recognize option {argument}"
497            )));
498        } else {
499            arguments.push(argument.clone());
500        }
501        index += 1;
502    }
503    arguments.extend(["--message-format".into(), "json".into()]);
504    Ok(NextestListInvocation {
505        arguments,
506        runner_arguments: invocation.runner_arguments.clone(),
507    })
508}
509
510pub(crate) fn cargo_invocation(
511    root: &Path,
512    command: &[String],
513) -> Result<CargoTestInvocation, RustTestRunnerError> {
514    // A process argv is already tokenized. Joining and shell-parsing a direct
515    // Cargo command destroys quotes that are payload (not shell syntax), most
516    // notably TOML strings passed to Cargo's --config. Only opaque wrapper or
517    // package-script commands need textual expansion and shell tokenization.
518    let words = if command.iter().any(|word| executable_name(word) == "cargo") {
519        command.to_vec()
520    } else {
521        let expanded = crate::project_discovery::expanded_command(root, command);
522        shell_words(&expanded)?
523    };
524    let cargo = words
525        .iter()
526        .position(|word| executable_name(word) == "cargo")
527        .ok_or_else(|| RustTestRunnerError::UnsupportedCommand(
528            "Rust was detected, but the expanded command does not expose a stable Cargo invocation".into(),
529        ))?;
530    let cargo_test = words[cargo + 1..]
531        .iter()
532        .position(|word| word == "test")
533        .map(|position| cargo + 1 + position);
534    let nextest = words[cargo + 1..]
535        .windows(2)
536        .position(|pair| pair == ["nextest", "run"])
537        .map(|position| cargo + 1 + position);
538    let (kind, command) = match (cargo_test, nextest) {
539        (Some(test), None) => (RustCargoCommandKind::CargoTest, test),
540        (None, Some(nextest)) => (RustCargoCommandKind::NextestRun, nextest),
541        (Some(_), Some(_)) => {
542            return Err(RustTestRunnerError::UnsupportedCommand(
543                "the Cargo invocation ambiguously contains both test and nextest run".into(),
544            ));
545        }
546        (None, None) => {
547            return Err(RustTestRunnerError::UnsupportedCommand(
548                "the owned Rust runner currently requires `cargo test` or `cargo nextest run`; cross remains explicitly unsupported"
549                    .into(),
550            ));
551        }
552    };
553    if words[cargo + 1..command]
554        .iter()
555        .any(|word| matches!(word.as_str(), "&&" | "||" | ";" | "|"))
556    {
557        return Err(RustTestRunnerError::UnsupportedCommand(
558            "the Cargo invocation contains a shell boundary before `test`".into(),
559        ));
560    }
561    let command_end = command
562        + if kind == RustCargoCommandKind::NextestRun {
563            1
564        } else {
565            0
566        };
567    let mut arguments = words[cargo + 1..=command_end].to_vec();
568    let mut runner_arguments = Vec::new();
569    let mut after_separator = false;
570    for argument in &words[command_end + 1..] {
571        if argument == "--" && !after_separator {
572            after_separator = true;
573            continue;
574        }
575        if matches!(argument.as_str(), "&&" | "||" | ";" | "|") {
576            return Err(RustTestRunnerError::UnsupportedCommand(
577                "the Cargo test command contains an unsupported shell boundary".into(),
578            ));
579        }
580        if after_separator {
581            runner_arguments.push(argument.clone());
582        } else {
583            arguments.push(argument.clone());
584        }
585    }
586    Ok(CargoTestInvocation {
587        program: words[cargo].clone(),
588        kind,
589        arguments,
590        runner_arguments,
591    })
592}
593
594fn cargo_option_takes_value(argument: &str) -> Option<bool> {
595    let name = argument.split_once('=').map_or(argument, |(name, _)| name);
596    match name {
597        "-p" | "--package" | "--exclude" | "--bin" | "--example" | "--test" | "--bench" | "-F"
598        | "--features" | "-j" | "--jobs" | "--profile" | "--target" | "--target-dir"
599        | "--message-format" | "--color" | "--config" | "-Z" | "--manifest-path" => {
600            Some(!argument.contains('='))
601        }
602        "--no-run"
603        | "--no-fail-fast"
604        | "--future-incompat-report"
605        | "-q"
606        | "--quiet"
607        | "-v"
608        | "--verbose"
609        | "--workspace"
610        | "--all"
611        | "--lib"
612        | "--bins"
613        | "--examples"
614        | "--tests"
615        | "--benches"
616        | "--all-targets"
617        | "--doc"
618        | "--all-features"
619        | "--no-default-features"
620        | "-r"
621        | "--release"
622        | "--timings"
623        | "--ignore-rust-version"
624        | "--locked"
625        | "--offline"
626        | "--frozen" => Some(false),
627        _ if argument.starts_with("-vv") => Some(false),
628        _ if argument.starts_with("-p") && argument.len() > 2 => Some(false),
629        _ if argument.starts_with("-F") && argument.len() > 2 => Some(false),
630        _ if argument.starts_with("-j") && argument.len() > 2 => Some(false),
631        _ => None,
632    }
633}
634
635pub(crate) fn rust_libtest_selection(
636    invocation: &CargoTestInvocation,
637) -> Result<RustLibtestSelection, RustTestRunnerError> {
638    if invocation.kind != RustCargoCommandKind::CargoTest {
639        return Err(RustTestRunnerError::UnsupportedCommand(
640            "libtest selection cannot be reconstructed from a nextest command".into(),
641        ));
642    }
643    let test = invocation
644        .arguments
645        .iter()
646        .position(|argument| argument == "test")
647        .ok_or_else(|| {
648            RustTestRunnerError::UnsupportedCommand(
649                "the expanded Cargo invocation lost its test subcommand".into(),
650            )
651        })?;
652    let mut cargo_filter = None;
653    let mut index = test + 1;
654    while index < invocation.arguments.len() {
655        let argument = &invocation.arguments[index];
656        if argument.starts_with('-') {
657            let takes_value = cargo_option_takes_value(argument).ok_or_else(|| {
658                RustTestRunnerError::UnsupportedCommand(format!(
659                    "the pinned Cargo test contract does not recognize option {argument}"
660                ))
661            })?;
662            if takes_value {
663                index += 1;
664                if index == invocation.arguments.len() {
665                    return Err(RustTestRunnerError::UnsupportedCommand(format!(
666                        "Cargo option {argument} has no value"
667                    )));
668                }
669            }
670        } else if cargo_filter.replace(argument.clone()).is_some() {
671            return Err(RustTestRunnerError::UnsupportedCommand(
672                "Cargo test has more than one pre-separator TESTNAME".into(),
673            ));
674        }
675        index += 1;
676    }
677
678    let mut list_arguments = cargo_filter.into_iter().collect::<Vec<_>>();
679    let mut test_threads = None;
680    let mut index = 0;
681    while index < invocation.runner_arguments.len() {
682        let argument = &invocation.runner_arguments[index];
683        match argument.as_str() {
684            "--ignored" | "--include-ignored" | "--exclude-should-panic" | "--test" | "--bench" => {
685                list_arguments.push(argument.clone());
686            }
687            "--exact" => list_arguments.push(argument.clone()),
688            "--skip" => {
689                let value = invocation.runner_arguments.get(index + 1).ok_or_else(|| {
690                    RustTestRunnerError::UnsupportedCommand(
691                        "libtest --skip has no filter value".into(),
692                    )
693                })?;
694                list_arguments.extend([argument.clone(), value.clone()]);
695                index += 1;
696            }
697            _ if argument.starts_with("--skip=") && argument.len() > "--skip=".len() => {
698                list_arguments.push(argument.clone());
699            }
700            "--test-threads" => {
701                let value = invocation.runner_arguments.get(index + 1).ok_or_else(|| {
702                    RustTestRunnerError::UnsupportedCommand(
703                        "libtest --test-threads has no value".into(),
704                    )
705                })?;
706                let parsed = parse_libtest_threads(value)?;
707                if test_threads.replace(parsed).is_some() {
708                    return Err(RustTestRunnerError::UnsupportedCommand(
709                        "libtest --test-threads was provided more than once".into(),
710                    ));
711                }
712                index += 1;
713            }
714            _ if argument.starts_with("--test-threads=") => {
715                let value = &argument["--test-threads=".len()..];
716                let parsed = parse_libtest_threads(value)?;
717                if test_threads.replace(parsed).is_some() {
718                    return Err(RustTestRunnerError::UnsupportedCommand(
719                        "libtest --test-threads was provided more than once".into(),
720                    ));
721                }
722            }
723            "-Z" => {
724                let value = invocation.runner_arguments.get(index + 1).ok_or_else(|| {
725                    RustTestRunnerError::UnsupportedCommand(
726                        "libtest -Z has no feature value".into(),
727                    )
728                })?;
729                // `exclude-should-panic` and the other unstable selection
730                // flags must be parsed under the same libtest feature gate
731                // during discovery. The user's exact pair is also preserved
732                // unchanged for the real artifact execution.
733                list_arguments.extend([argument.clone(), value.clone()]);
734                index += 1;
735            }
736            _ if argument.starts_with("-Z") && argument.len() > 2 => {
737                list_arguments.push(argument.clone());
738            }
739            "--logfile" | "--color" | "--format" | "--shuffle-seed" => {
740                if invocation.runner_arguments.get(index + 1).is_none() {
741                    return Err(RustTestRunnerError::UnsupportedCommand(format!(
742                        "libtest {argument} has no value"
743                    )));
744                }
745                // Presentation-only values must not leak into the synthetic
746                // terse listing. The original argument and value are passed
747                // byte-for-byte to the one stock artifact execution.
748                index += 1;
749            }
750            _ if ["--logfile=", "--color=", "--format=", "--shuffle-seed="]
751                .iter()
752                .any(|prefix| argument.starts_with(prefix) && argument.len() > prefix.len()) => {}
753            "--force-run-in-process"
754            | "--fail-fast"
755            | "--no-capture"
756            | "--nocapture"
757            | "-q"
758            | "--quiet"
759            | "--show-output"
760            | "--report-time"
761            | "--ensure-time"
762            | "--shuffle" => {
763                // These affect only scheduling, execution or presentation.
764                // They are intentionally absent from discovery and retained
765                // unchanged in the real artifact argv.
766            }
767            "--list" | "-h" | "--help" => {
768                return Err(RustTestRunnerError::UnsupportedCommand(format!(
769                    "libtest {argument} does not execute a test suite; exact non-execution mode support is not implemented"
770                )));
771            }
772            _ if !argument.starts_with('-') => list_arguments.push(argument.clone()),
773            _ => {
774                return Err(RustTestRunnerError::UnsupportedCommand(format!(
775                    "the pinned Rust 1.95 libtest discovery contract does not recognize option {argument}"
776                )));
777            }
778        }
779        index += 1;
780    }
781    Ok(RustLibtestSelection { list_arguments })
782}
783
784fn parse_libtest_threads(value: &str) -> Result<usize, RustTestRunnerError> {
785    match value.parse::<usize>() {
786        Ok(0) => Err(RustTestRunnerError::UnsupportedCommand(
787            "argument for --test-threads must not be 0".into(),
788        )),
789        Ok(value) => Ok(value),
790        Err(error) => Err(RustTestRunnerError::UnsupportedCommand(format!(
791            "argument for --test-threads must be a number > 0 (error: {error})"
792        ))),
793    }
794}
795
796pub(crate) fn rust_cargo_execution_selection(
797    invocation: &CargoTestInvocation,
798) -> Result<RustCargoExecutionSelection, RustTestRunnerError> {
799    if invocation.kind == RustCargoCommandKind::NextestRun {
800        return Ok(RustCargoExecutionSelection {
801            run_libtests: true,
802            run_doctests: false,
803            doctest_arguments: Vec::new(),
804        });
805    }
806    let test = invocation
807        .arguments
808        .iter()
809        .position(|argument| argument == "test")
810        .ok_or_else(|| {
811            RustTestRunnerError::UnsupportedCommand(
812                "the expanded Cargo invocation lost its test subcommand".into(),
813            )
814        })?;
815    let mut doc = false;
816    let mut other_target = false;
817    let mut index = test + 1;
818    while index < invocation.arguments.len() {
819        let argument = &invocation.arguments[index];
820        let name = argument
821            .split_once('=')
822            .map_or(argument.as_str(), |(name, _)| name);
823        match name {
824            "--doc" => doc = true,
825            "--lib" | "--bins" | "--bin" | "--examples" | "--example" | "--tests" | "--test"
826            | "--benches" | "--bench" | "--all-targets" => other_target = true,
827            _ => {}
828        }
829        if argument.starts_with('-') {
830            let takes_value = cargo_option_takes_value(argument).ok_or_else(|| {
831                RustTestRunnerError::UnsupportedCommand(format!(
832                    "the pinned Cargo test contract does not recognize option {argument}"
833                ))
834            })?;
835            if takes_value {
836                index += 1;
837                if index == invocation.arguments.len() {
838                    return Err(RustTestRunnerError::UnsupportedCommand(format!(
839                        "Cargo option {argument} has no value"
840                    )));
841                }
842            }
843        }
844        index += 1;
845    }
846    if doc && other_target {
847        return Err(RustTestRunnerError::UnsupportedCommand(
848            "Cargo --doc cannot be combined with another explicit target selection".into(),
849        ));
850    }
851    let run_doctests = doc || !other_target;
852    let run_libtests = !doc;
853    let mut doctest_arguments = invocation.arguments.clone();
854    if run_doctests && !doc {
855        doctest_arguments.insert(test + 1, "--doc".into());
856    }
857    if !invocation.runner_arguments.is_empty() {
858        doctest_arguments.push("--".into());
859        doctest_arguments.extend(invocation.runner_arguments.iter().cloned());
860    }
861    Ok(RustCargoExecutionSelection {
862        run_libtests,
863        run_doctests,
864        doctest_arguments,
865    })
866}
867
868fn relative_source(root: &Path, path: &Path) -> Result<String, RustTestRunnerError> {
869    let relative = path
870        .strip_prefix(root)
871        .map_err(|_| RustTestRunnerError::UnsafeArtifact(path.display().to_string()))?;
872    if relative.as_os_str().is_empty()
873        || relative
874            .components()
875            .any(|part| !matches!(part, Component::Normal(_)))
876    {
877        return Err(RustTestRunnerError::UnsafeArtifact(
878            path.display().to_string(),
879        ));
880    }
881    Ok(relative.to_string_lossy().replace('\\', "/"))
882}
883
884fn build_test_artifacts(
885    project: &PreparedRustProject,
886    command: &[String],
887) -> Result<Vec<TestArtifact>, RustTestRunnerError> {
888    let mut invocation = cargo_invocation(&project.workspace_root, command)?;
889    invocation
890        .arguments
891        .extend(["--no-run".into(), "--message-format=json".into()]);
892    // The instrumented workspace is ephemeral and its sources are generated,
893    // so the HOST crate's lint policy must not reject them: serde builds with
894    // `#![deny(warnings)]`, and http's `if ({ frame ... })` decision wrapping
895    // trips `unused_parens` into a hard error under it. Capping lints to warn
896    // changes nothing about the user's own `cargo test` runs. The user's
897    // RUSTFLAGS are preserved ahead of the cap.
898    let mut rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
899    if !rustflags.is_empty() {
900        rustflags.push(' ');
901    }
902    rustflags.push_str("--cap-lints=warn");
903    let output = Command::new(&invocation.program)
904        .args(invocation.arguments)
905        .current_dir(&project.workspace_root)
906        .env("CARGO_TARGET_DIR", &project.target_directory)
907        .env("RUSTFLAGS", rustflags)
908        .output()
909        .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
910    if !output.status.success() {
911        return Err(RustTestRunnerError::CargoFailed(
912            String::from_utf8_lossy(&output.stderr).trim().to_owned(),
913        ));
914    }
915    let canonical_target = fs::canonicalize(&project.target_directory)
916        .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
917    let mut artifacts = Vec::new();
918    for line in output
919        .stdout
920        .split(|byte| *byte == b'\n')
921        .filter(|line| !line.is_empty())
922    {
923        let message: CargoMessage = serde_json::from_slice(line)
924            .map_err(|error| RustTestRunnerError::CargoJson(error.to_string()))?;
925        if message.reason != "compiler-artifact"
926            || !message.profile.as_ref().is_some_and(|profile| profile.test)
927        {
928            continue;
929        }
930        let (Some(executable), Some(target)) = (message.executable, message.target) else {
931            continue;
932        };
933        let executable = fs::canonicalize(&executable)
934            .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
935        if !executable.starts_with(&canonical_target)
936            || !fs::metadata(&executable).is_ok_and(|metadata| metadata.is_file())
937        {
938            return Err(RustTestRunnerError::UnsafeArtifact(
939                executable.display().to_string(),
940            ));
941        }
942        let source = fs::canonicalize(target.src_path)
943            .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
944        artifacts.push(TestArtifact {
945            executable,
946            name: target.name,
947            kind: if target.kind.iter().any(|kind| kind == "test") {
948                "integration".into()
949            } else {
950                "unit".into()
951            },
952            source: relative_source(&project.workspace_root, &source)?,
953        });
954    }
955    artifacts.sort_by(|left, right| left.executable.cmp(&right.executable));
956    artifacts.dedup_by(|left, right| left.executable == right.executable);
957    if artifacts.is_empty() {
958        return Err(RustTestRunnerError::CargoJson(
959            "Cargo emitted no libtest artifacts".into(),
960        ));
961    }
962    Ok(artifacts)
963}
964
965fn list_tests(artifact: &TestArtifact) -> Result<Vec<String>, RustTestRunnerError> {
966    let output = Command::new(&artifact.executable)
967        .args(["--list", "--format", "terse"])
968        .output()
969        .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
970    if !output.status.success() {
971        // A libtest binary that dies on a signal writes nothing to stderr, so
972        // reporting stderr alone produces an empty, undiagnosable message.
973        // Name the artifact and how it ended, and fall back to stdout.
974        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
975        let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned();
976        let detail = if !stderr.is_empty() {
977            stderr
978        } else if !stdout.is_empty() {
979            format!("no stderr; stdout was {stdout}")
980        } else {
981            "no output on either stream".to_owned()
982        };
983        return Err(RustTestRunnerError::ListFailed(format!(
984            "{} exited with {} when asked to --list: {detail}",
985            artifact.executable.display(),
986            output.status
987        )));
988    }
989    let mut tests = String::from_utf8_lossy(&output.stdout)
990        .lines()
991        .filter_map(|line| line.strip_suffix(": test"))
992        .map(str::to_owned)
993        .collect::<Vec<_>>();
994    tests.sort();
995    tests.dedup();
996    Ok(tests)
997}
998
999fn snapshot(
1000    manifest: &CoverageManifest,
1001    directory: &Path,
1002) -> Result<RuntimeSnapshot, RustTestRunnerError> {
1003    let points = manifest
1004        .points
1005        .iter()
1006        .map(|point| point.id.as_str())
1007        .collect::<BTreeSet<_>>();
1008    let alternatives = manifest
1009        .branches
1010        .iter()
1011        .flat_map(|branch| {
1012            branch
1013                .alternatives
1014                .iter()
1015                .map(|alternative| alternative.id.as_str())
1016        })
1017        .collect::<BTreeSet<_>>();
1018    let decisions = manifest
1019        .decisions
1020        .iter()
1021        .map(|decision| (decision.id.as_str(), decision))
1022        .collect::<BTreeMap<_, _>>();
1023    let mut hits = BTreeSet::new();
1024    let mut vectors = BTreeMap::<String, BTreeSet<(Vec<Option<bool>>, bool)>>::new();
1025    for observations in read_rust_probe_directory(directory)
1026        .map_err(|error| RustTestRunnerError::Probe(error.to_string()))?
1027        .into_values()
1028    {
1029        for observation in observations {
1030            match observation {
1031                RustProbeObservation::Hit { id } => {
1032                    if !points.contains(id.as_str()) && !alternatives.contains(id.as_str()) {
1033                        return Err(RustTestRunnerError::UnknownProbe(id));
1034                    }
1035                    hits.insert(id);
1036                }
1037                RustProbeObservation::Decision {
1038                    id,
1039                    values,
1040                    outcome,
1041                } => {
1042                    let Some(meta) = decisions.get(id.as_str()) else {
1043                        return Err(RustTestRunnerError::UnknownProbe(id));
1044                    };
1045                    if values.len() != meta.conditions.len() {
1046                        return Err(RustTestRunnerError::InvalidVector {
1047                            id,
1048                            expected: meta.conditions.len(),
1049                            actual: values.len(),
1050                        });
1051                    }
1052                    hits.insert(format!(
1053                        "{}:outcome:{}",
1054                        meta.id,
1055                        if outcome { "true" } else { "false" }
1056                    ));
1057                    vectors
1058                        .entry(meta.id.clone())
1059                        .or_default()
1060                        .insert((values, outcome));
1061                }
1062            }
1063        }
1064    }
1065    let mut decision_snapshots = Vec::new();
1066    for (id, observed) in vectors {
1067        let meta: DecisionMeta = (*decisions[id.as_str()]).clone();
1068        decision_snapshots.push(DecisionSnapshot {
1069            meta,
1070            vectors: observed
1071                .into_iter()
1072                .map(|(values, outcome)| McdcVector { values, outcome })
1073                .collect(),
1074        });
1075    }
1076    Ok(RuntimeSnapshot {
1077        decisions: decision_snapshots,
1078        hits: hits.into_iter().collect(),
1079        events: Vec::new(),
1080    })
1081}
1082
1083fn rust_coverage_model() -> CoverageModelDeclaration {
1084    CoverageModelDeclaration {
1085        language: "rust".into(),
1086        variant: "rust-owned-probes-v1".into(),
1087        name: "supercov-rust-owned-v1".into(),
1088        completeness_meaning: "Every semantics-proven Rust obligation in the owned source denominator was observed; explicit manifest limitations identify unmeasured Rust surfaces.".into(),
1089        measured: vec![
1090            "owned Rust statements and function entries".into(),
1091            "owned atomic condition vectors and decision outcomes".into(),
1092            "exact process-per-libtest attribution".into(),
1093        ],
1094        not_measured: vec![
1095            "macro-expanded and generated Rust code".into(),
1096            "const-evaluated code and unsupported structural branch probes".into(),
1097            "causal linkage to individual actions or passing assertions".into(),
1098            "all input values, semantic partitions, paths, or concurrency interleavings".into(),
1099            "mutation score or assertion fault-detection strength".into(),
1100        ],
1101    }
1102}
1103
1104pub fn run_prepared_rust_tests(
1105    project: &PreparedRustProject,
1106    command: &[String],
1107    run_id: &str,
1108    generated_at: &str,
1109    diagnostics: &mut dyn Write,
1110) -> Result<RustFrontendRun, RustTestRunnerError> {
1111    let build_started = Instant::now();
1112    let artifacts = build_test_artifacts(project, command)?;
1113    let build_ms = build_started.elapsed().as_secs_f64() * 1000.0;
1114    let evidence_root = project
1115        .workspace_root
1116        .join(".supercov/rust-evidence")
1117        .join(run_id);
1118    fs::create_dir_all(&evidence_root)
1119        .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1120    let mut results = Vec::new();
1121    let mut overall_exit = 0;
1122    let execution_started = Instant::now();
1123    let mut tasks = Vec::new();
1124    for (artifact_index, artifact) in artifacts.iter().enumerate() {
1125        let tests = list_tests(artifact)?;
1126        let contexts = preflight_rust_test_contexts(tests.clone())
1127            .map_err(|error| RustTestRunnerError::Context(error.to_string()))?;
1128        for (test_index, test) in tests.into_iter().enumerate() {
1129            let directory = evidence_root.join(format!("{artifact_index:04}-{test_index:08}"));
1130            fs::create_dir(&directory)
1131                .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1132            tasks.push(ProcessTask {
1133                ordinal: tasks.len(),
1134                artifact_index,
1135                test_index,
1136                artifact: artifact.clone(),
1137                context_id: contexts[&test],
1138                test,
1139                directory,
1140            });
1141        }
1142    }
1143    let workers = std::thread::available_parallelism()
1144        .map(usize::from)
1145        .unwrap_or(1)
1146        .min(tasks.len().max(1));
1147    let next = AtomicUsize::new(0);
1148    let outcomes = Mutex::new(Vec::<Result<ProcessOutcome, String>>::with_capacity(
1149        tasks.len(),
1150    ));
1151    std::thread::scope(|scope| {
1152        for _ in 0..workers {
1153            scope.spawn(|| {
1154                loop {
1155                    let index = next.fetch_add(1, Ordering::Relaxed);
1156                    let Some(task) = tasks.get(index) else { break };
1157                    let result = Command::new(&task.artifact.executable)
1158                        // No --nocapture: libtest's in-memory capture is what
1159                        // plain `cargo test` gives users, and it re-emits a
1160                        // failing test's output, which `.output()` still
1161                        // receives. Streaming instead turns every print in a
1162                        // hot loop into an unbuffered stderr syscall: bytes'
1163                        // advance_bytes_mut_remaining_capacity prints per
1164                        // iteration, and --nocapture alone cost 6.9s of its
1165                        // 14.4s (baseline 8.0s streamed vs 1.0s captured).
1166                        .args(["--exact", &task.test])
1167                        .current_dir(&project.workspace_root)
1168                        .env("SUPERCOV_RUST_EVIDENCE_DIR", &task.directory)
1169                        .env(
1170                            crate::rust_probe_transport::RUST_CONTEXT_ENV,
1171                            format!("{:016x}", task.context_id),
1172                        )
1173                        .output()
1174                        .map(|output| ProcessOutcome {
1175                            task: ProcessTask {
1176                                ordinal: task.ordinal,
1177                                artifact_index: task.artifact_index,
1178                                test_index: task.test_index,
1179                                artifact: task.artifact.clone(),
1180                                test: task.test.clone(),
1181                                context_id: task.context_id,
1182                                directory: task.directory.clone(),
1183                            },
1184                            output,
1185                        })
1186                        .map_err(|error| error.to_string());
1187                    outcomes
1188                        .lock()
1189                        .expect("Rust test result lock poisoned")
1190                        .push(result);
1191                }
1192            });
1193        }
1194    });
1195    let mut outcomes = outcomes
1196        .into_inner()
1197        .map_err(|_| RustTestRunnerError::Io("Rust test result lock poisoned".into()))?
1198        .into_iter()
1199        .map(|result| result.map_err(RustTestRunnerError::Launch))
1200        .collect::<Result<Vec<_>, _>>()?;
1201    outcomes.sort_by_key(|outcome| outcome.task.ordinal);
1202    for outcome in outcomes {
1203        let ProcessTask {
1204            artifact_index,
1205            test_index,
1206            artifact,
1207            test,
1208            directory,
1209            ..
1210        } = outcome.task;
1211        // Target names are not workspace-unique: two packages may both
1212        // expose `lib` or the same integration-test target. Source path +
1213        // libtest name is stable and unique within the frozen workspace.
1214        let test_id = format!("{}::{test}", artifact.source);
1215        let worker_id = format!("artifact-{artifact_index:04}");
1216        let attempt_id = format!("{run_id}:{artifact_index:04}:{test_index:08}");
1217        let output = outcome.output;
1218        let exit = output.status.code().unwrap_or(1);
1219        let stdout = String::from_utf8_lossy(&output.stdout);
1220        let skipped =
1221            exit == 0 && (stdout.contains("running 0 tests") || stdout.contains("; 1 ignored;"));
1222        if exit != 0 {
1223            writeln!(diagnostics, "[supercov] Rust test failed: {test_id}")
1224                .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1225            diagnostics
1226                .write_all(&output.stdout)
1227                .and_then(|_| diagnostics.write_all(&output.stderr))
1228                .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1229        }
1230        if exit != 0 {
1231            overall_exit = exit;
1232        }
1233        results.push(RawTestResult {
1234            test_id: Some(test_id.clone()),
1235            scope: Some(ExecutionScope {
1236                version: 1,
1237                run_id: run_id.into(),
1238                worker_id,
1239                test_id: test_id.clone(),
1240                test_key: format!("{}::{test}", artifact.source),
1241                retry: 0,
1242                attempt_id,
1243            }),
1244            test: test_id,
1245            test_file: Some(artifact.source.clone()),
1246            title: Some(test),
1247            retry: Some(0),
1248            status: Some(
1249                if exit != 0 {
1250                    "failed"
1251                } else if skipped {
1252                    "skipped"
1253                } else {
1254                    "passed"
1255                }
1256                .into(),
1257            ),
1258            expected_status: Some("passed".into()),
1259            flaky: false,
1260            provenance: TestProvenance {
1261                runner: "rust-libtest".into(),
1262                kind: artifact.kind,
1263                project: Some(artifact.name),
1264                source: "supercov-owned-process-per-test".into(),
1265            },
1266            role: "test".into(),
1267            phases: Vec::new(),
1268            runtime: vec![snapshot(&project.manifest, &directory)?],
1269            browser: Vec::new(),
1270            server: Vec::new(),
1271        });
1272    }
1273    let structural_limitations = project
1274        .manifest
1275        .limitations
1276        .iter()
1277        .filter_map(|item| {
1278            item.get("id")
1279                .and_then(|value| value.as_str())
1280                .map(str::to_owned)
1281        })
1282        .collect();
1283    Ok(RustFrontendRun {
1284        declaration: FrontendRunDeclaration {
1285            protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
1286            frontend_id: "rust".into(),
1287            frontend_version: "rust-owned-v1".into(),
1288            language: "rust".into(),
1289            structural_source: StructuralSource::OwnedProbes,
1290            runners: vec![FrontendRunnerDeclaration {
1291                runner: "rust-libtest".into(),
1292                execution_model: ExecutionModel::ProcessPerTest,
1293                attribution: FrontendAttribution {
1294                    run: AttributionPrecision::Exact,
1295                    worker: AttributionPrecision::Exact,
1296                    test: AttributionPrecision::Exact,
1297                    retry: AttributionPrecision::Exact,
1298                    phase: AttributionPrecision::Exact,
1299                    action: AttributionPrecision::Unavailable,
1300                    assertion: AttributionPrecision::Unavailable,
1301                },
1302                limitations: vec![
1303                    FrontendLimitation {
1304                        id: "rust-action-linkage-unavailable".into(),
1305                        scopes: vec![FrontendLimitationScope::Action],
1306                        reason: "Rust test frameworks expose no general action lifecycle".into(),
1307                    },
1308                    FrontendLimitation {
1309                        id: "rust-assertion-linkage-unavailable".into(),
1310                        scopes: vec![FrontendLimitationScope::Assertion],
1311                        reason: "assertion macros do not expose a stable per-assertion success lifecycle".into(),
1312                    },
1313                ],
1314            }],
1315            structural_limitations,
1316        },
1317        request: CoverageReportRequest {
1318            run_id: run_id.into(),
1319            manifest: project.manifest.clone(),
1320            raw_results: results,
1321            generated_at: generated_at.into(),
1322            coverage_model: Some(rust_coverage_model()),
1323            integrity: None,
1324            test_exit_code: ExitCodeInput::Present(Some(overall_exit)),
1325        },
1326        exit_code: overall_exit,
1327        artifacts: artifacts.len(),
1328        artifact_files: artifacts
1329            .iter()
1330            .map(|artifact| artifact.executable.clone())
1331            .collect(),
1332        build_ms,
1333        execution_ms: execution_started.elapsed().as_secs_f64() * 1000.0,
1334    })
1335}
1336
1337#[cfg(test)]
1338mod tests {
1339    use std::time::{SystemTime, UNIX_EPOCH};
1340
1341    use super::*;
1342    use crate::{
1343        coverage_report::{ArchiveReportRequest, analyze_coverage_archive},
1344        evidence_archive::write_archive,
1345        frontend_protocol::validate_frontend_report_request,
1346        rust_project::prepare_rust_project,
1347    };
1348
1349    #[test]
1350    fn cargo_and_libtest_selection_is_preserved_without_presentation_guessing() {
1351        let root = Path::new(".");
1352        let invocation = cargo_invocation(
1353            root,
1354            &[
1355                "cargo".into(),
1356                "test".into(),
1357                "-p".into(),
1358                "fixture".into(),
1359                "authored".into(),
1360                "--".into(),
1361                "generated".into(),
1362                "--skip".into(),
1363                "slow".into(),
1364                "--include-ignored".into(),
1365            ],
1366        )
1367        .unwrap();
1368        assert_eq!(invocation.arguments, ["test", "-p", "fixture", "authored"]);
1369        assert_eq!(
1370            invocation.runner_arguments,
1371            ["generated", "--skip", "slow", "--include-ignored"]
1372        );
1373        let selection = rust_libtest_selection(&invocation).unwrap();
1374        assert_eq!(
1375            selection.list_arguments,
1376            [
1377                "authored",
1378                "generated",
1379                "--skip",
1380                "slow",
1381                "--include-ignored"
1382            ]
1383        );
1384    }
1385
1386    #[test]
1387    fn direct_cargo_argv_preserves_toml_quotes_inside_config_values() {
1388        let config = "target.host.runner=[\"runner with spaces\",\"--fixed\"]";
1389        let invocation = cargo_invocation(
1390            Path::new("."),
1391            &[
1392                "cargo".into(),
1393                "test".into(),
1394                "--config".into(),
1395                config.into(),
1396            ],
1397        )
1398        .unwrap();
1399        assert_eq!(invocation.arguments, ["test", "--config", config]);
1400    }
1401
1402    #[test]
1403    fn nextest_run_is_detected_without_reclassifying_its_filters_or_retries() {
1404        let invocation = cargo_invocation(
1405            Path::new("."),
1406            &[
1407                "cargo".into(),
1408                "+1.95.0".into(),
1409                "nextest".into(),
1410                "run".into(),
1411                "--retries".into(),
1412                "2".into(),
1413                "-E".into(),
1414                "test(/flaky/)".into(),
1415                "--".into(),
1416                "--nocapture".into(),
1417            ],
1418        )
1419        .unwrap();
1420        assert_eq!(invocation.kind, RustCargoCommandKind::NextestRun);
1421        assert_eq!(
1422            invocation.arguments,
1423            [
1424                "+1.95.0",
1425                "nextest",
1426                "run",
1427                "--retries",
1428                "2",
1429                "-E",
1430                "test(/flaky/)",
1431            ]
1432        );
1433        assert_eq!(invocation.runner_arguments, ["--nocapture"]);
1434        let execution = rust_cargo_execution_selection(&invocation).unwrap();
1435        assert!(execution.run_libtests);
1436        assert!(!execution.run_doctests);
1437        assert!(execution.doctest_arguments.is_empty());
1438        assert!(rust_libtest_selection(&invocation).is_err());
1439        assert_eq!(
1440            nextest_list_invocation(&invocation).unwrap(),
1441            NextestListInvocation {
1442                arguments: vec![
1443                    "+1.95.0".into(),
1444                    "nextest".into(),
1445                    "list".into(),
1446                    "-E".into(),
1447                    "test(/flaky/)".into(),
1448                    "--message-format".into(),
1449                    "json".into(),
1450                ],
1451                runner_arguments: vec!["--nocapture".into()],
1452            }
1453        );
1454    }
1455
1456    #[test]
1457    fn nextest_list_projection_preserves_selection_and_rejects_external_state() {
1458        let invocation = CargoTestInvocation {
1459            program: "cargo".into(),
1460            kind: RustCargoCommandKind::NextestRun,
1461            arguments: vec![
1462                "nextest".into(),
1463                "run".into(),
1464                "--package=fixture".into(),
1465                "--partition".into(),
1466                "hash:1/2".into(),
1467                "--test-threads=8".into(),
1468                "--failure-output".into(),
1469                "final".into(),
1470                "name".into(),
1471            ],
1472            runner_arguments: vec!["--exact".into(), "full::name".into()],
1473        };
1474        assert_eq!(
1475            nextest_list_invocation(&invocation).unwrap(),
1476            NextestListInvocation {
1477                arguments: vec![
1478                    "nextest".into(),
1479                    "list".into(),
1480                    "--package=fixture".into(),
1481                    "--partition".into(),
1482                    "hash:1/2".into(),
1483                    "name".into(),
1484                    "--message-format".into(),
1485                    "json".into(),
1486                ],
1487                runner_arguments: vec!["--exact".into(), "full::name".into()],
1488            }
1489        );
1490
1491        let mut rerun = invocation;
1492        rerun.arguments.extend(["--rerun".into(), "latest".into()]);
1493        assert!(
1494            nextest_list_invocation(&rerun)
1495                .unwrap_err()
1496                .to_string()
1497                .contains("cannot yet be assigned exact selected-test identity")
1498        );
1499    }
1500
1501    #[test]
1502    fn nextest_list_projection_preserves_post_separator_libtest_selection() {
1503        let invocation = cargo_invocation(
1504            Path::new("."),
1505            &[
1506                "cargo".into(),
1507                "nextest".into(),
1508                "run".into(),
1509                "--timings".into(),
1510                "-vv".into(),
1511                "--".into(),
1512                "--include-ignored".into(),
1513                "--skip".into(),
1514                "slow".into(),
1515                "--exact".into(),
1516                "tests::selected".into(),
1517            ],
1518        )
1519        .unwrap();
1520        assert_eq!(
1521            nextest_list_invocation(&invocation).unwrap(),
1522            NextestListInvocation {
1523                arguments: vec![
1524                    "nextest".to_owned(),
1525                    "list".to_owned(),
1526                    "--timings".to_owned(),
1527                    "-vv".to_owned(),
1528                    "--message-format".to_owned(),
1529                    "json".to_owned(),
1530                ],
1531                runner_arguments: vec![
1532                    "--include-ignored".to_owned(),
1533                    "--skip".to_owned(),
1534                    "slow".to_owned(),
1535                    "--exact".to_owned(),
1536                    "tests::selected".to_owned(),
1537                ],
1538            }
1539        );
1540    }
1541
1542    #[test]
1543    fn nextest_version_handshake_preserves_the_cargo_toolchain_selector() {
1544        let invocation = CargoTestInvocation {
1545            program: "cargo".into(),
1546            kind: RustCargoCommandKind::NextestRun,
1547            arguments: vec![
1548                "+1.95.0".into(),
1549                "nextest".into(),
1550                "run".into(),
1551                "-p".into(),
1552                "fixture".into(),
1553            ],
1554            runner_arguments: Vec::new(),
1555        };
1556        assert_eq!(
1557            nextest_version_arguments(&invocation).unwrap(),
1558            ["+1.95.0", "nextest", "--version"]
1559        );
1560    }
1561
1562    #[test]
1563    fn stock_libtest_presentation_and_scheduling_options_do_not_change_discovery() {
1564        let invocation = CargoTestInvocation {
1565            program: "cargo".into(),
1566            kind: RustCargoCommandKind::CargoTest,
1567            arguments: vec!["test".into(), "cargo-filter".into()],
1568            runner_arguments: [
1569                "runner-filter",
1570                "--nocapture",
1571                "--show-output",
1572                "--format=json",
1573                "--color",
1574                "never",
1575                "--test-threads=4",
1576                "--fail-fast",
1577                "--shuffle-seed",
1578                "17",
1579                "-Zunstable-options",
1580                "--exclude-should-panic",
1581            ]
1582            .into_iter()
1583            .map(str::to_owned)
1584            .collect(),
1585        };
1586        let selection = rust_libtest_selection(&invocation).unwrap();
1587        assert_eq!(
1588            selection.list_arguments,
1589            [
1590                "cargo-filter",
1591                "runner-filter",
1592                "-Zunstable-options",
1593                "--exclude-should-panic"
1594            ]
1595        );
1596    }
1597
1598    #[test]
1599    fn cargo_test_options_are_not_mistaken_for_the_test_name_filter() {
1600        let invocation = CargoTestInvocation {
1601            program: "cargo".into(),
1602            kind: RustCargoCommandKind::CargoTest,
1603            arguments: vec![
1604                "test".into(),
1605                "--manifest-path".into(),
1606                "nested/Cargo.toml".into(),
1607                "--features=one,two".into(),
1608                "needle".into(),
1609            ],
1610            runner_arguments: vec!["--ignored".into(), "other".into()],
1611        };
1612        let selection = rust_libtest_selection(&invocation).unwrap();
1613        assert_eq!(selection.list_arguments, ["needle", "--ignored", "other"]);
1614    }
1615
1616    #[test]
1617    fn libtest_thread_count_is_preserved_as_runner_scheduling() {
1618        for arguments in [vec!["--test-threads", "1"], vec!["--test-threads=8"]] {
1619            let invocation = CargoTestInvocation {
1620                program: "cargo".into(),
1621                kind: RustCargoCommandKind::CargoTest,
1622                arguments: vec!["test".into()],
1623                runner_arguments: arguments.into_iter().map(str::to_owned).collect(),
1624            };
1625            let selection = rust_libtest_selection(&invocation).unwrap();
1626            assert!(selection.list_arguments.is_empty());
1627        }
1628    }
1629
1630    #[test]
1631    fn invalid_or_duplicate_libtest_thread_counts_fail_closed() {
1632        for arguments in [
1633            vec!["--test-threads"],
1634            vec!["--test-threads=0"],
1635            vec!["--test-threads=abc"],
1636            vec!["--test-threads", "1", "--test-threads=2"],
1637        ] {
1638            let invocation = CargoTestInvocation {
1639                program: "cargo".into(),
1640                kind: RustCargoCommandKind::CargoTest,
1641                arguments: vec!["test".into()],
1642                runner_arguments: arguments.into_iter().map(str::to_owned).collect(),
1643            };
1644            assert!(rust_libtest_selection(&invocation).is_err());
1645        }
1646    }
1647
1648    #[test]
1649    fn cargo_target_selection_reproduces_when_cargo_runs_doctests() {
1650        let invocation = CargoTestInvocation {
1651            program: "cargo".into(),
1652            kind: RustCargoCommandKind::CargoTest,
1653            arguments: vec![
1654                "test".into(),
1655                "-p".into(),
1656                "fixture".into(),
1657                "needle".into(),
1658            ],
1659            runner_arguments: vec!["--include-ignored".into()],
1660        };
1661        let selection = rust_cargo_execution_selection(&invocation).unwrap();
1662        assert!(selection.run_libtests);
1663        assert!(selection.run_doctests);
1664        assert_eq!(
1665            selection.doctest_arguments,
1666            [
1667                "test",
1668                "--doc",
1669                "-p",
1670                "fixture",
1671                "needle",
1672                "--",
1673                "--include-ignored"
1674            ]
1675        );
1676
1677        let mut explicit_doc = invocation.clone();
1678        explicit_doc.arguments.insert(1, "--doc".into());
1679        let selection = rust_cargo_execution_selection(&explicit_doc).unwrap();
1680        assert!(!selection.run_libtests);
1681        assert!(selection.run_doctests);
1682
1683        for target in ["--lib", "--tests", "--all-targets", "--example=demo"] {
1684            let mut selected = invocation.clone();
1685            selected.arguments.insert(1, target.into());
1686            let selection = rust_cargo_execution_selection(&selected).unwrap();
1687            assert!(selection.run_libtests);
1688            assert!(!selection.run_doctests);
1689        }
1690    }
1691
1692    #[test]
1693    fn cargo_libtest_runs_produce_queryable_owned_evidence() {
1694        let nonce = SystemTime::now()
1695            .duration_since(UNIX_EPOCH)
1696            .unwrap()
1697            .as_nanos();
1698        let root = std::env::temp_dir().join(format!(
1699            "supercov-rust-runner-{}-{nonce}",
1700            std::process::id()
1701        ));
1702        fs::create_dir_all(root.join("src")).unwrap();
1703        fs::write(
1704            root.join("Cargo.toml"),
1705            "[package]\nname='fixture'\nversion='0.0.0'\nedition='2024'\n",
1706        )
1707        .unwrap();
1708        fs::write(
1709            root.join("src/lib.rs"),
1710            r#"
1711pub fn choose(left: bool, right: bool) -> i32 {
1712    if left && right { 1 } else { 0 }
1713}
1714#[cfg(test)]
1715mod tests {
1716    #[test] fn false_path() { assert_eq!(super::choose(false, true), 0); }
1717    #[test] fn true_path() { assert_eq!(super::choose(true, true), 1); }
1718    #[test] #[ignore] fn ignored_path() { unreachable!(); }
1719}
1720"#,
1721        )
1722        .unwrap();
1723        let project = prepare_rust_project(&root).unwrap();
1724        let run = run_prepared_rust_tests(
1725            &project,
1726            &["cargo".into(), "test".into()],
1727            "rust-fixture",
1728            "2026-08-26T00:00:00.000Z",
1729            &mut Vec::new(),
1730        )
1731        .unwrap();
1732        assert_eq!(run.exit_code, 0);
1733        assert_eq!(run.request.raw_results.len(), 3);
1734        assert_eq!(
1735            run.request
1736                .raw_results
1737                .iter()
1738                .filter_map(|result| result.status.as_deref())
1739                .collect::<Vec<_>>(),
1740            ["passed", "skipped", "passed"]
1741        );
1742        validate_frontend_report_request(&run.declaration, &run.request).unwrap();
1743        let archive = root.join("evidence.raw.gz");
1744        write_archive(run.archive_entries().unwrap(), &archive).unwrap();
1745        let report = analyze_coverage_archive(&ArchiveReportRequest {
1746            archive_path: archive,
1747            run_id: "rust-fixture".into(),
1748            generated_at: "2026-08-26T00:00:00.000Z".into(),
1749            integrity: None,
1750            test_exit_code: ExitCodeInput::Present(Some(0)),
1751        })
1752        .unwrap();
1753        assert_eq!(report.view.tests.len(), 3);
1754        assert!(report.view.summary.lines.covered > 0);
1755        assert!(report.view.summary.decisions > 0);
1756        fs::remove_dir_all(root).unwrap();
1757    }
1758}