1use 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
438pub(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 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 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 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 }
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 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 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 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 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
1121pub(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
1138pub(crate) fn libtest_skipped(exit: i32, stdout: &str) -> bool {
1141 exit == 0 && (stdout.contains("running 0 tests") || stdout.contains("; 1 ignored;"))
1142}
1143
1144fn 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
1184fn 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 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 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 .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 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 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 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 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 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 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 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}