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