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