1use 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
437pub(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 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 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 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 }
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 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 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 .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 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}