1use std::{
12 collections::{BTreeMap, BTreeSet},
13 ffi::OsString,
14 fs,
15 io::Write,
16 path::{Component, Path, PathBuf},
17 process::{Command, Output},
18 sync::{
19 Mutex,
20 atomic::{AtomicUsize, Ordering},
21 },
22 time::Instant,
23};
24
25use serde::Deserialize;
26use supercov_contracts::{
27 AttributionPrecision, ExecutionModel, FrontendAttribution, FrontendLimitation,
28 FrontendLimitationScope, FrontendRunDeclaration, FrontendRunnerDeclaration,
29 LANGUAGE_FRONTEND_PROTOCOL_VERSION, StructuralSource,
30};
31
32use crate::{
33 coverage_analysis::McdcVector,
34 coverage_report::{
35 CoverageManifest, CoverageModelDeclaration, CoveragePhase, CoverageReportRequest,
36 DecisionMeta, DecisionSnapshot, ExecutionScope, ExitCodeInput, PersistedCoverageModel,
37 RawTestResult, RuntimeEvent, RuntimeSnapshot, TestProvenance,
38 },
39 evidence_archive::EvidenceArchiveEntry,
40 rust_project::PreparedRustProject,
41 rust_runtime::{RustProbeObservation, read_rust_probe_directory},
42 rust_test_context::preflight_rust_test_contexts,
43};
44
45#[derive(Debug, Clone, PartialEq)]
46pub struct RustFrontendRun {
47 pub declaration: FrontendRunDeclaration,
48 pub request: CoverageReportRequest,
49 pub exit_code: i32,
50 pub artifacts: usize,
51 pub artifact_files: Vec<PathBuf>,
52 pub build_ms: f64,
53 pub execution_ms: f64,
54}
55
56impl RustFrontendRun {
57 pub fn archive_entries(&self) -> Result<Vec<EvidenceArchiveEntry>, serde_json::Error> {
58 let model = PersistedCoverageModel::from_declaration(
59 self.request
60 .coverage_model
61 .as_ref()
62 .expect("Rust frontend always declares a coverage model"),
63 )
64 .expect("Rust coverage model is contract-valid");
65 let mut entries = vec![
66 EvidenceArchiveEntry {
67 path: "coverage-model.json".into(),
68 contents: serde_json::to_vec(&model)?,
69 },
70 EvidenceArchiveEntry {
71 path: "frontend.json".into(),
72 contents: serde_json::to_vec(&self.declaration)?,
73 },
74 EvidenceArchiveEntry {
75 path: "manifest.json".into(),
76 contents: serde_json::to_vec(&self.request.manifest)?,
77 },
78 ];
79 for (index, result) in self.request.raw_results.iter().enumerate() {
80 entries.push(EvidenceArchiveEntry {
81 path: format!("results/{index:08}/mcdc.json"),
82 contents: serde_json::to_vec(result)?,
83 });
84 }
85 Ok(entries)
86 }
87}
88
89#[derive(Debug)]
90pub enum RustTestRunnerError {
91 UnsupportedCommand(String),
92 Launch(String),
93 CargoFailed(String),
94 CargoJson(String),
95 UnsafeArtifact(String),
96 ListFailed(String),
97 Probe(String),
98 Context(String),
99 UnknownProbe(String),
100 InvalidVector {
101 id: String,
102 expected: usize,
103 actual: usize,
104 },
105 Json(serde_json::Error),
106 Io(String),
107}
108
109impl std::fmt::Display for RustTestRunnerError {
110 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 match self {
112 Self::UnsupportedCommand(reason) => formatter.write_str(reason),
113 Self::Launch(reason) => {
114 write!(formatter, "could not launch Rust test process: {reason}")
115 }
116 Self::CargoFailed(reason) => write!(formatter, "Cargo test build failed: {reason}"),
117 Self::CargoJson(reason) => write!(formatter, "invalid Cargo JSON output: {reason}"),
118 Self::UnsafeArtifact(path) => {
119 write!(formatter, "Cargo emitted an unsafe test artifact: {path}")
120 }
121 Self::ListFailed(reason) => {
122 write!(formatter, "could not enumerate Rust tests: {reason}")
123 }
124 Self::Probe(reason) => write!(formatter, "invalid Rust probe evidence: {reason}"),
125 Self::Context(reason) => write!(formatter, "invalid Rust test context: {reason}"),
126 Self::UnknownProbe(id) => write!(
127 formatter,
128 "Rust runtime emitted an unknown obligation: {id}"
129 ),
130 Self::InvalidVector {
131 id,
132 expected,
133 actual,
134 } => write!(
135 formatter,
136 "Rust decision {id} emitted vector width {actual}; expected {expected}"
137 ),
138 Self::Json(error) => write!(formatter, "could not encode Rust evidence: {error}"),
139 Self::Io(reason) => formatter.write_str(reason),
140 }
141 }
142}
143
144impl std::error::Error for RustTestRunnerError {}
145
146impl From<serde_json::Error> for RustTestRunnerError {
147 fn from(value: serde_json::Error) -> Self {
148 Self::Json(value)
149 }
150}
151
152#[derive(Debug, Deserialize)]
153struct CargoMessage {
154 reason: String,
155 #[serde(default)]
156 target: Option<CargoArtifactTarget>,
157 #[serde(default)]
158 profile: Option<CargoArtifactProfile>,
159 executable: Option<PathBuf>,
160 manifest_path: Option<PathBuf>,
161}
162
163#[derive(Debug, Deserialize)]
164struct CargoArtifactTarget {
165 name: String,
166 kind: Vec<String>,
167 src_path: PathBuf,
168}
169
170#[derive(Debug, Deserialize)]
171struct CargoArtifactProfile {
172 test: bool,
173}
174
175#[derive(Debug, Clone)]
176struct TestArtifact {
177 executable: PathBuf,
178 name: String,
179 kind: String,
180 source: String,
181 package_directory: PathBuf,
184}
185
186#[derive(Debug)]
187struct ProcessTask {
188 ordinal: usize,
189 artifact_index: usize,
190 test_index: usize,
191 artifact: TestArtifact,
192 test: String,
193 context_id: u64,
194 directory: PathBuf,
195}
196
197#[derive(Debug)]
198struct ProcessOutcome {
199 task: ProcessTask,
200 output: Output,
201}
202
203fn shell_words(value: &str) -> Result<Vec<String>, RustTestRunnerError> {
207 let mut words = Vec::new();
208 let mut current = String::new();
209 let mut started = false;
211 let mut quote = None;
212 let mut characters = value.chars().peekable();
213 while let Some(character) = characters.next() {
214 match (quote, character) {
215 (Some('\''), '\'') | (Some('"'), '"') => quote = None,
216 (Some('\''), _) => current.push(character),
217 (Some(_), '\\') => match characters.peek() {
218 Some('\n') => {
220 characters.next();
221 }
222 Some('$' | '`' | '"' | '\\') => {
223 current.extend(characters.next());
224 }
225 Some(_) => current.push('\\'),
226 None => quote = Some('\\'),
227 },
228 (Some(_), _) => current.push(character),
229 (None, '\\') => match characters.next() {
230 Some('\n') => {}
231 Some(escaped) => {
232 current.push(escaped);
233 started = true;
234 }
235 None => quote = Some('\\'),
236 },
237 (None, '\'' | '"') => {
238 quote = Some(character);
239 started = true;
240 }
241 (None, _) if character.is_whitespace() => {
242 if std::mem::take(&mut started) {
243 words.push(std::mem::take(&mut current));
244 }
245 }
246 (None, _) => {
247 current.push(character);
248 started = true;
249 }
250 }
251 }
252 if quote.is_some() {
253 return Err(RustTestRunnerError::UnsupportedCommand(
254 "the expanded Cargo command contains an incomplete quote or escape".into(),
255 ));
256 }
257 if started {
258 words.push(current);
259 }
260 Ok(words)
261}
262
263fn executable_name(value: &str) -> &str {
264 Path::new(value)
265 .file_name()
266 .and_then(|name| name.to_str())
267 .unwrap_or(value)
268 .trim_end_matches(".exe")
269 .trim_end_matches(".cmd")
270}
271
272#[derive(Debug, Clone, PartialEq, Eq)]
273pub(crate) struct CargoTestInvocation {
274 pub program: String,
275 pub kind: RustCargoCommandKind,
276 pub arguments: Vec<String>,
277 pub runner_arguments: Vec<String>,
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq)]
281pub(crate) enum RustCargoCommandKind {
282 CargoTest,
283 NextestRun,
284}
285
286impl CargoTestInvocation {
287 pub(crate) fn command_position(&self) -> usize {
291 match self.kind {
292 RustCargoCommandKind::CargoTest => self
293 .arguments
294 .iter()
295 .position(|argument| argument == "test"),
296 RustCargoCommandKind::NextestRun => self
297 .arguments
298 .windows(2)
299 .position(|pair| pair == ["nextest", "run"]),
300 }
301 .expect("cargo_invocation keeps the test subcommand")
302 }
303}
304
305#[derive(Debug, Clone, PartialEq, Eq)]
306pub(crate) struct RustLibtestSelection {
307 pub list_arguments: Vec<String>,
308}
309
310#[derive(Debug, Clone, PartialEq, Eq)]
311pub(crate) struct RustCargoExecutionSelection {
312 pub run_libtests: bool,
313 pub run_doctests: bool,
314 pub doctest_arguments: Vec<String>,
315}
316
317#[derive(Debug, Clone, PartialEq, Eq)]
318pub(crate) struct NextestListInvocation {
319 pub arguments: Vec<String>,
320 pub runner_arguments: Vec<String>,
321}
322
323pub(crate) fn nextest_version_arguments(invocation: &CargoTestInvocation) -> Vec<String> {
324 let command = invocation.command_position();
325 let mut arguments = invocation.arguments[..command].to_vec();
326 arguments.extend(["nextest".into(), "--version".into()]);
327 arguments
328}
329
330fn nextest_run_only_option(argument: &str) -> Option<bool> {
331 let name = argument.split_once('=').map_or(argument, |(name, _)| name);
332 match name {
333 "-j"
334 | "--jobs"
335 | "--test-threads"
336 | "--retries"
337 | "--flaky-result"
338 | "--max-fail"
339 | "--no-tests"
340 | "--failure-output"
341 | "--success-output"
342 | "--status-level"
343 | "--final-status-level"
344 | "--show-progress"
345 | "--max-progress-running"
346 | "--message-format"
347 | "--message-format-version" => Some(!argument.contains('=')),
348 "--fail-fast"
349 | "--ff"
350 | "--no-fail-fast"
351 | "--nff"
352 | "--no-capture"
353 | "--nocapture"
354 | "--no-output-indent"
355 | "--hide-progress-bar"
356 | "--no-input-handler" => Some(false),
357 _ if argument.starts_with("-j") && argument.len() > 2 => Some(false),
358 _ => None,
359 }
360}
361
362fn nextest_unsupported_run_option(argument: &str) -> Option<bool> {
363 let name = argument.split_once('=').map_or(argument, |(name, _)| name);
364 match name {
365 "-R"
366 | "--rerun"
367 | "--debugger"
368 | "--tracer"
369 | "--stress-count"
370 | "--stress-duration"
371 | "--archive-file"
372 | "--archive-format"
373 | "--extract-to"
374 | "--cargo-metadata"
375 | "--workspace-remap"
376 | "--binaries-metadata"
377 | "--target-dir-remap"
378 | "--build-dir-remap" => Some(!argument.contains('=')),
379 "--no-run" | "--extract-overwrite" | "--persist-extract-tempdir" => Some(false),
380 _ => None,
381 }
382}
383
384fn nextest_shared_option(argument: &str) -> Option<bool> {
385 let name = argument.split_once('=').map_or(argument, |(name, _)| name);
386 match name {
387 "--color"
388 | "-p"
389 | "--package"
390 | "--exclude"
391 | "--bin"
392 | "--example"
393 | "--test"
394 | "--bench"
395 | "-F"
396 | "--features"
397 | "--build-jobs"
398 | "--cargo-profile"
399 | "--target"
400 | "--target-dir"
401 | "--cargo-message-format"
402 | "--config"
403 | "--timings"
404 | "-Z"
405 | "--run-ignored"
406 | "--partition"
407 | "--platform-filter"
408 | "-E"
409 | "--filterset"
410 | "--filter-expr"
411 | "--manifest-path"
412 | "--config-file"
413 | "--user-config-file"
414 | "--tool-config-file"
415 | "-P"
416 | "--profile" => Some(!argument.contains('=')),
417 "--no-pager"
418 | "-v"
419 | "--verbose"
420 | "--workspace"
421 | "--all"
422 | "--lib"
423 | "--bins"
424 | "--examples"
425 | "--tests"
426 | "--benches"
427 | "--all-targets"
428 | "--all-features"
429 | "--no-default-features"
430 | "-r"
431 | "--release"
432 | "--unit-graph"
433 | "--frozen"
434 | "--locked"
435 | "--offline"
436 | "--cargo-quiet"
437 | "--cargo-verbose"
438 | "--ignore-rust-version"
439 | "--future-incompat-report"
440 | "--ignore-default-filter"
441 | "--override-version-check" => Some(false),
442 _ if argument.starts_with("-p") && argument.len() > 2 => Some(false),
443 _ if argument.starts_with("-F") && argument.len() > 2 => Some(false),
444 _ if argument.starts_with("-E") && argument.len() > 2 => Some(false),
445 _ if argument.starts_with("-P") && argument.len() > 2 => Some(false),
446 _ if argument.starts_with("-Z") && argument.len() > 2 => Some(false),
447 _ if argument.len() > 2
448 && argument.starts_with('-')
449 && argument[1..].bytes().all(|byte| byte == b'v') =>
450 {
451 Some(false)
452 }
453 _ if argument.starts_with("--timings=") => Some(false),
454 _ => None,
455 }
456}
457
458pub(crate) fn nextest_list_invocation(
464 invocation: &CargoTestInvocation,
465) -> Result<NextestListInvocation, RustTestRunnerError> {
466 let command = invocation.command_position();
467 let mut arguments = invocation.arguments[..command].to_vec();
468 arguments.extend(["nextest".into(), "list".into()]);
469 let mut index = command + 2;
470 while index < invocation.arguments.len() {
471 let argument = &invocation.arguments[index];
472 if argument == "--" {
473 arguments.extend(invocation.arguments[index..].iter().cloned());
474 break;
475 }
476 if let Some(takes_value) = nextest_unsupported_run_option(argument) {
477 if takes_value && invocation.arguments.get(index + 1).is_none() {
478 return Err(RustTestRunnerError::UnsupportedCommand(format!(
479 "nextest option {argument} has no value"
480 )));
481 }
482 return Err(RustTestRunnerError::UnsupportedCommand(format!(
483 "nextest option {argument} cannot yet be assigned exact selected-test identity"
484 )));
485 }
486 if let Some(takes_value) = nextest_run_only_option(argument) {
487 if takes_value {
488 index += 1;
489 if index == invocation.arguments.len() {
490 return Err(RustTestRunnerError::UnsupportedCommand(format!(
491 "nextest option {argument} has no value"
492 )));
493 }
494 }
495 } else if let Some(takes_value) = nextest_shared_option(argument) {
496 arguments.push(argument.clone());
497 if takes_value {
498 index += 1;
499 let value = invocation.arguments.get(index).ok_or_else(|| {
500 RustTestRunnerError::UnsupportedCommand(format!(
501 "nextest option {argument} has no value"
502 ))
503 })?;
504 arguments.push(value.clone());
505 }
506 } else if argument.starts_with('-') {
507 return Err(RustTestRunnerError::UnsupportedCommand(format!(
508 "the pinned nextest run contract does not recognize option {argument}"
509 )));
510 } else {
511 arguments.push(argument.clone());
512 }
513 index += 1;
514 }
515 arguments.extend(["--message-format".into(), "json".into()]);
516 Ok(NextestListInvocation {
517 arguments,
518 runner_arguments: invocation.runner_arguments.clone(),
519 })
520}
521
522pub(crate) fn cargo_invocation(
523 root: &Path,
524 command: &[String],
525) -> Result<CargoTestInvocation, RustTestRunnerError> {
526 let words = if command.iter().any(|word| executable_name(word) == "cargo") {
531 command.to_vec()
532 } else {
533 let expanded = crate::project_discovery::script_command(root, command)
536 .unwrap_or_else(|| crate::project_discovery::expanded_command(root, command));
537 shell_words(&expanded)?
538 };
539 let cargo = words
540 .iter()
541 .position(|word| executable_name(word) == "cargo")
542 .ok_or_else(|| RustTestRunnerError::UnsupportedCommand(
543 "Rust was detected, but the expanded command does not expose a stable Cargo invocation".into(),
544 ))?;
545 let cargo_test = words[cargo + 1..]
546 .iter()
547 .position(|word| word == "test")
548 .map(|position| cargo + 1 + position);
549 let nextest = words[cargo + 1..]
550 .windows(2)
551 .position(|pair| pair == ["nextest", "run"])
552 .map(|position| cargo + 1 + position);
553 let (kind, command) = match (cargo_test, nextest) {
554 (Some(test), None) => (RustCargoCommandKind::CargoTest, test),
555 (None, Some(nextest)) => (RustCargoCommandKind::NextestRun, nextest),
556 (Some(_), Some(_)) => {
557 return Err(RustTestRunnerError::UnsupportedCommand(
558 "the Cargo invocation ambiguously contains both test and nextest run".into(),
559 ));
560 }
561 (None, None) => {
562 return Err(RustTestRunnerError::UnsupportedCommand(
563 "the owned Rust runner currently requires `cargo test` or `cargo nextest run`; cross remains explicitly unsupported"
564 .into(),
565 ));
566 }
567 };
568 if words[cargo + 1..command]
569 .iter()
570 .any(|word| matches!(word.as_str(), "&&" | "||" | ";" | "|"))
571 {
572 return Err(RustTestRunnerError::UnsupportedCommand(
573 "the Cargo invocation contains a shell boundary before `test`".into(),
574 ));
575 }
576 let command_end = command
577 + if kind == RustCargoCommandKind::NextestRun {
578 1
579 } else {
580 0
581 };
582 let mut arguments = words[cargo + 1..=command_end].to_vec();
583 let mut runner_arguments = Vec::new();
584 let mut after_separator = false;
585 for argument in &words[command_end + 1..] {
586 if argument == "--" && !after_separator {
587 after_separator = true;
588 continue;
589 }
590 if matches!(argument.as_str(), "&&" | "||" | ";" | "|") {
591 return Err(RustTestRunnerError::UnsupportedCommand(
592 "the Cargo test command contains an unsupported shell boundary".into(),
593 ));
594 }
595 if after_separator {
596 runner_arguments.push(argument.clone());
597 } else {
598 arguments.push(argument.clone());
599 }
600 }
601 Ok(CargoTestInvocation {
602 program: words[cargo].clone(),
603 kind,
604 arguments,
605 runner_arguments,
606 })
607}
608
609fn cargo_option_takes_value(argument: &str) -> Option<bool> {
610 let name = argument.split_once('=').map_or(argument, |(name, _)| name);
611 match name {
612 "-p" | "--package" | "--exclude" | "--bin" | "--example" | "--test" | "--bench" | "-F"
613 | "--features" | "-j" | "--jobs" | "--profile" | "--target" | "--target-dir"
614 | "--message-format" | "--color" | "--config" | "-Z" | "--manifest-path" => {
615 Some(!argument.contains('='))
616 }
617 "--no-run"
618 | "--no-fail-fast"
619 | "--future-incompat-report"
620 | "-q"
621 | "--quiet"
622 | "-v"
623 | "--verbose"
624 | "--workspace"
625 | "--all"
626 | "--lib"
627 | "--bins"
628 | "--examples"
629 | "--tests"
630 | "--benches"
631 | "--all-targets"
632 | "--doc"
633 | "--all-features"
634 | "--no-default-features"
635 | "-r"
636 | "--release"
637 | "--timings"
638 | "--ignore-rust-version"
639 | "--locked"
640 | "--offline"
641 | "--frozen" => Some(false),
642 _ if argument.starts_with("-vv") => Some(false),
643 _ if argument.starts_with("-p") && argument.len() > 2 => Some(false),
644 _ if argument.starts_with("-F") && argument.len() > 2 => Some(false),
645 _ if argument.starts_with("-j") && argument.len() > 2 => Some(false),
646 _ => None,
647 }
648}
649
650pub(crate) fn rust_libtest_selection(
651 invocation: &CargoTestInvocation,
652) -> Result<RustLibtestSelection, RustTestRunnerError> {
653 let mut cargo_filter = None;
656 let mut index = invocation.command_position() + 1;
657 while index < invocation.arguments.len() {
658 let argument = &invocation.arguments[index];
659 if argument.starts_with('-') {
660 if cargo_option_takes_value(argument) == Some(true) {
661 index += 1;
662 }
663 } else {
664 cargo_filter = Some(argument.clone());
666 }
667 index += 1;
668 }
669
670 let mut list_arguments = cargo_filter.into_iter().collect::<Vec<_>>();
671 let mut test_threads = None;
672 let mut index = 0;
673 while index < invocation.runner_arguments.len() {
674 let argument = &invocation.runner_arguments[index];
675 match argument.as_str() {
676 "--ignored" | "--include-ignored" | "--exclude-should-panic" | "--test" | "--bench" => {
677 list_arguments.push(argument.clone());
678 }
679 "--exact" => list_arguments.push(argument.clone()),
680 "--skip" => {
681 let value = invocation.runner_arguments.get(index + 1).ok_or_else(|| {
682 RustTestRunnerError::UnsupportedCommand(
683 "libtest --skip has no filter value".into(),
684 )
685 })?;
686 list_arguments.extend([argument.clone(), value.clone()]);
687 index += 1;
688 }
689 _ if argument.starts_with("--skip=") && argument.len() > "--skip=".len() => {
690 list_arguments.push(argument.clone());
691 }
692 "--test-threads" => {
693 let value = invocation.runner_arguments.get(index + 1).ok_or_else(|| {
694 RustTestRunnerError::UnsupportedCommand(
695 "libtest --test-threads has no value".into(),
696 )
697 })?;
698 let parsed = parse_libtest_threads(value)?;
699 if test_threads.replace(parsed).is_some() {
700 return Err(RustTestRunnerError::UnsupportedCommand(
701 "libtest --test-threads was provided more than once".into(),
702 ));
703 }
704 index += 1;
705 }
706 _ if argument.starts_with("--test-threads=") => {
707 let value = &argument["--test-threads=".len()..];
708 let parsed = parse_libtest_threads(value)?;
709 if test_threads.replace(parsed).is_some() {
710 return Err(RustTestRunnerError::UnsupportedCommand(
711 "libtest --test-threads was provided more than once".into(),
712 ));
713 }
714 }
715 "-Z" => {
716 let value = invocation.runner_arguments.get(index + 1).ok_or_else(|| {
717 RustTestRunnerError::UnsupportedCommand(
718 "libtest -Z has no feature value".into(),
719 )
720 })?;
721 list_arguments.extend([argument.clone(), value.clone()]);
726 index += 1;
727 }
728 _ if argument.starts_with("-Z") && argument.len() > 2 => {
729 list_arguments.push(argument.clone());
730 }
731 "--logfile" | "--color" | "--format" | "--shuffle-seed" => {
732 if invocation.runner_arguments.get(index + 1).is_none() {
733 return Err(RustTestRunnerError::UnsupportedCommand(format!(
734 "libtest {argument} has no value"
735 )));
736 }
737 index += 1;
741 }
742 _ if ["--logfile=", "--color=", "--format=", "--shuffle-seed="]
743 .iter()
744 .any(|prefix| argument.starts_with(prefix) && argument.len() > prefix.len()) => {}
745 "--force-run-in-process"
746 | "--fail-fast"
747 | "--no-capture"
748 | "--nocapture"
749 | "-q"
750 | "--quiet"
751 | "--show-output"
752 | "--report-time"
753 | "--ensure-time"
754 | "--shuffle" => {
755 }
759 "--list" | "-h" | "--help" => {
760 return Err(RustTestRunnerError::UnsupportedCommand(format!(
761 "libtest {argument} does not execute a test suite; exact non-execution mode support is not implemented"
762 )));
763 }
764 _ if !argument.starts_with('-') => list_arguments.push(argument.clone()),
765 _ => {
766 return Err(RustTestRunnerError::UnsupportedCommand(format!(
767 "the pinned Rust 1.95 libtest discovery contract does not recognize option {argument}"
768 )));
769 }
770 }
771 index += 1;
772 }
773 Ok(RustLibtestSelection { list_arguments })
774}
775
776fn parse_libtest_threads(value: &str) -> Result<usize, RustTestRunnerError> {
777 match value.parse::<usize>() {
778 Ok(0) => Err(RustTestRunnerError::UnsupportedCommand(
779 "argument for --test-threads must not be 0".into(),
780 )),
781 Ok(value) => Ok(value),
782 Err(error) => Err(RustTestRunnerError::UnsupportedCommand(format!(
783 "argument for --test-threads must be a number > 0 (error: {error})"
784 ))),
785 }
786}
787
788pub(crate) fn rust_cargo_execution_selection(
789 invocation: &CargoTestInvocation,
790) -> Result<RustCargoExecutionSelection, RustTestRunnerError> {
791 if invocation.kind == RustCargoCommandKind::NextestRun {
792 return Ok(RustCargoExecutionSelection {
793 run_libtests: true,
794 run_doctests: false,
795 doctest_arguments: Vec::new(),
796 });
797 }
798 let test = invocation.command_position();
799 let mut doc = false;
800 let mut other_target = false;
801 let mut index = test + 1;
802 while index < invocation.arguments.len() {
803 let argument = &invocation.arguments[index];
804 let name = argument
805 .split_once('=')
806 .map_or(argument.as_str(), |(name, _)| name);
807 match name {
808 "--doc" => doc = true,
809 "--lib" | "--bins" | "--bin" | "--examples" | "--example" | "--tests" | "--test"
810 | "--benches" | "--bench" | "--all-targets" => other_target = true,
811 _ => {}
812 }
813 if argument.starts_with('-') {
814 let takes_value = cargo_option_takes_value(argument).ok_or_else(|| {
815 RustTestRunnerError::UnsupportedCommand(format!(
816 "the pinned Cargo test contract does not recognize option {argument}"
817 ))
818 })?;
819 if takes_value {
820 index += 1;
821 if index == invocation.arguments.len() {
822 return Err(RustTestRunnerError::UnsupportedCommand(format!(
823 "Cargo option {argument} has no value"
824 )));
825 }
826 }
827 }
828 index += 1;
829 }
830 if doc && other_target {
831 return Err(RustTestRunnerError::UnsupportedCommand(
832 "Cargo --doc cannot be combined with another explicit target selection".into(),
833 ));
834 }
835 let run_doctests = doc || !other_target;
836 let run_libtests = !doc;
837 let mut doctest_arguments = invocation.arguments.clone();
838 if run_doctests && !doc {
839 doctest_arguments.insert(test + 1, "--doc".into());
840 }
841 if !invocation.runner_arguments.is_empty() {
842 doctest_arguments.push("--".into());
843 doctest_arguments.extend(invocation.runner_arguments.iter().cloned());
844 }
845 Ok(RustCargoExecutionSelection {
846 run_libtests,
847 run_doctests,
848 doctest_arguments,
849 })
850}
851
852pub(crate) fn relative_source(root: &Path, path: &Path) -> Result<String, RustTestRunnerError> {
853 let relative = path
854 .strip_prefix(root)
855 .map_err(|_| RustTestRunnerError::UnsafeArtifact(path.display().to_string()))?;
856 if relative.as_os_str().is_empty()
857 || relative
858 .components()
859 .any(|part| !matches!(part, Component::Normal(_)))
860 {
861 return Err(RustTestRunnerError::UnsafeArtifact(
862 path.display().to_string(),
863 ));
864 }
865 Ok(relative.to_string_lossy().replace('\\', "/"))
866}
867
868fn build_owned_arguments(arguments: Vec<String>) -> Result<Vec<String>, RustTestRunnerError> {
874 let mut kept = Vec::with_capacity(arguments.len());
875 let mut arguments = arguments.into_iter();
876 while let Some(argument) = arguments.next() {
877 let name = argument
878 .split_once('=')
879 .map_or(argument.as_str(), |(name, _)| name);
880 match name {
881 "--no-run" => {
882 return Err(RustTestRunnerError::UnsupportedCommand(
883 "`--no-run` builds the tests without running any, so there is nothing to measure; run the command without it".into(),
884 ));
885 }
886 "--message-format" | "--target-dir" => {
887 if !argument.contains('=') {
888 arguments.next();
889 }
890 }
891 _ => kept.push(argument),
892 }
893 }
894 Ok(kept)
895}
896
897fn build_test_artifacts(
898 project: &PreparedRustProject,
899 command: &[String],
900) -> Result<Vec<TestArtifact>, RustTestRunnerError> {
901 let mut invocation = cargo_invocation(&project.workspace_root, command)?;
902 invocation.arguments = build_owned_arguments(invocation.arguments)?;
903 invocation
904 .arguments
905 .extend(["--no-run".into(), "--message-format=json".into()]);
906 let output = Command::new(&invocation.program)
913 .args(invocation.arguments)
914 .current_dir(&project.workspace_root)
915 .env("CARGO_TARGET_DIR", &project.target_directory)
916 .env("RUSTFLAGS", capped_rustflags())
917 .output()
918 .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
919 if !output.status.success() {
920 let rendered = output
924 .stdout
925 .split(|byte| *byte == b'\n')
926 .filter_map(|line| serde_json::from_slice::<serde_json::Value>(line).ok())
927 .filter(|message| {
928 message["reason"] == "compiler-message" && message["message"]["level"] == "error"
929 })
930 .filter_map(|message| message["message"]["rendered"].as_str().map(str::to_owned))
931 .collect::<String>();
932 return Err(RustTestRunnerError::CargoFailed(format!(
933 "{rendered}{}",
934 String::from_utf8_lossy(&output.stderr).trim()
935 )));
936 }
937 let canonical_target = fs::canonicalize(&project.target_directory)
938 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
939 let mut artifacts = Vec::new();
940 for line in output
941 .stdout
942 .split(|byte| *byte == b'\n')
943 .filter(|line| !line.is_empty())
944 {
945 let message: CargoMessage = serde_json::from_slice(line)
946 .map_err(|error| RustTestRunnerError::CargoJson(error.to_string()))?;
947 if message.reason != "compiler-artifact"
948 || !message.profile.as_ref().is_some_and(|profile| profile.test)
949 {
950 continue;
951 }
952 let (Some(executable), Some(target)) = (message.executable, message.target) else {
953 continue;
954 };
955 let executable = fs::canonicalize(&executable)
956 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
957 if !executable.starts_with(&canonical_target)
958 || !fs::metadata(&executable).is_ok_and(|metadata| metadata.is_file())
959 {
960 return Err(RustTestRunnerError::UnsafeArtifact(
961 executable.display().to_string(),
962 ));
963 }
964 let source = fs::canonicalize(target.src_path)
965 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
966 let package_directory = message
967 .manifest_path
968 .as_deref()
969 .and_then(Path::parent)
970 .map_or_else(|| project.workspace_root.clone(), Path::to_path_buf);
971 artifacts.push(TestArtifact {
972 executable,
973 package_directory,
974 name: target.name,
975 kind: if target.kind.iter().any(|kind| kind == "test") {
976 "integration".into()
977 } else {
978 "unit".into()
979 },
980 source: relative_source(&project.workspace_root, &source)?,
981 });
982 }
983 artifacts.sort_by(|left, right| left.executable.cmp(&right.executable));
984 artifacts.dedup_by(|left, right| left.executable == right.executable);
985 if artifacts.is_empty() {
986 return Err(RustTestRunnerError::CargoJson(
987 "Cargo emitted no libtest artifacts".into(),
988 ));
989 }
990 Ok(artifacts)
991}
992
993fn list_tests(
998 executable: &Path,
999 filters: &[String],
1000 environment: &[(&'static str, OsString)],
1001) -> Result<Vec<String>, RustTestRunnerError> {
1002 let output = Command::new(executable)
1003 .args(["--list", "--format", "terse"])
1004 .args(filters)
1005 .envs(environment.iter().map(|(key, value)| (key, value)))
1006 .output()
1007 .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
1008 if !output.status.success() {
1009 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
1013 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned();
1014 let detail = if !stderr.is_empty() {
1015 stderr
1016 } else if !stdout.is_empty() {
1017 format!("no stderr; stdout was {stdout}")
1018 } else {
1019 "no output on either stream".to_owned()
1020 };
1021 return Err(RustTestRunnerError::ListFailed(format!(
1022 "{} exited with {} when asked to --list: {detail}",
1023 executable.display(),
1024 output.status
1025 )));
1026 }
1027 let mut tests = String::from_utf8_lossy(&output.stdout)
1028 .lines()
1029 .filter_map(|line| line.strip_suffix(": test"))
1030 .map(str::to_owned)
1031 .collect::<Vec<_>>();
1032 tests.sort();
1033 tests.dedup();
1034 Ok(tests)
1035}
1036
1037pub(crate) struct RustEvidence {
1040 pub(crate) snapshot: RuntimeSnapshot,
1041 pub(crate) phases: Vec<CoveragePhase>,
1042}
1043
1044struct PendingRecord {
1046 event_type: &'static str,
1047 id: String,
1048 vector: Option<McdcVector>,
1049 sequence: i64,
1050}
1051
1052pub(crate) fn snapshot(
1056 manifest: &CoverageManifest,
1057 directory: &Path,
1058 attempt: &str,
1059) -> Result<RustEvidence, RustTestRunnerError> {
1060 let points = manifest
1061 .points
1062 .iter()
1063 .map(|point| (point.id.as_str(), point))
1064 .collect::<BTreeMap<_, _>>();
1065 let asserting = manifest
1067 .points
1068 .iter()
1069 .chain(&manifest.assertion_sites)
1070 .map(|point| (point.id.as_str(), point))
1071 .collect::<BTreeMap<_, _>>();
1072 let alternatives = manifest
1073 .branches
1074 .iter()
1075 .flat_map(|branch| {
1076 branch
1077 .alternatives
1078 .iter()
1079 .map(|alternative| alternative.id.as_str())
1080 })
1081 .collect::<BTreeSet<_>>();
1082 let decisions = manifest
1083 .decisions
1084 .iter()
1085 .map(|decision| (decision.id.as_str(), decision))
1086 .collect::<BTreeMap<_, _>>();
1087 let mut hits = BTreeSet::new();
1088 let mut vectors = BTreeMap::<String, BTreeSet<(Vec<Option<bool>>, bool)>>::new();
1089 let mut pending = BTreeMap::<u64, Vec<PendingRecord>>::new();
1093 let mut events = Vec::new();
1094 let mut phases = Vec::new();
1095 let mut sequence = 0_i64;
1098 let token = crate::rust_project::manifest_token(manifest);
1103 for (name, observations) in read_rust_probe_directory(directory)
1104 .map_err(|error| RustTestRunnerError::Probe(error.to_string()))?
1105 {
1106 if !name.starts_with(&token) {
1107 continue;
1108 }
1109 for entry in observations {
1110 sequence += 1;
1111 let thread = entry.thread;
1112 match entry.observation {
1113 RustProbeObservation::Hit { id } => {
1114 if !points.contains_key(id.as_str()) && !alternatives.contains(id.as_str()) {
1115 return Err(RustTestRunnerError::UnknownProbe(id));
1116 }
1117 hits.insert(id.clone());
1118 pending.entry(thread).or_default().push(PendingRecord {
1119 event_type: "hit",
1120 id,
1121 vector: None,
1122 sequence,
1123 });
1124 }
1125 RustProbeObservation::Decision {
1126 id,
1127 values,
1128 outcome,
1129 } => {
1130 let Some(meta) = decisions.get(id.as_str()) else {
1131 return Err(RustTestRunnerError::UnknownProbe(id));
1132 };
1133 if values.len() != meta.conditions.len() {
1134 return Err(RustTestRunnerError::InvalidVector {
1135 id,
1136 expected: meta.conditions.len(),
1137 actual: values.len(),
1138 });
1139 }
1140 hits.insert(format!(
1141 "{}:outcome:{}",
1142 meta.id,
1143 if outcome { "true" } else { "false" }
1144 ));
1145 vectors
1146 .entry(meta.id.clone())
1147 .or_default()
1148 .insert((values.clone(), outcome));
1149 pending.entry(thread).or_default().push(PendingRecord {
1150 event_type: "decision",
1151 id,
1152 vector: Some(McdcVector { values, outcome }),
1153 sequence,
1154 });
1155 }
1156 RustProbeObservation::Assertion { id } => {
1157 let Some(point) = asserting.get(id.as_str()) else {
1160 return Err(RustTestRunnerError::UnknownProbe(id));
1161 };
1162 let witnessed = pending.remove(&thread).unwrap_or_default();
1163 let phase_id = format!("{attempt}:assertion:{sequence}");
1164 phases.push(CoveragePhase {
1165 id: phase_id.clone(),
1166 kind: "assertion".into(),
1167 operation: format!(
1168 "Rust assertion at {}:{}:{}",
1169 point.file, point.line, point.column
1170 ),
1171 source: Some(point.source.clone()),
1172 caused_by_phase_id: None,
1173 started_at_ms: witnessed.first().map_or(sequence, |record| record.sequence),
1174 ended_at_ms: Some(sequence),
1175 status: Some("passed".into()),
1178 error: None,
1179 });
1180 for record in witnessed {
1181 events.push(RuntimeEvent {
1182 event_type: record.event_type.into(),
1183 id: record.id,
1184 vector: record.vector,
1185 timestamp_ms: record.sequence,
1186 phase_id: Some(phase_id.clone()),
1187 statement_id: None,
1188 environment: "server".into(),
1189 });
1190 }
1191 }
1192 }
1193 }
1194 }
1195 let mut decision_snapshots = Vec::new();
1196 for (id, observed) in vectors {
1197 let meta: DecisionMeta = (*decisions[id.as_str()]).clone();
1198 decision_snapshots.push(DecisionSnapshot {
1199 meta,
1200 vectors: observed
1201 .into_iter()
1202 .map(|(values, outcome)| McdcVector { values, outcome })
1203 .collect(),
1204 });
1205 }
1206 Ok(RustEvidence {
1207 snapshot: RuntimeSnapshot {
1208 decisions: decision_snapshots,
1209 hits: hits.into_iter().collect(),
1210 events,
1211 logicals: Vec::new(),
1212 phase_id: None,
1213 },
1214 phases,
1215 })
1216}
1217
1218fn rust_coverage_model() -> CoverageModelDeclaration {
1219 CoverageModelDeclaration {
1220 language: "rust".into(),
1221 variant: "rust-owned-probes-v1".into(),
1222 name: "supercov-rust-owned-v1".into(),
1223 completeness_meaning: "Every semantics-proven Rust obligation in the owned source denominator was observed; explicit manifest limitations identify unmeasured Rust surfaces.".into(),
1224 measured: vec![
1225 "owned Rust statements and function entries".into(),
1226 "owned atomic condition vectors and decision outcomes".into(),
1227 "exact process-per-libtest attribution".into(),
1228 "exact process-per-doctest attribution".into(),
1229 "evidence a passing assertion of the same thread witnessed".into(),
1230 ],
1231 not_measured: vec![
1232 "macro-expanded and generated Rust code".into(),
1233 "const-evaluated code and unsupported structural branch probes".into(),
1234 "causal linkage to individual actions".into(),
1235 "all input values, semantic partitions, paths, or concurrency interleavings".into(),
1236 "mutation score or assertion fault-detection strength".into(),
1237 ],
1238 }
1239}
1240
1241pub(crate) fn instrumented_stack_environment() -> Vec<(&'static str, &'static str)> {
1253 if std::env::var_os("RUST_MIN_STACK").is_some() {
1254 Vec::new()
1255 } else {
1256 vec![("RUST_MIN_STACK", "16777216")]
1257 }
1258}
1259
1260fn rustc_print_path(request: &str) -> Result<PathBuf, RustTestRunnerError> {
1262 let output = Command::new("rustc")
1263 .args(["--print", request])
1264 .output()
1265 .map_err(|error| {
1266 RustTestRunnerError::Launch(format!("rustc --print {request}: {error}"))
1267 })?;
1268 if !output.status.success() {
1269 return Err(RustTestRunnerError::Launch(format!(
1270 "rustc --print {request} exited with {}",
1271 output.status
1272 )));
1273 }
1274 Ok(PathBuf::from(
1275 String::from_utf8_lossy(&output.stdout).trim(),
1276 ))
1277}
1278
1279pub(crate) fn rustc_sysroot() -> Result<PathBuf, RustTestRunnerError> {
1281 rustc_print_path("sysroot")
1282}
1283
1284pub(crate) fn rustc_target_libdir() -> Result<PathBuf, RustTestRunnerError> {
1288 rustc_print_path("target-libdir")
1289}
1290
1291pub(crate) fn dynamic_library_environment(
1297 target_libdir: &Path,
1298 executable: &Path,
1299) -> Vec<(&'static str, OsString)> {
1300 let variable = if cfg!(target_os = "macos") {
1301 "DYLD_FALLBACK_LIBRARY_PATH"
1302 } else if cfg!(windows) {
1303 "PATH"
1304 } else {
1305 "LD_LIBRARY_PATH"
1306 };
1307 let mut entries = Vec::new();
1308 if let Some(deps) = executable.parent() {
1309 entries.push(deps.to_path_buf());
1310 if let Some(profile) = deps.parent() {
1311 entries.push(profile.to_path_buf());
1312 }
1313 }
1314 entries.push(target_libdir.to_path_buf());
1315 if let Some(existing) = std::env::var_os(variable) {
1316 entries.extend(std::env::split_paths(&existing));
1317 }
1318 match std::env::join_paths(entries) {
1319 Ok(value) => vec![(variable, value)],
1320 Err(_) => Vec::new(),
1321 }
1322}
1323
1324pub(crate) fn capped_rustflags() -> String {
1330 let mut rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
1331 if !rustflags.is_empty() {
1332 rustflags.push(' ');
1333 }
1334 rustflags.push_str("--cap-lints=allow");
1335 rustflags
1336}
1337
1338pub(crate) fn io_error(error: impl std::fmt::Display) -> RustTestRunnerError {
1339 RustTestRunnerError::Io(error.to_string())
1340}
1341
1342fn running_line(
1348 workspace: &Path,
1349 target: &Path,
1350 artifact: &TestArtifact,
1351 coloured: bool,
1352) -> String {
1353 let text = |path: &Path| {
1358 let text = path.to_string_lossy().replace('\\', "/");
1359 text.strip_prefix("//?/").unwrap_or(&text).to_owned()
1360 };
1361 let package = text(&artifact.package_directory);
1362 let package = package
1363 .strip_prefix(&text(workspace))
1364 .map_or("", |inside| inside.trim_matches('/'));
1365 let source = artifact
1366 .source
1367 .strip_prefix(package)
1368 .map_or(artifact.source.as_str(), |local| {
1369 local.trim_start_matches('/')
1370 })
1371 .split('/')
1372 .collect::<PathBuf>();
1373 let executable = artifact.executable.strip_prefix(target).map_or_else(
1376 |_| artifact.executable.clone(),
1377 |built| Path::new("target").join(built),
1378 );
1379 format!(
1380 "{} {}{} ({})",
1381 if coloured {
1382 "\u{1b}[1m\u{1b}[92m Running\u{1b}[0m"
1383 } else {
1384 " Running"
1385 },
1386 if artifact.kind == "integration" {
1387 ""
1388 } else {
1389 "unittests "
1390 },
1391 source.display(),
1392 executable.display()
1393 )
1394}
1395
1396fn cargo_colours(invocation: &CargoTestInvocation) -> bool {
1400 use std::io::IsTerminal;
1401 let arguments = &invocation.arguments;
1402 let asked = arguments
1403 .iter()
1404 .enumerate()
1405 .find_map(|(index, argument)| {
1406 argument
1407 .strip_prefix("--color=")
1408 .map(str::to_owned)
1409 .or_else(|| (argument == "--color").then(|| arguments.get(index + 1).cloned())?)
1410 })
1411 .or_else(|| std::env::var("CARGO_TERM_COLOR").ok());
1412 let set = |name: &str| std::env::var(name).ok().filter(|value| !value.is_empty());
1413 match asked.as_deref() {
1414 Some("always") => true,
1415 Some("never") => false,
1416 _ if set("CLICOLOR_FORCE").is_some_and(|value| value != "0") => true,
1419 _ if set("NO_COLOR").is_some() || set("CLICOLOR").is_some_and(|value| value == "0") => {
1420 false
1421 }
1422 _ => {
1423 std::io::stderr().is_terminal()
1424 && (cfg!(windows) || set("TERM").is_some_and(|term| term != "dumb"))
1425 }
1426 }
1427}
1428
1429fn libtest_colours(invocation: &CargoTestInvocation) -> bool {
1433 use std::io::IsTerminal;
1434 let arguments = &invocation.runner_arguments;
1435 let asked = arguments.iter().enumerate().find_map(|(index, argument)| {
1436 argument
1437 .strip_prefix("--color=")
1438 .map(str::to_owned)
1439 .or_else(|| (argument == "--color").then(|| arguments.get(index + 1).cloned())?)
1440 });
1441 match asked.as_deref() {
1442 Some("always") => true,
1443 Some("never") => false,
1444 _ => {
1445 std::io::stdout().is_terminal()
1446 && (cfg!(windows)
1447 || std::env::var("TERM").is_ok_and(|term| !term.is_empty() && term != "dumb"))
1448 }
1449 }
1450}
1451
1452pub(crate) fn cargo_quiet(invocation: &CargoTestInvocation) -> bool {
1454 invocation.arguments[invocation.command_position() + 1..]
1455 .iter()
1456 .any(|argument| matches!(argument.as_str(), "-q" | "--quiet"))
1457}
1458
1459pub(crate) fn libtest_quiet(invocation: &CargoTestInvocation) -> bool {
1461 let runner = &invocation.runner_arguments;
1462 runner
1463 .iter()
1464 .any(|argument| matches!(argument.as_str(), "-q" | "--quiet" | "--format=terse"))
1465 || runner
1466 .windows(2)
1467 .any(|pair| pair[0] == "--format" && pair[1] == "terse")
1468}
1469
1470pub(crate) fn libtest_skipped(exit: i32, stdout: &str) -> bool {
1471 exit == 0 && (stdout.contains("running 0 tests") || stdout.contains("; 1 ignored;"))
1472}
1473
1474fn rust_runner_limitations(runner: &str) -> Vec<FrontendLimitation> {
1477 let prefix = if runner == "rust-libtest" {
1478 "rust".to_owned()
1479 } else {
1480 runner.to_owned()
1481 };
1482 vec![
1483 FrontendLimitation {
1484 id: format!("{prefix}-action-linkage-unavailable"),
1485 scopes: vec![FrontendLimitationScope::Action],
1486 reason: "Rust test frameworks expose no general action lifecycle".into(),
1487 },
1488 FrontendLimitation {
1489 id: format!("{prefix}-assertion-linkage-unavailable"),
1490 scopes: vec![FrontendLimitationScope::Assertion],
1491 reason: "assertion macros do not expose a stable per-assertion success lifecycle"
1492 .into(),
1493 },
1494 ]
1495}
1496
1497fn rust_runner_declaration(runner: &str) -> FrontendRunnerDeclaration {
1498 FrontendRunnerDeclaration {
1499 runner: runner.into(),
1500 execution_model: ExecutionModel::ProcessPerTest,
1501 attribution: FrontendAttribution {
1502 run: AttributionPrecision::Exact,
1503 worker: AttributionPrecision::Exact,
1504 test: AttributionPrecision::Exact,
1505 retry: AttributionPrecision::Exact,
1506 phase: AttributionPrecision::Exact,
1507 action: AttributionPrecision::Unavailable,
1508 assertion: AttributionPrecision::Unavailable,
1509 },
1510 limitations: rust_runner_limitations(runner),
1511 }
1512}
1513
1514fn decline_uncompiled_sources(
1523 project: &PreparedRustProject,
1524) -> (CoverageManifest, BTreeSet<String>) {
1525 let mut manifest = project.manifest.clone();
1526 let Some(compiled) = crate::rust_project::compiled_source_files(
1527 &project.workspace_root,
1528 &project.target_directory,
1529 ) else {
1530 return (manifest, BTreeSet::new());
1531 };
1532 let declined = project
1533 .source_files
1534 .iter()
1535 .filter(|file| !compiled.contains(*file))
1536 .cloned()
1537 .collect::<BTreeSet<_>>();
1538 if declined.is_empty()
1541 || project
1542 .crate_roots
1543 .iter()
1544 .all(|root| !compiled.contains(root))
1545 {
1546 return (manifest, BTreeSet::new());
1547 }
1548 let mut unmeasured = manifest.unmeasured.iter().cloned().collect::<BTreeSet<_>>();
1549 unmeasured.extend(
1550 manifest
1551 .points
1552 .iter()
1553 .filter(|point| declined.contains(&point.file))
1554 .map(|point| point.id.clone()),
1555 );
1556 unmeasured.extend(
1557 manifest
1558 .decisions
1559 .iter()
1560 .filter(|decision| declined.contains(&decision.file))
1561 .map(|decision| decision.id.clone()),
1562 );
1563 unmeasured.extend(
1564 manifest
1565 .branches
1566 .iter()
1567 .filter(|branch| declined.contains(&branch.file))
1568 .map(|branch| branch.id.clone()),
1569 );
1570 manifest.unmeasured = unmeasured.into_iter().collect();
1571 manifest.limitations.retain(|limitation| {
1573 limitation
1574 .get("file")
1575 .and_then(|file| file.as_str())
1576 .is_none_or(|file| !declined.contains(file))
1577 });
1578 if let Some(entries) = manifest
1580 .scope
1581 .as_mut()
1582 .and_then(|scope| scope.get_mut("entries"))
1583 .and_then(serde_json::Value::as_array_mut)
1584 {
1585 for entry in entries {
1586 let named = entry
1587 .get("file")
1588 .and_then(serde_json::Value::as_str)
1589 .is_some_and(|file| declined.contains(file));
1590 if named && let Some(entry) = entry.as_object_mut() {
1591 entry.insert("status".into(), "excluded".into());
1592 entry.insert(
1593 "reason".into(),
1594 crate::rust_project::RUST_NOT_COMPILED.into(),
1595 );
1596 }
1597 }
1598 }
1599 (manifest, declined)
1600}
1601
1602fn structural_limitations(manifest: &CoverageManifest) -> Vec<String> {
1604 manifest
1605 .limitations
1606 .iter()
1607 .filter_map(|item| {
1608 item.get("id")
1609 .and_then(|value| value.as_str())
1610 .map(str::to_owned)
1611 })
1612 .collect()
1613}
1614
1615pub fn run_prepared_rust_tests(
1616 project: &PreparedRustProject,
1617 command: &[String],
1618 run_id: &str,
1619 generated_at: &str,
1620 diagnostics: &mut dyn Write,
1621) -> Result<RustFrontendRun, RustTestRunnerError> {
1622 let invocation = cargo_invocation(&project.workspace_root, command)?;
1623 let selection = rust_cargo_execution_selection(&invocation)?;
1624 let build_started = Instant::now();
1625 let artifacts = if selection.run_libtests && invocation.kind == RustCargoCommandKind::CargoTest
1629 {
1630 build_test_artifacts(project, command)?
1631 } else {
1632 Vec::new()
1633 };
1634 let build_ms = build_started.elapsed().as_secs_f64() * 1000.0;
1635 let (manifest, declined) = decline_uncompiled_sources(project);
1639 if !declined.is_empty() {
1640 writeln!(
1641 diagnostics,
1642 "[supercov] {} source file(s) this build did not compile are outside the denominator: {}{}",
1643 declined.len(),
1644 declined
1645 .iter()
1646 .take(3)
1647 .cloned()
1648 .collect::<Vec<_>>()
1649 .join(", "),
1650 if declined.len() > 3 { ", ..." } else { "" }
1651 )
1652 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1653 }
1654 let project = &PreparedRustProject {
1655 manifest,
1656 ..project.clone()
1657 };
1658 let _signal_guard = crate::child_signal_guard::ChildSignalGuard::install()
1662 .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
1663 let evidence_root = project
1664 .workspace_root
1665 .join(".supercov/rust-evidence")
1666 .join(run_id);
1667 fs::create_dir_all(&evidence_root)
1668 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1669 let mut results = Vec::new();
1670 let mut overall_exit = 0;
1671 let execution_started = Instant::now();
1672 if invocation.kind == RustCargoCommandKind::NextestRun {
1673 let outcome = crate::rust_owned_nextest::run_nextest(
1676 project,
1677 &invocation,
1678 &evidence_root.join("nextest"),
1679 run_id,
1680 diagnostics,
1681 )?;
1682 let artifact_count = outcome.artifact_files.len();
1683 return Ok(RustFrontendRun {
1684 declaration: FrontendRunDeclaration {
1685 protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
1686 frontend_id: "rust".into(),
1687 frontend_version: "rust-owned-v1".into(),
1688 language: "rust".into(),
1689 structural_source: StructuralSource::OwnedProbes,
1690 runners: vec![rust_runner_declaration("nextest")],
1691 structural_limitations: structural_limitations(&project.manifest),
1692 },
1693 request: CoverageReportRequest {
1694 run_id: run_id.into(),
1695 manifest: project.manifest.clone(),
1696 raw_results: outcome.results,
1697 generated_at: generated_at.into(),
1698 coverage_model: Some(rust_coverage_model()),
1699 integrity: None,
1700 test_exit_code: ExitCodeInput::Present(Some(outcome.exit_code)),
1701 },
1702 exit_code: outcome.exit_code,
1703 artifacts: artifact_count,
1704 artifact_files: outcome.artifact_files,
1705 build_ms,
1706 execution_ms: execution_started.elapsed().as_secs_f64() * 1000.0,
1707 });
1708 }
1709 let libtest_selection = rust_libtest_selection(&invocation)?;
1715 let coloured_results = libtest_colours(&invocation);
1718 let ignored_mode = libtest_selection
1719 .list_arguments
1720 .iter()
1721 .filter(|argument| {
1722 matches!(
1723 argument.as_str(),
1724 "--ignored" | "--include-ignored" | "--exclude-should-panic"
1725 )
1726 })
1727 .cloned()
1728 .collect::<Vec<_>>();
1729 let mut tasks = Vec::new();
1730 let target_libdir = if artifacts.is_empty() {
1731 PathBuf::new()
1732 } else {
1733 rustc_target_libdir()?
1734 };
1735 let mut unasked = Vec::new();
1737 for (artifact_index, artifact) in artifacts.iter().enumerate() {
1738 let tests = list_tests(
1739 &artifact.executable,
1740 &libtest_selection.list_arguments,
1741 &dynamic_library_environment(&target_libdir, &artifact.executable),
1742 )?;
1743 if tests.is_empty() {
1744 let all = list_tests(
1745 &artifact.executable,
1746 &["--include-ignored".to_owned()],
1747 &dynamic_library_environment(&target_libdir, &artifact.executable),
1748 )?;
1749 unasked.push((artifact, all.len()));
1750 }
1751 let contexts = preflight_rust_test_contexts(tests.clone())
1752 .map_err(|error| RustTestRunnerError::Context(error.to_string()))?;
1753 for (test_index, test) in tests.into_iter().enumerate() {
1754 let directory = evidence_root.join(format!("{artifact_index:04}-{test_index:08}"));
1755 fs::create_dir(&directory)
1756 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1757 tasks.push(ProcessTask {
1758 ordinal: tasks.len(),
1759 artifact_index,
1760 test_index,
1761 artifact: artifact.clone(),
1762 context_id: contexts[&test],
1763 test,
1764 directory,
1765 });
1766 }
1767 }
1768 let workers = std::thread::available_parallelism()
1769 .map(usize::from)
1770 .unwrap_or(1)
1771 .min(tasks.len().max(1));
1772 let next = AtomicUsize::new(0);
1773 let outcomes = Mutex::new(Vec::<Result<ProcessOutcome, String>>::with_capacity(
1774 tasks.len(),
1775 ));
1776 std::thread::scope(|scope| {
1777 for _ in 0..workers {
1778 scope.spawn(|| {
1779 loop {
1780 let index = next.fetch_add(1, Ordering::Relaxed);
1781 let Some(task) = tasks.get(index) else { break };
1782 let result = crate::child_signal_guard::output(
1783 Command::new(&task.artifact.executable)
1784 .args(["--exact", &task.test])
1793 .args(&ignored_mode)
1794 .args(coloured_results.then_some("--color=always"))
1795 .current_dir(&task.artifact.package_directory)
1796 .envs(instrumented_stack_environment())
1797 .envs(dynamic_library_environment(
1798 &target_libdir,
1799 &task.artifact.executable,
1800 ))
1801 .env("SUPERCOV_RUST_EVIDENCE_DIR", &task.directory)
1802 .env(
1803 crate::rust_probe_transport::RUST_CONTEXT_ENV,
1804 format!("{:016x}", task.context_id),
1805 ),
1806 )
1807 .map(|output| ProcessOutcome {
1808 task: ProcessTask {
1809 ordinal: task.ordinal,
1810 artifact_index: task.artifact_index,
1811 test_index: task.test_index,
1812 artifact: task.artifact.clone(),
1813 test: task.test.clone(),
1814 context_id: task.context_id,
1815 directory: task.directory.clone(),
1816 },
1817 output,
1818 })
1819 .map_err(|error| error.to_string());
1820 outcomes
1821 .lock()
1822 .expect("Rust test result lock poisoned")
1823 .push(result);
1824 }
1825 });
1826 }
1827 });
1828 let mut outcomes = outcomes
1829 .into_inner()
1830 .map_err(|_| RustTestRunnerError::Io("Rust test result lock poisoned".into()))?
1831 .into_iter()
1832 .map(|result| result.map_err(RustTestRunnerError::Launch))
1833 .collect::<Result<Vec<_>, _>>()?;
1834 outcomes.sort_by_key(|outcome| outcome.task.ordinal);
1835 let unannounced = cargo_quiet(&invocation);
1840 let terse = unannounced || libtest_quiet(&invocation);
1841 let coloured = cargo_colours(&invocation);
1842 let announced = |artifact: &TestArtifact, report: String| {
1843 (
1844 (artifact.kind == "integration", artifact.name.clone()),
1845 crate::rust_libtest_report::Announced {
1846 heading: (!unannounced).then(|| {
1847 running_line(
1848 &project.workspace_root,
1849 &project.target_directory,
1850 artifact,
1851 coloured,
1852 )
1853 }),
1854 report,
1855 },
1856 )
1857 };
1858 let mut reports = Vec::new();
1860 for binary in
1861 outcomes.chunk_by(|left, right| left.task.artifact_index == right.task.artifact_index)
1862 {
1863 let texts = binary
1864 .iter()
1865 .map(|outcome| {
1866 (
1867 String::from_utf8_lossy(&outcome.output.stdout),
1868 String::from_utf8_lossy(&outcome.output.stderr),
1869 )
1870 })
1871 .collect::<Vec<_>>();
1872 let cases = binary
1873 .iter()
1874 .zip(&texts)
1875 .map(
1876 |(outcome, (stdout, stderr))| crate::rust_libtest_report::CaseOutput {
1877 name: &outcome.task.test,
1878 exit: outcome.output.status.code().unwrap_or(1),
1879 stdout,
1880 stderr,
1881 },
1882 )
1883 .collect::<Vec<_>>();
1884 let artifact = &binary[0].task.artifact;
1885 reports.push(announced(
1886 artifact,
1887 crate::rust_libtest_report::libtest_report(&cases, terse),
1888 ));
1889 }
1890 for (artifact, all) in unasked {
1891 reports.push(announced(
1892 artifact,
1893 crate::rust_libtest_report::unasked_report(all),
1894 ));
1895 }
1896 reports.sort_by(|(left, _), (right, _)| left.cmp(right));
1897 let reports = reports
1898 .into_iter()
1899 .map(|(_, report)| report)
1900 .collect::<Vec<_>>();
1901 crate::rust_libtest_report::write_reports(diagnostics, &reports)
1902 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1903 for outcome in outcomes {
1904 let ProcessTask {
1905 artifact_index,
1906 test_index,
1907 artifact,
1908 test,
1909 directory,
1910 ..
1911 } = outcome.task;
1912 let test_id = format!("{}::{test}", artifact.source);
1916 let worker_id = format!("artifact-{artifact_index:04}");
1917 let attempt_id = format!("{run_id}:{artifact_index:04}:{test_index:08}");
1918 let output = outcome.output;
1919 let exit = output.status.code().unwrap_or(1);
1920 let stdout = String::from_utf8_lossy(&output.stdout);
1921 let skipped = libtest_skipped(exit, &stdout);
1922 if exit != 0 {
1923 overall_exit = exit;
1924 }
1925 let evidence = snapshot(&project.manifest, &directory, &attempt_id)?;
1926 results.push(RawTestResult {
1927 test_id: Some(test_id.clone()),
1928 scope: Some(ExecutionScope {
1929 version: 1,
1930 run_id: run_id.into(),
1931 worker_id,
1932 test_id: test_id.clone(),
1933 test_key: format!("{}::{test}", artifact.source),
1934 retry: 0,
1935 attempt_id,
1936 }),
1937 test: test_id,
1938 test_file: Some(artifact.source.clone()),
1939 title: Some(test),
1940 retry: Some(0),
1941 status: Some(
1942 if exit != 0 {
1943 "failed"
1944 } else if skipped {
1945 "skipped"
1946 } else {
1947 "passed"
1948 }
1949 .into(),
1950 ),
1951 expected_status: Some("passed".into()),
1952 flaky: false,
1953 provenance: TestProvenance {
1954 runner: "rust-libtest".into(),
1955 kind: artifact.kind,
1956 project: Some(artifact.name),
1957 source: "supercov-owned-process-per-test".into(),
1958 tags: Vec::new(),
1959 },
1960 role: "test".into(),
1961 attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
1962 phases: evidence.phases,
1963 runtime: vec![evidence.snapshot],
1964 browser: Vec::new(),
1965 server: Vec::new(),
1966 });
1967 }
1968 let doctest_results =
1969 if selection.run_doctests && invocation.kind == RustCargoCommandKind::CargoTest {
1970 crate::rust_owned_doctests::run_doctests(
1971 project,
1972 &invocation,
1973 &selection,
1974 &evidence_root.join("doctests"),
1975 run_id,
1976 diagnostics,
1977 &mut overall_exit,
1978 )?
1979 } else {
1980 Vec::new()
1981 };
1982 let ran_libtests = !results.is_empty();
1985 let ran_doctests = !doctest_results.is_empty();
1986 results.extend(doctest_results);
1987 let mut runners = Vec::new();
1988 if ran_libtests || !ran_doctests {
1989 runners.push(rust_runner_declaration("rust-libtest"));
1990 }
1991 if ran_doctests {
1992 runners.push(rust_runner_declaration("rustdoc"));
1993 }
1994 let structural_limitations = structural_limitations(&project.manifest);
1995 Ok(RustFrontendRun {
1996 declaration: FrontendRunDeclaration {
1997 protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
1998 frontend_id: "rust".into(),
1999 frontend_version: "rust-owned-v1".into(),
2000 language: "rust".into(),
2001 structural_source: StructuralSource::OwnedProbes,
2002 runners,
2003 structural_limitations,
2004 },
2005 request: CoverageReportRequest {
2006 run_id: run_id.into(),
2007 manifest: project.manifest.clone(),
2008 raw_results: results,
2009 generated_at: generated_at.into(),
2010 coverage_model: Some(rust_coverage_model()),
2011 integrity: None,
2012 test_exit_code: ExitCodeInput::Present(Some(overall_exit)),
2013 },
2014 exit_code: overall_exit,
2015 artifacts: artifacts.len(),
2016 artifact_files: artifacts
2017 .iter()
2018 .map(|artifact| artifact.executable.clone())
2019 .collect(),
2020 build_ms,
2021 execution_ms: execution_started.elapsed().as_secs_f64() * 1000.0,
2022 })
2023}
2024
2025#[cfg(test)]
2026mod tests {
2027 use std::time::{SystemTime, UNIX_EPOCH};
2028
2029 use super::*;
2030 use crate::{
2031 coverage_report::{ArchiveReportRequest, analyze_coverage_archive},
2032 evidence_archive::write_archive,
2033 frontend_protocol::validate_frontend_report_request,
2034 rust_project::prepare_rust_project,
2035 };
2036
2037 #[test]
2038 fn a_test_binary_is_announced_as_cargo_announces_it() {
2039 let artifact = |package: &str, source: &str, kind: &str| TestArtifact {
2043 executable: PathBuf::from(
2044 "/work/copy/.supercov/cache/rust-target/debug/deps/shop-0123456789abcdef",
2045 ),
2046 name: "shop".into(),
2047 kind: kind.into(),
2048 source: source.into(),
2049 package_directory: PathBuf::from(package),
2050 };
2051 let line = |artifact: &TestArtifact| {
2052 running_line(
2053 Path::new("/work/copy"),
2054 Path::new("/work/copy/.supercov/cache/rust-target"),
2055 artifact,
2056 false,
2057 )
2058 .replace('\\', "/")
2059 };
2060 assert_eq!(
2061 line(&artifact("/work/copy", "src/lib.rs", "unit")),
2062 " Running unittests src/lib.rs (target/debug/deps/shop-0123456789abcdef)"
2063 );
2064 assert_eq!(
2065 line(&artifact(
2066 "/work/copy/crates/shop",
2067 "crates/shop/tests/outside.rs",
2068 "integration"
2069 )),
2070 " Running tests/outside.rs (target/debug/deps/shop-0123456789abcdef)"
2071 );
2072 let windows = running_line(
2074 Path::new("C:\\work\\copy"),
2075 Path::new("C:\\work\\copy\\target"),
2076 &artifact(
2077 "\\\\?\\C:\\work\\copy\\crates\\shop",
2078 "crates/shop/src/lib.rs",
2079 "unit",
2080 ),
2081 false,
2082 )
2083 .replace('\\', "/");
2084 assert!(
2085 windows.starts_with(" Running unittests src/lib.rs ("),
2086 "{windows}"
2087 );
2088 assert!(
2089 running_line(
2090 Path::new("/work/copy"),
2091 Path::new("/work/copy/.supercov/cache/rust-target"),
2092 &artifact("/work/copy", "src/lib.rs", "unit"),
2093 true,
2094 )
2095 .starts_with("\u{1b}[1m\u{1b}[92m Running\u{1b}[0m unittests ")
2096 );
2097 }
2098
2099 #[test]
2102 fn a_filter_given_to_a_package_script_reaches_cargo() {
2103 let root = std::env::temp_dir().join(format!(
2104 "supercov-npm-cargo-{}-{}",
2105 std::process::id(),
2106 std::time::SystemTime::now()
2107 .duration_since(std::time::UNIX_EPOCH)
2108 .unwrap()
2109 .as_nanos()
2110 ));
2111 std::fs::create_dir(&root).unwrap();
2112 std::fs::write(
2113 root.join("package.json"),
2114 r#"{ "scripts": { "test": "cargo test --lib" } }"#,
2115 )
2116 .unwrap();
2117 let invocation = cargo_invocation(
2118 &root,
2119 &["npm".into(), "test".into(), "--".into(), "adds".into()],
2120 )
2121 .unwrap();
2122 std::fs::remove_dir_all(&root).unwrap();
2123 assert_eq!(invocation.kind, RustCargoCommandKind::CargoTest);
2124 assert_eq!(invocation.arguments, ["test", "--lib", "adds"]);
2125 }
2126
2127 #[test]
2128 fn cargo_and_libtest_selection_is_preserved_without_presentation_guessing() {
2129 let root = Path::new(".");
2130 let invocation = cargo_invocation(
2131 root,
2132 &[
2133 "cargo".into(),
2134 "test".into(),
2135 "-p".into(),
2136 "fixture".into(),
2137 "authored".into(),
2138 "--".into(),
2139 "generated".into(),
2140 "--skip".into(),
2141 "slow".into(),
2142 "--include-ignored".into(),
2143 ],
2144 )
2145 .unwrap();
2146 assert_eq!(invocation.arguments, ["test", "-p", "fixture", "authored"]);
2147 assert_eq!(
2148 invocation.runner_arguments,
2149 ["generated", "--skip", "slow", "--include-ignored"]
2150 );
2151 let selection = rust_libtest_selection(&invocation).unwrap();
2152 assert_eq!(
2153 selection.list_arguments,
2154 [
2155 "authored",
2156 "generated",
2157 "--skip",
2158 "slow",
2159 "--include-ignored"
2160 ]
2161 );
2162 }
2163
2164 #[test]
2165 fn direct_cargo_argv_preserves_toml_quotes_inside_config_values() {
2166 let config = "target.host.runner=[\"runner with spaces\",\"--fixed\"]";
2167 let invocation = cargo_invocation(
2168 Path::new("."),
2169 &[
2170 "cargo".into(),
2171 "test".into(),
2172 "--config".into(),
2173 config.into(),
2174 ],
2175 )
2176 .unwrap();
2177 assert_eq!(invocation.arguments, ["test", "--config", config]);
2178 }
2179
2180 #[test]
2181 fn nextest_run_is_detected_without_reclassifying_its_filters_or_retries() {
2182 let invocation = cargo_invocation(
2183 Path::new("."),
2184 &[
2185 "cargo".into(),
2186 "+1.95.0".into(),
2187 "nextest".into(),
2188 "run".into(),
2189 "--retries".into(),
2190 "2".into(),
2191 "-E".into(),
2192 "test(/flaky/)".into(),
2193 "--".into(),
2194 "--nocapture".into(),
2195 ],
2196 )
2197 .unwrap();
2198 assert_eq!(invocation.kind, RustCargoCommandKind::NextestRun);
2199 assert_eq!(
2200 invocation.arguments,
2201 [
2202 "+1.95.0",
2203 "nextest",
2204 "run",
2205 "--retries",
2206 "2",
2207 "-E",
2208 "test(/flaky/)",
2209 ]
2210 );
2211 assert_eq!(invocation.runner_arguments, ["--nocapture"]);
2212 let execution = rust_cargo_execution_selection(&invocation).unwrap();
2213 assert!(execution.run_libtests);
2214 assert!(!execution.run_doctests);
2215 assert!(execution.doctest_arguments.is_empty());
2216 assert_eq!(
2217 nextest_list_invocation(&invocation).unwrap(),
2218 NextestListInvocation {
2219 arguments: vec![
2220 "+1.95.0".into(),
2221 "nextest".into(),
2222 "list".into(),
2223 "-E".into(),
2224 "test(/flaky/)".into(),
2225 "--message-format".into(),
2226 "json".into(),
2227 ],
2228 runner_arguments: vec!["--nocapture".into()],
2229 }
2230 );
2231 }
2232
2233 #[test]
2234 fn nextest_list_projection_preserves_selection_and_rejects_external_state() {
2235 let invocation = CargoTestInvocation {
2236 program: "cargo".into(),
2237 kind: RustCargoCommandKind::NextestRun,
2238 arguments: vec![
2239 "nextest".into(),
2240 "run".into(),
2241 "--package=fixture".into(),
2242 "--partition".into(),
2243 "hash:1/2".into(),
2244 "--test-threads=8".into(),
2245 "--failure-output".into(),
2246 "final".into(),
2247 "name".into(),
2248 ],
2249 runner_arguments: vec!["--exact".into(), "full::name".into()],
2250 };
2251 assert_eq!(
2252 nextest_list_invocation(&invocation).unwrap(),
2253 NextestListInvocation {
2254 arguments: vec![
2255 "nextest".into(),
2256 "list".into(),
2257 "--package=fixture".into(),
2258 "--partition".into(),
2259 "hash:1/2".into(),
2260 "name".into(),
2261 "--message-format".into(),
2262 "json".into(),
2263 ],
2264 runner_arguments: vec!["--exact".into(), "full::name".into()],
2265 }
2266 );
2267
2268 let mut rerun = invocation;
2269 rerun.arguments.extend(["--rerun".into(), "latest".into()]);
2270 assert!(
2271 nextest_list_invocation(&rerun)
2272 .unwrap_err()
2273 .to_string()
2274 .contains("cannot yet be assigned exact selected-test identity")
2275 );
2276 }
2277
2278 #[test]
2280 fn a_script_command_splits_the_way_sh_splits_it() {
2281 let words = |text: &str| shell_words(text).unwrap();
2282 assert_eq!(
2283 words("cargo test --workspace"),
2284 ["cargo", "test", "--workspace"]
2285 );
2286 assert_eq!(
2287 words(r#"cargo test --config 'target.x.runner="a b"' -- "two words""#),
2288 [
2289 "cargo",
2290 "test",
2291 "--config",
2292 r#"target.x.runner="a b""#,
2293 "--",
2294 "two words"
2295 ]
2296 );
2297 assert_eq!(
2299 words(r#"cargo test -- '' """#),
2300 ["cargo", "test", "--", "", ""]
2301 );
2302 assert_eq!(
2304 words(r#""C:\work\app" "say \"hi\" \$HOME""#),
2305 [r"C:\work\app", r#"say "hi" $HOME"#]
2306 );
2307 assert_eq!(words(r"a\ b 'c\d'"), ["a b", r"c\d"]);
2309 assert_eq!(words("joined'' x\"\"y"), ["joined", "xy"]);
2310 assert_eq!(
2311 words("cargo test \\\n --workspace \"a\\\nb\""),
2312 ["cargo", "test", "--workspace", "ab"]
2313 );
2314 for broken in ["cargo test 'open", "cargo test \"open", r"cargo test \"] {
2315 assert!(shell_words(broken).is_err(), "{broken}");
2316 }
2317 }
2318
2319 #[test]
2320 fn nextest_list_projection_keeps_short_options_with_attached_values() {
2321 let invocation = CargoTestInvocation {
2322 program: "cargo".into(),
2323 kind: RustCargoCommandKind::NextestRun,
2324 arguments: [
2325 "nextest",
2326 "run",
2327 "-pfixture",
2328 "-Ffast",
2329 "-Etest(=a)",
2330 "-Pci",
2331 "-Zbuild-std",
2332 "-vv",
2333 "name",
2334 ]
2335 .map(String::from)
2336 .to_vec(),
2337 runner_arguments: Vec::new(),
2338 };
2339 let listed = nextest_list_invocation(&invocation).unwrap();
2340 assert_eq!(
2343 listed.arguments,
2344 [
2345 "nextest",
2346 "list",
2347 "-pfixture",
2348 "-Ffast",
2349 "-Etest(=a)",
2350 "-Pci",
2351 "-Zbuild-std",
2352 "-vv",
2353 "name",
2354 "--message-format",
2355 "json",
2356 ]
2357 );
2358 }
2359
2360 #[test]
2361 fn nextest_list_projection_preserves_post_separator_libtest_selection() {
2362 let invocation = cargo_invocation(
2363 Path::new("."),
2364 &[
2365 "cargo".into(),
2366 "nextest".into(),
2367 "run".into(),
2368 "--timings".into(),
2369 "-vv".into(),
2370 "--".into(),
2371 "--include-ignored".into(),
2372 "--skip".into(),
2373 "slow".into(),
2374 "--exact".into(),
2375 "tests::selected".into(),
2376 ],
2377 )
2378 .unwrap();
2379 assert_eq!(
2380 nextest_list_invocation(&invocation).unwrap(),
2381 NextestListInvocation {
2382 arguments: vec![
2383 "nextest".to_owned(),
2384 "list".to_owned(),
2385 "--timings".to_owned(),
2386 "-vv".to_owned(),
2387 "--message-format".to_owned(),
2388 "json".to_owned(),
2389 ],
2390 runner_arguments: vec![
2391 "--include-ignored".to_owned(),
2392 "--skip".to_owned(),
2393 "slow".to_owned(),
2394 "--exact".to_owned(),
2395 "tests::selected".to_owned(),
2396 ],
2397 }
2398 );
2399 }
2400
2401 #[test]
2402 fn nextest_version_handshake_preserves_the_cargo_toolchain_selector() {
2403 let invocation = CargoTestInvocation {
2404 program: "cargo".into(),
2405 kind: RustCargoCommandKind::NextestRun,
2406 arguments: vec![
2407 "+1.95.0".into(),
2408 "nextest".into(),
2409 "run".into(),
2410 "-p".into(),
2411 "fixture".into(),
2412 ],
2413 runner_arguments: Vec::new(),
2414 };
2415 assert_eq!(
2416 nextest_version_arguments(&invocation),
2417 ["+1.95.0", "nextest", "--version"]
2418 );
2419 }
2420
2421 #[test]
2422 fn stock_libtest_presentation_and_scheduling_options_do_not_change_discovery() {
2423 let invocation = CargoTestInvocation {
2424 program: "cargo".into(),
2425 kind: RustCargoCommandKind::CargoTest,
2426 arguments: vec!["test".into(), "cargo-filter".into()],
2427 runner_arguments: [
2428 "runner-filter",
2429 "--nocapture",
2430 "--show-output",
2431 "--format=json",
2432 "--color",
2433 "never",
2434 "--test-threads=4",
2435 "--fail-fast",
2436 "--shuffle-seed",
2437 "17",
2438 "-Zunstable-options",
2439 "--exclude-should-panic",
2440 ]
2441 .into_iter()
2442 .map(str::to_owned)
2443 .collect(),
2444 };
2445 let selection = rust_libtest_selection(&invocation).unwrap();
2446 assert_eq!(
2447 selection.list_arguments,
2448 [
2449 "cargo-filter",
2450 "runner-filter",
2451 "-Zunstable-options",
2452 "--exclude-should-panic"
2453 ]
2454 );
2455 }
2456
2457 #[test]
2458 fn cargo_test_options_are_not_mistaken_for_the_test_name_filter() {
2459 let invocation = CargoTestInvocation {
2460 program: "cargo".into(),
2461 kind: RustCargoCommandKind::CargoTest,
2462 arguments: vec![
2463 "test".into(),
2464 "--manifest-path".into(),
2465 "nested/Cargo.toml".into(),
2466 "--features=one,two".into(),
2467 "needle".into(),
2468 ],
2469 runner_arguments: vec!["--ignored".into(), "other".into()],
2470 };
2471 let selection = rust_libtest_selection(&invocation).unwrap();
2472 assert_eq!(selection.list_arguments, ["needle", "--ignored", "other"]);
2473 }
2474
2475 #[test]
2476 fn libtest_thread_count_is_preserved_as_runner_scheduling() {
2477 for arguments in [vec!["--test-threads", "1"], vec!["--test-threads=8"]] {
2478 let invocation = CargoTestInvocation {
2479 program: "cargo".into(),
2480 kind: RustCargoCommandKind::CargoTest,
2481 arguments: vec!["test".into()],
2482 runner_arguments: arguments.into_iter().map(str::to_owned).collect(),
2483 };
2484 let selection = rust_libtest_selection(&invocation).unwrap();
2485 assert!(selection.list_arguments.is_empty());
2486 }
2487 }
2488
2489 #[test]
2490 fn invalid_or_duplicate_libtest_thread_counts_fail_closed() {
2491 for arguments in [
2492 vec!["--test-threads"],
2493 vec!["--test-threads=0"],
2494 vec!["--test-threads=abc"],
2495 vec!["--test-threads", "1", "--test-threads=2"],
2496 ] {
2497 let invocation = CargoTestInvocation {
2498 program: "cargo".into(),
2499 kind: RustCargoCommandKind::CargoTest,
2500 arguments: vec!["test".into()],
2501 runner_arguments: arguments.into_iter().map(str::to_owned).collect(),
2502 };
2503 assert!(rust_libtest_selection(&invocation).is_err());
2504 }
2505 }
2506
2507 #[test]
2508 fn cargo_target_selection_reproduces_when_cargo_runs_doctests() {
2509 let invocation = CargoTestInvocation {
2510 program: "cargo".into(),
2511 kind: RustCargoCommandKind::CargoTest,
2512 arguments: vec![
2513 "test".into(),
2514 "-p".into(),
2515 "fixture".into(),
2516 "needle".into(),
2517 ],
2518 runner_arguments: vec!["--include-ignored".into()],
2519 };
2520 let selection = rust_cargo_execution_selection(&invocation).unwrap();
2521 assert!(selection.run_libtests);
2522 assert!(selection.run_doctests);
2523 assert_eq!(
2524 selection.doctest_arguments,
2525 [
2526 "test",
2527 "--doc",
2528 "-p",
2529 "fixture",
2530 "needle",
2531 "--",
2532 "--include-ignored"
2533 ]
2534 );
2535
2536 let mut explicit_doc = invocation.clone();
2537 explicit_doc.arguments.insert(1, "--doc".into());
2538 let selection = rust_cargo_execution_selection(&explicit_doc).unwrap();
2539 assert!(!selection.run_libtests);
2540 assert!(selection.run_doctests);
2541
2542 for target in ["--lib", "--tests", "--all-targets", "--example=demo"] {
2543 let mut selected = invocation.clone();
2544 selected.arguments.insert(1, target.into());
2545 let selection = rust_cargo_execution_selection(&selected).unwrap();
2546 assert!(selection.run_libtests);
2547 assert!(!selection.run_doctests);
2548 }
2549 }
2550
2551 #[test]
2552 fn a_module_the_build_never_compiles_leaves_the_denominator() {
2553 let nonce = SystemTime::now()
2557 .duration_since(UNIX_EPOCH)
2558 .unwrap()
2559 .as_nanos();
2560 let root = std::env::temp_dir().join(format!(
2561 "supercov-rust-runner-cfg-{}-{nonce}",
2562 std::process::id()
2563 ));
2564 fs::create_dir_all(root.join("src")).unwrap();
2565 fs::write(
2566 root.join("Cargo.toml"),
2567 "[package]\nname='fixture'\nversion='0.0.0'\nedition='2024'\n\n[features]\nextra=[]\n",
2568 )
2569 .unwrap();
2570 fs::write(
2571 root.join("src/lib.rs"),
2572 r#"
2573#[cfg(feature = "extra")]
2574mod extra;
2575
2576pub fn kept(value: i32) -> i32 {
2577 if value > 0 { value } else { -value }
2578}
2579#[cfg(test)]
2580mod tests {
2581 #[test] fn positive() { assert_eq!(super::kept(2), 2); }
2582 #[test] fn negative() { assert_eq!(super::kept(-2), 2); }
2583}
2584"#,
2585 )
2586 .unwrap();
2587 fs::write(
2588 root.join("src/extra.rs"),
2589 "pub fn never_built(value: i32) -> i32 {\n if value > 0 { 1 } else { 0 }\n}\n",
2590 )
2591 .unwrap();
2592 let project = prepare_rust_project(&root, None).unwrap();
2593 assert!(
2595 project
2596 .source_files
2597 .iter()
2598 .any(|file| file == "src/extra.rs"),
2599 "{:?}",
2600 project.source_files
2601 );
2602 let run = run_prepared_rust_tests(
2603 &project,
2604 &["cargo".into(), "test".into(), "--lib".into()],
2605 "rust-fixture-cfg",
2606 "2026-08-26T00:00:00.000Z",
2607 &mut Vec::new(),
2608 )
2609 .unwrap();
2610 assert_eq!(run.exit_code, 0);
2611 let manifest = &run.request.manifest;
2614 assert!(
2615 manifest
2616 .points
2617 .iter()
2618 .any(|point| point.file == "src/extra.rs"),
2619 "the obligations must stay in the manifest"
2620 );
2621 let declined = manifest.unmeasured.iter().collect::<BTreeSet<_>>();
2622 for point in &manifest.points {
2623 assert_eq!(
2624 declined.contains(&point.id),
2625 point.file == "src/extra.rs",
2626 "{} in {}",
2627 point.id,
2628 point.file
2629 );
2630 }
2631 for decision in &manifest.decisions {
2632 assert_eq!(
2633 declined.contains(&decision.id),
2634 decision.file == "src/extra.rs"
2635 );
2636 }
2637 let report =
2639 crate::frontend_protocol::analyze_frontend_results(&run.declaration, &run.request)
2640 .unwrap();
2641 assert_eq!(
2642 report.view.summary.lines.percentage, 100.0,
2643 "declined obligations must not read as uncovered"
2644 );
2645 fs::remove_dir_all(&root).ok();
2646 }
2647
2648 #[test]
2649 fn assertion_occurrences_are_retained_without_automatic_asserted_credit() {
2650 let nonce = SystemTime::now()
2655 .duration_since(UNIX_EPOCH)
2656 .unwrap()
2657 .as_nanos();
2658 let root = std::env::temp_dir().join(format!(
2659 "supercov-rust-runner-assert-{}-{nonce}",
2660 std::process::id()
2661 ));
2662 fs::create_dir_all(root.join("src")).unwrap();
2663 fs::write(
2664 root.join("Cargo.toml"),
2665 "[package]\nname='fixture'\nversion='0.0.0'\nedition='2024'\n",
2666 )
2667 .unwrap();
2668 fs::write(
2669 root.join("src/lib.rs"),
2670 r#"
2671pub fn checked(value: i32) -> i32 {
2672 value * 2
2673}
2674pub fn unchecked(value: i32) -> i32 {
2675 value + 1
2676}
2677#[cfg(test)]
2678mod tests {
2679 #[test]
2680 fn asserts() {
2681 let doubled = super::checked(2);
2682 assert_eq!(doubled, 4);
2683 }
2684 #[test]
2685 fn asserts_nothing() {
2686 let _ = super::unchecked(1);
2687 }
2688}
2689"#,
2690 )
2691 .unwrap();
2692 let project = prepare_rust_project(&root, None).unwrap();
2693 let run = run_prepared_rust_tests(
2694 &project,
2695 &["cargo".into(), "test".into(), "--lib".into()],
2696 "rust-fixture-assert",
2697 "2026-08-26T00:00:00.000Z",
2698 &mut Vec::new(),
2699 )
2700 .unwrap();
2701 assert_eq!(run.exit_code, 0);
2702
2703 let phases = |test: &str| {
2706 run.request
2707 .raw_results
2708 .iter()
2709 .find(|result| result.test.ends_with(test))
2710 .unwrap_or_else(|| panic!("no test {test}"))
2711 .phases
2712 .clone()
2713 };
2714 let asserting = phases("asserts");
2715 assert!(
2716 !asserting.is_empty(),
2717 "the asserting test recorded no phase"
2718 );
2719 assert!(
2720 asserting
2721 .iter()
2722 .all(|phase| phase.kind == "assertion" && phase.status.as_deref() == Some("passed"))
2723 );
2724 assert!(
2725 phases("asserts_nothing").is_empty(),
2726 "a test that checks nothing witnesses nothing"
2727 );
2728
2729 let report =
2730 crate::frontend_protocol::analyze_frontend_results(&run.declaration, &run.request)
2731 .unwrap();
2732 let level = |line: usize| {
2733 report
2734 .view
2735 .lines
2736 .iter()
2737 .find(|entry| entry.file == "src/lib.rs" && entry.line == line)
2738 .map(|entry| entry.confidence.level.clone())
2739 .unwrap_or_else(|| panic!("no line {line}"))
2740 };
2741 assert_eq!(level(3), "executed");
2743 assert_eq!(level(6), "executed");
2744 fs::remove_dir_all(&root).ok();
2745 }
2746
2747 #[test]
2748 fn libtest_filters_select_the_tests_plain_cargo_would_run() {
2749 let nonce = SystemTime::now()
2753 .duration_since(UNIX_EPOCH)
2754 .unwrap()
2755 .as_nanos();
2756 let root = std::env::temp_dir().join(format!(
2757 "supercov-rust-runner-filters-{}-{nonce}",
2758 std::process::id()
2759 ));
2760 fs::create_dir_all(root.join("src")).unwrap();
2761 fs::write(
2762 root.join("Cargo.toml"),
2763 "[package]\nname='fixture'\nversion='0.0.0'\nedition='2024'\n",
2764 )
2765 .unwrap();
2766 fs::write(
2767 root.join("src/lib.rs"),
2768 r#"
2769pub fn value(flag: bool) -> i32 { if flag { 1 } else { 0 } }
2770#[cfg(test)]
2771mod tests {
2772 #[test] fn alpha() { assert_eq!(super::value(true), 1); }
2773 #[test] fn beta() { assert_eq!(super::value(false), 0); }
2774 #[test] #[ignore] fn gamma_slow() { assert_eq!(super::value(true), 1); }
2775}
2776"#,
2777 )
2778 .unwrap();
2779 let project = prepare_rust_project(&root, None).unwrap();
2780 let names = |run: &str, command: &[&str]| {
2781 let run = run_prepared_rust_tests(
2782 &project,
2783 &command
2784 .iter()
2785 .map(|word| (*word).into())
2786 .collect::<Vec<String>>(),
2787 run,
2788 "2026-08-26T00:00:00.000Z",
2789 &mut Vec::new(),
2790 )
2791 .unwrap();
2792 assert_eq!(run.exit_code, 0, "{:?}", run.request.raw_results);
2793 let mut names = run
2794 .request
2795 .raw_results
2796 .iter()
2797 .map(|result| {
2798 (
2799 result.test.rsplit("::").next().unwrap().to_owned(),
2800 result.status.clone().unwrap(),
2801 )
2802 })
2803 .collect::<Vec<_>>();
2804 names.sort();
2805 names
2806 };
2807 assert_eq!(
2809 names("rust-fixture-filters-1", &["cargo", "test", "--lib"]),
2810 [
2811 ("alpha".to_owned(), "passed".to_owned()),
2812 ("beta".to_owned(), "passed".to_owned()),
2813 ("gamma_slow".to_owned(), "skipped".to_owned()),
2814 ]
2815 );
2816 assert_eq!(
2818 names(
2819 "rust-fixture-filters-2",
2820 &["cargo", "test", "--lib", "alpha"]
2821 ),
2822 [("alpha".to_owned(), "passed".to_owned())]
2823 );
2824 assert_eq!(
2825 names(
2826 "rust-fixture-filters-3",
2827 &["cargo", "test", "--lib", "--", "--skip", "beta"]
2828 ),
2829 [
2830 ("alpha".to_owned(), "passed".to_owned()),
2831 ("gamma_slow".to_owned(), "skipped".to_owned()),
2832 ]
2833 );
2834 assert_eq!(
2836 names(
2837 "rust-fixture-filters-4",
2838 &["cargo", "test", "--lib", "--", "--ignored"]
2839 ),
2840 [("gamma_slow".to_owned(), "passed".to_owned())]
2841 );
2842 fs::remove_dir_all(&root).ok();
2843 }
2844
2845 #[test]
2846 fn libtest_processes_run_in_their_package_directory() {
2847 let nonce = SystemTime::now()
2851 .duration_since(UNIX_EPOCH)
2852 .unwrap()
2853 .as_nanos();
2854 let root = std::env::temp_dir().join(format!(
2855 "supercov-rust-runner-cwd-{}-{nonce}",
2856 std::process::id()
2857 ));
2858 fs::create_dir_all(root.join("member/src")).unwrap();
2859 fs::write(
2860 root.join("Cargo.toml"),
2861 "[workspace]\nmembers = ['member']\nresolver = '2'\n",
2862 )
2863 .unwrap();
2864 fs::write(
2865 root.join("member/Cargo.toml"),
2866 "[package]\nname='member'\nversion='0.0.0'\nedition='2024'\n",
2867 )
2868 .unwrap();
2869 fs::write(
2870 root.join("member/src/lib.rs"),
2871 r#"
2872pub fn manifest() -> String {
2873 std::fs::read_to_string("Cargo.toml").unwrap()
2874}
2875#[cfg(test)]
2876mod tests {
2877 #[test] fn reads_own_manifest() { assert!(super::manifest().contains("name='member'")); }
2878}
2879"#,
2880 )
2881 .unwrap();
2882 let project = prepare_rust_project(&root, None).unwrap();
2883 let run = run_prepared_rust_tests(
2884 &project,
2885 &["cargo".into(), "test".into(), "--lib".into()],
2886 "rust-fixture-cwd",
2887 "2026-08-26T00:00:00.000Z",
2888 &mut Vec::new(),
2889 )
2890 .unwrap();
2891 assert_eq!(run.exit_code, 0, "{:?}", run.request.raw_results);
2892 assert_eq!(run.request.raw_results.len(), 1);
2893 assert_eq!(run.request.raw_results[0].status.as_deref(), Some("passed"));
2894 fs::remove_dir_all(&root).ok();
2895 }
2896
2897 #[test]
2898 fn cargo_libtest_runs_produce_queryable_owned_evidence() {
2899 let nonce = SystemTime::now()
2900 .duration_since(UNIX_EPOCH)
2901 .unwrap()
2902 .as_nanos();
2903 let root = std::env::temp_dir().join(format!(
2904 "supercov-rust-runner-{}-{nonce}",
2905 std::process::id()
2906 ));
2907 fs::create_dir_all(root.join("src")).unwrap();
2908 fs::write(
2909 root.join("Cargo.toml"),
2910 "[package]\nname='fixture'\nversion='0.0.0'\nedition='2024'\n",
2911 )
2912 .unwrap();
2913 fs::write(
2914 root.join("src/lib.rs"),
2915 r#"
2916pub fn choose(left: bool, right: bool) -> i32 {
2917 if left && right { 1 } else { 0 }
2918}
2919pub fn pick(value: i32) -> &'static str {
2920 match value {
2921 0 => "zero",
2922 1 => "one",
2923 _ => "many",
2924 }
2925}
2926pub fn total(values: &[i32]) -> i32 {
2927 let mut sum = 0;
2928 for value in values {
2929 sum += value;
2930 }
2931 sum
2932}
2933pub fn first_even(values: &[i32]) -> Option<i32> {
2934 let mut index = 0;
2935 while index < values.len() {
2936 if values[index] % 2 == 0 {
2937 return Some(values[index]);
2938 }
2939 index += 1;
2940 }
2941 None
2942}
2943pub fn parse_twice(text: &str) -> Option<i32> {
2944 let value: i32 = text.parse().ok()?;
2945 Some(value * 2)
2946}
2947pub fn describe(value: Option<i32>, flag: bool) -> &'static str {
2948 if let Some(inner) = value && inner > 0 && flag {
2949 "positive"
2950 } else {
2951 "other"
2952 }
2953}
2954pub fn depth(n: u32) -> Result<u32, String> {
2955 if n == 0 {
2956 Ok(0)
2957 } else {
2958 let below = depth(n - 1)?;
2959 Ok(below + 1)
2960 }
2961}
2962#[cfg(test)]
2963mod tests {
2964 #[test] fn false_path() { assert_eq!(super::choose(false, true), 0); }
2965 #[test] fn true_path() { assert_eq!(super::choose(true, true), 1); }
2966 #[test] #[ignore] fn ignored_path() { unreachable!(); }
2967 #[test] fn pick_zero() { assert_eq!(super::pick(0), "zero"); }
2968 #[test] fn pick_many() { assert_eq!(super::pick(7), "many"); }
2969 #[test] fn total_empty() { assert_eq!(super::total(&[]), 0); }
2970 #[test] fn total_some() { assert_eq!(super::total(&[1, 2]), 3); }
2971 #[test] fn first_even_empty() { assert_eq!(super::first_even(&[]), None); }
2972 #[test] fn first_even_found() { assert_eq!(super::first_even(&[1, 4]), Some(4)); }
2973 #[test] fn parse_ok() { assert_eq!(super::parse_twice("4"), Some(8)); }
2974 #[test] fn parse_bad() { assert_eq!(super::parse_twice("x"), None); }
2975 #[test] fn chain_taken() { assert_eq!(super::describe(Some(1), true), "positive"); }
2976 #[test] fn chain_pattern_fails() { assert_eq!(super::describe(None, true), "other"); }
2977 #[test] fn chain_negative() { assert_eq!(super::describe(Some(-1), true), "other"); }
2978 #[test] fn chain_flag_fails() { assert_eq!(super::describe(Some(1), false), "other"); }
2979 #[test] fn deep_recursion() { assert_eq!(super::depth(5_000), Ok(5_000)); }
2980}
2981"#,
2982 )
2983 .unwrap();
2984 let project = prepare_rust_project(&root, None).unwrap();
2985 let run = run_prepared_rust_tests(
2989 &project,
2990 &["cargo".into(), "test".into(), "--lib".into()],
2991 "rust-fixture",
2992 "2026-08-26T00:00:00.000Z",
2993 &mut Vec::new(),
2994 )
2995 .unwrap();
2996 assert_eq!(run.exit_code, 0);
2997 assert_eq!(run.request.raw_results.len(), 16);
2998 let statuses = run
2999 .request
3000 .raw_results
3001 .iter()
3002 .filter_map(|result| result.status.as_deref())
3003 .collect::<Vec<_>>();
3004 assert_eq!(
3005 statuses
3006 .iter()
3007 .filter(|status| **status == "skipped")
3008 .count(),
3009 1
3010 );
3011 assert_eq!(
3012 statuses
3013 .iter()
3014 .filter(|status| **status == "passed")
3015 .count(),
3016 15
3017 );
3018
3019 let chain_vectors = |test: &str| {
3022 let result = run
3023 .request
3024 .raw_results
3025 .iter()
3026 .find(|result| result.test.ends_with(test))
3027 .unwrap_or_else(|| panic!("no test {test}"));
3028 let snapshot = result
3029 .runtime
3030 .iter()
3031 .flat_map(|snapshot| &snapshot.decisions)
3032 .find(|decision| decision.meta.source.starts_with("let Some(inner) = value"))
3033 .unwrap_or_else(|| panic!("{test} recorded no chain decision"));
3034 snapshot
3035 .vectors
3036 .iter()
3037 .map(|vector| (vector.values.clone(), vector.outcome))
3038 .collect::<Vec<_>>()
3039 };
3040 assert_eq!(
3041 chain_vectors("chain_taken"),
3042 [(vec![Some(true), Some(true), Some(true)], true)]
3043 );
3044 assert_eq!(
3045 chain_vectors("chain_pattern_fails"),
3046 [(vec![Some(false), None, None], false)]
3047 );
3048 assert_eq!(
3049 chain_vectors("chain_negative"),
3050 [(vec![Some(true), Some(false), None], false)]
3051 );
3052 assert_eq!(
3053 chain_vectors("chain_flag_fails"),
3054 [(vec![Some(true), Some(true), Some(false)], false)]
3055 );
3056 validate_frontend_report_request(&run.declaration, &run.request).unwrap();
3057 let archive = root.join("evidence.raw.gz");
3058 write_archive(run.archive_entries().unwrap(), &archive).unwrap();
3059 let report = analyze_coverage_archive(&ArchiveReportRequest {
3060 archive_path: archive,
3061 run_id: "rust-fixture".into(),
3062 generated_at: "2026-08-26T00:00:00.000Z".into(),
3063 integrity: None,
3064 test_exit_code: ExitCodeInput::Present(Some(0)),
3065 test_filter: None,
3066 })
3067 .unwrap();
3068 assert_eq!(report.view.tests.len(), 16);
3069 assert!(report.view.summary.lines.covered > 0);
3070 assert!(report.view.summary.decisions > 0);
3071
3072 let single = |kind: &str| {
3075 let mut found = report
3076 .view
3077 .branches
3078 .iter()
3079 .filter(|branch| branch.meta.kind == kind);
3080 let branch = found.next().unwrap_or_else(|| panic!("no {kind} branch"));
3081 assert!(found.next().is_none(), "more than one {kind} branch");
3082 branch
3083 };
3084 let tests_of = |branch: &crate::coverage_report::BranchResult, label: &str| {
3085 branch
3086 .alternatives
3087 .iter()
3088 .find(|alternative| alternative.label == label)
3089 .unwrap_or_else(|| panic!("{} has no alternative {label}", branch.meta.kind))
3090 .tests
3091 .clone()
3092 };
3093 let for_loop = single("for-loop");
3094 assert_eq!(
3095 tests_of(for_loop, "zero iterations"),
3096 ["src/lib.rs::tests::total_empty"]
3097 );
3098 assert_eq!(
3099 tests_of(for_loop, "entered"),
3100 ["src/lib.rs::tests::total_some"]
3101 );
3102 let while_loop = single("while-loop");
3103 assert_eq!(
3104 tests_of(while_loop, "zero iterations"),
3105 ["src/lib.rs::tests::first_even_empty"]
3106 );
3107 assert_eq!(
3108 tests_of(while_loop, "entered"),
3109 ["src/lib.rs::tests::first_even_found"]
3110 );
3111 let try_operator = report
3114 .view
3115 .branches
3116 .iter()
3117 .find(|branch| {
3118 branch.meta.kind == "try-operator" && branch.meta.source.contains("parse().ok()")
3119 })
3120 .expect("parse_twice's try operator");
3121 assert_eq!(
3122 tests_of(try_operator, "continued"),
3123 ["src/lib.rs::tests::parse_ok"]
3124 );
3125 assert_eq!(
3126 tests_of(try_operator, "early return"),
3127 ["src/lib.rs::tests::parse_bad"]
3128 );
3129 let mut logical = report
3130 .view
3131 .branches
3132 .iter()
3133 .filter(|branch| branch.meta.kind == "logical-and")
3134 .collect::<Vec<_>>();
3135 logical.sort_by_key(|branch| (branch.meta.line, branch.meta.column));
3136 assert_eq!(logical.len(), 3);
3138 assert_eq!(
3139 tests_of(logical[0], "short-circuited"),
3140 ["src/lib.rs::tests::false_path"]
3141 );
3142 assert_eq!(
3143 tests_of(logical[0], "right operand evaluated"),
3144 ["src/lib.rs::tests::true_path"]
3145 );
3146 assert_eq!(
3147 tests_of(logical[1], "short-circuited"),
3148 ["src/lib.rs::tests::chain_pattern_fails"]
3149 );
3150 assert_eq!(
3151 tests_of(logical[1], "right operand evaluated"),
3152 [
3153 "src/lib.rs::tests::chain_flag_fails",
3154 "src/lib.rs::tests::chain_negative",
3155 "src/lib.rs::tests::chain_taken",
3156 ]
3157 );
3158 assert_eq!(
3159 tests_of(logical[2], "short-circuited"),
3160 [
3161 "src/lib.rs::tests::chain_negative",
3162 "src/lib.rs::tests::chain_pattern_fails",
3163 ]
3164 );
3165 assert_eq!(
3166 tests_of(logical[2], "right operand evaluated"),
3167 [
3168 "src/lib.rs::tests::chain_flag_fails",
3169 "src/lib.rs::tests::chain_taken",
3170 ]
3171 );
3172 assert!(for_loop.covered && while_loop.covered && try_operator.covered);
3173 assert!(logical.iter().all(|branch| branch.covered));
3174
3175 let mut arms = report
3178 .view
3179 .branches
3180 .iter()
3181 .filter(|branch| branch.meta.kind == "match-arm")
3182 .collect::<Vec<_>>();
3183 arms.sort_by_key(|branch| branch.meta.line);
3184 assert_eq!(arms.len(), 3);
3185 let alternative = |arm: usize, label: &str| {
3186 arms[arm]
3187 .alternatives
3188 .iter()
3189 .find(|alternative| alternative.label == label)
3190 .unwrap_or_else(|| panic!("arm {arm} has no alternative {label}"))
3191 };
3192 assert_eq!(
3193 alternative(0, "selected").tests,
3194 ["src/lib.rs::tests::pick_zero"]
3195 );
3196 assert_eq!(
3197 alternative(0, "not selected").tests,
3198 ["src/lib.rs::tests::pick_many"]
3199 );
3200 assert!(!alternative(1, "selected").covered);
3201 assert_eq!(
3202 alternative(1, "not selected").tests,
3203 ["src/lib.rs::tests::pick_many"]
3204 );
3205 assert_eq!(arms[2].alternatives.len(), 1);
3206 assert_eq!(
3207 alternative(2, "selected").tests,
3208 ["src/lib.rs::tests::pick_many"]
3209 );
3210 assert!(arms[0].covered && !arms[1].covered && arms[2].covered);
3211
3212 fs::remove_dir_all(root).unwrap();
3213 }
3214}