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, CoverageReportRequest, DecisionMeta,
36 DecisionSnapshot, ExecutionScope, ExitCodeInput, PersistedCoverageModel, RawTestResult,
37 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) -> Option<usize> {
266 match self.kind {
267 RustCargoCommandKind::CargoTest => self
268 .arguments
269 .iter()
270 .position(|argument| argument == "test"),
271 RustCargoCommandKind::NextestRun => self
272 .arguments
273 .windows(2)
274 .position(|pair| pair == ["nextest", "run"]),
275 }
276 }
277}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
280pub(crate) struct RustLibtestSelection {
281 pub list_arguments: Vec<String>,
282}
283
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub(crate) struct RustCargoExecutionSelection {
286 pub run_libtests: bool,
287 pub run_doctests: bool,
288 pub doctest_arguments: Vec<String>,
289}
290
291#[derive(Debug, Clone, PartialEq, Eq)]
292pub(crate) struct NextestListInvocation {
293 pub arguments: Vec<String>,
294 pub runner_arguments: Vec<String>,
295}
296
297pub(crate) fn nextest_version_arguments(
298 invocation: &CargoTestInvocation,
299) -> Result<Vec<String>, RustTestRunnerError> {
300 if invocation.kind != RustCargoCommandKind::NextestRun {
301 return Err(RustTestRunnerError::UnsupportedCommand(
302 "a nextest version handshake requires `cargo nextest run`".into(),
303 ));
304 }
305 let command = invocation.command_position().ok_or_else(|| {
306 RustTestRunnerError::UnsupportedCommand(
307 "the expanded Cargo invocation lost its nextest run subcommand".into(),
308 )
309 })?;
310 let mut arguments = invocation.arguments[..command].to_vec();
311 arguments.extend(["nextest".into(), "--version".into()]);
312 Ok(arguments)
313}
314
315fn nextest_run_only_option(argument: &str) -> Option<bool> {
316 let name = argument.split_once('=').map_or(argument, |(name, _)| name);
317 match name {
318 "-j"
319 | "--jobs"
320 | "--test-threads"
321 | "--retries"
322 | "--flaky-result"
323 | "--max-fail"
324 | "--no-tests"
325 | "--failure-output"
326 | "--success-output"
327 | "--status-level"
328 | "--final-status-level"
329 | "--show-progress"
330 | "--max-progress-running"
331 | "--message-format"
332 | "--message-format-version" => Some(!argument.contains('=')),
333 "--fail-fast"
334 | "--ff"
335 | "--no-fail-fast"
336 | "--nff"
337 | "--no-capture"
338 | "--nocapture"
339 | "--no-output-indent"
340 | "--hide-progress-bar"
341 | "--no-input-handler" => Some(false),
342 _ if argument.starts_with("-j") && argument.len() > 2 => Some(false),
343 _ => None,
344 }
345}
346
347fn nextest_unsupported_run_option(argument: &str) -> Option<bool> {
348 let name = argument.split_once('=').map_or(argument, |(name, _)| name);
349 match name {
350 "-R"
351 | "--rerun"
352 | "--debugger"
353 | "--tracer"
354 | "--stress-count"
355 | "--stress-duration"
356 | "--archive-file"
357 | "--archive-format"
358 | "--extract-to"
359 | "--cargo-metadata"
360 | "--workspace-remap"
361 | "--binaries-metadata"
362 | "--target-dir-remap"
363 | "--build-dir-remap" => Some(!argument.contains('=')),
364 "--no-run" | "--extract-overwrite" | "--persist-extract-tempdir" => Some(false),
365 _ => None,
366 }
367}
368
369fn nextest_shared_option(argument: &str) -> Option<bool> {
370 let name = argument.split_once('=').map_or(argument, |(name, _)| name);
371 match name {
372 "--color"
373 | "-p"
374 | "--package"
375 | "--exclude"
376 | "--bin"
377 | "--example"
378 | "--test"
379 | "--bench"
380 | "-F"
381 | "--features"
382 | "--build-jobs"
383 | "--cargo-profile"
384 | "--target"
385 | "--target-dir"
386 | "--cargo-message-format"
387 | "--config"
388 | "--timings"
389 | "-Z"
390 | "--run-ignored"
391 | "--partition"
392 | "--platform-filter"
393 | "-E"
394 | "--filterset"
395 | "--filter-expr"
396 | "--manifest-path"
397 | "--config-file"
398 | "--user-config-file"
399 | "--tool-config-file"
400 | "-P"
401 | "--profile" => Some(!argument.contains('=')),
402 "--no-pager"
403 | "-v"
404 | "--verbose"
405 | "--workspace"
406 | "--all"
407 | "--lib"
408 | "--bins"
409 | "--examples"
410 | "--tests"
411 | "--benches"
412 | "--all-targets"
413 | "--all-features"
414 | "--no-default-features"
415 | "-r"
416 | "--release"
417 | "--unit-graph"
418 | "--frozen"
419 | "--locked"
420 | "--offline"
421 | "--cargo-quiet"
422 | "--cargo-verbose"
423 | "--ignore-rust-version"
424 | "--future-incompat-report"
425 | "--ignore-default-filter"
426 | "--override-version-check" => Some(false),
427 _ if argument.starts_with("-p") && argument.len() > 2 => Some(false),
428 _ if argument.starts_with("-F") && argument.len() > 2 => Some(false),
429 _ if argument.starts_with("-E") && argument.len() > 2 => Some(false),
430 _ if argument.starts_with("-P") && argument.len() > 2 => Some(false),
431 _ if argument.starts_with("-Z") && argument.len() > 2 => Some(false),
432 _ if argument.len() > 2
433 && argument.starts_with('-')
434 && argument[1..].bytes().all(|byte| byte == b'v') =>
435 {
436 Some(false)
437 }
438 _ if argument.starts_with("--timings=") => Some(false),
439 _ => None,
440 }
441}
442
443pub(crate) fn nextest_list_invocation(
449 invocation: &CargoTestInvocation,
450) -> Result<NextestListInvocation, RustTestRunnerError> {
451 if invocation.kind != RustCargoCommandKind::NextestRun {
452 return Err(RustTestRunnerError::UnsupportedCommand(
453 "a nextest list projection requires `cargo nextest run`".into(),
454 ));
455 }
456 let command = invocation.command_position().ok_or_else(|| {
457 RustTestRunnerError::UnsupportedCommand(
458 "the expanded Cargo invocation lost its nextest run subcommand".into(),
459 )
460 })?;
461 let mut arguments = invocation.arguments[..command].to_vec();
462 arguments.extend(["nextest".into(), "list".into()]);
463 let mut index = command + 2;
464 while index < invocation.arguments.len() {
465 let argument = &invocation.arguments[index];
466 if argument == "--" {
467 arguments.extend(invocation.arguments[index..].iter().cloned());
468 break;
469 }
470 if let Some(takes_value) = nextest_unsupported_run_option(argument) {
471 if takes_value && invocation.arguments.get(index + 1).is_none() {
472 return Err(RustTestRunnerError::UnsupportedCommand(format!(
473 "nextest option {argument} has no value"
474 )));
475 }
476 return Err(RustTestRunnerError::UnsupportedCommand(format!(
477 "nextest option {argument} cannot yet be assigned exact selected-test identity"
478 )));
479 }
480 if let Some(takes_value) = nextest_run_only_option(argument) {
481 if takes_value {
482 index += 1;
483 if index == invocation.arguments.len() {
484 return Err(RustTestRunnerError::UnsupportedCommand(format!(
485 "nextest option {argument} has no value"
486 )));
487 }
488 }
489 } else if let Some(takes_value) = nextest_shared_option(argument) {
490 arguments.push(argument.clone());
491 if takes_value {
492 index += 1;
493 let value = invocation.arguments.get(index).ok_or_else(|| {
494 RustTestRunnerError::UnsupportedCommand(format!(
495 "nextest option {argument} has no value"
496 ))
497 })?;
498 arguments.push(value.clone());
499 }
500 } else if argument.starts_with('-') {
501 return Err(RustTestRunnerError::UnsupportedCommand(format!(
502 "the pinned nextest run contract does not recognize option {argument}"
503 )));
504 } else {
505 arguments.push(argument.clone());
506 }
507 index += 1;
508 }
509 arguments.extend(["--message-format".into(), "json".into()]);
510 Ok(NextestListInvocation {
511 arguments,
512 runner_arguments: invocation.runner_arguments.clone(),
513 })
514}
515
516pub(crate) fn cargo_invocation(
517 root: &Path,
518 command: &[String],
519) -> Result<CargoTestInvocation, RustTestRunnerError> {
520 let words = if command.iter().any(|word| executable_name(word) == "cargo") {
525 command.to_vec()
526 } else {
527 let expanded = crate::project_discovery::expanded_command(root, command);
528 shell_words(&expanded)?
529 };
530 let cargo = words
531 .iter()
532 .position(|word| executable_name(word) == "cargo")
533 .ok_or_else(|| RustTestRunnerError::UnsupportedCommand(
534 "Rust was detected, but the expanded command does not expose a stable Cargo invocation".into(),
535 ))?;
536 let cargo_test = words[cargo + 1..]
537 .iter()
538 .position(|word| word == "test")
539 .map(|position| cargo + 1 + position);
540 let nextest = words[cargo + 1..]
541 .windows(2)
542 .position(|pair| pair == ["nextest", "run"])
543 .map(|position| cargo + 1 + position);
544 let (kind, command) = match (cargo_test, nextest) {
545 (Some(test), None) => (RustCargoCommandKind::CargoTest, test),
546 (None, Some(nextest)) => (RustCargoCommandKind::NextestRun, nextest),
547 (Some(_), Some(_)) => {
548 return Err(RustTestRunnerError::UnsupportedCommand(
549 "the Cargo invocation ambiguously contains both test and nextest run".into(),
550 ));
551 }
552 (None, None) => {
553 return Err(RustTestRunnerError::UnsupportedCommand(
554 "the owned Rust runner currently requires `cargo test` or `cargo nextest run`; cross remains explicitly unsupported"
555 .into(),
556 ));
557 }
558 };
559 if words[cargo + 1..command]
560 .iter()
561 .any(|word| matches!(word.as_str(), "&&" | "||" | ";" | "|"))
562 {
563 return Err(RustTestRunnerError::UnsupportedCommand(
564 "the Cargo invocation contains a shell boundary before `test`".into(),
565 ));
566 }
567 let command_end = command
568 + if kind == RustCargoCommandKind::NextestRun {
569 1
570 } else {
571 0
572 };
573 let mut arguments = words[cargo + 1..=command_end].to_vec();
574 let mut runner_arguments = Vec::new();
575 let mut after_separator = false;
576 for argument in &words[command_end + 1..] {
577 if argument == "--" && !after_separator {
578 after_separator = true;
579 continue;
580 }
581 if matches!(argument.as_str(), "&&" | "||" | ";" | "|") {
582 return Err(RustTestRunnerError::UnsupportedCommand(
583 "the Cargo test command contains an unsupported shell boundary".into(),
584 ));
585 }
586 if after_separator {
587 runner_arguments.push(argument.clone());
588 } else {
589 arguments.push(argument.clone());
590 }
591 }
592 Ok(CargoTestInvocation {
593 program: words[cargo].clone(),
594 kind,
595 arguments,
596 runner_arguments,
597 })
598}
599
600fn cargo_option_takes_value(argument: &str) -> Option<bool> {
601 let name = argument.split_once('=').map_or(argument, |(name, _)| name);
602 match name {
603 "-p" | "--package" | "--exclude" | "--bin" | "--example" | "--test" | "--bench" | "-F"
604 | "--features" | "-j" | "--jobs" | "--profile" | "--target" | "--target-dir"
605 | "--message-format" | "--color" | "--config" | "-Z" | "--manifest-path" => {
606 Some(!argument.contains('='))
607 }
608 "--no-run"
609 | "--no-fail-fast"
610 | "--future-incompat-report"
611 | "-q"
612 | "--quiet"
613 | "-v"
614 | "--verbose"
615 | "--workspace"
616 | "--all"
617 | "--lib"
618 | "--bins"
619 | "--examples"
620 | "--tests"
621 | "--benches"
622 | "--all-targets"
623 | "--doc"
624 | "--all-features"
625 | "--no-default-features"
626 | "-r"
627 | "--release"
628 | "--timings"
629 | "--ignore-rust-version"
630 | "--locked"
631 | "--offline"
632 | "--frozen" => Some(false),
633 _ if argument.starts_with("-vv") => Some(false),
634 _ if argument.starts_with("-p") && argument.len() > 2 => Some(false),
635 _ if argument.starts_with("-F") && argument.len() > 2 => Some(false),
636 _ if argument.starts_with("-j") && argument.len() > 2 => Some(false),
637 _ => None,
638 }
639}
640
641pub(crate) fn rust_libtest_selection(
642 invocation: &CargoTestInvocation,
643) -> Result<RustLibtestSelection, RustTestRunnerError> {
644 if invocation.kind != RustCargoCommandKind::CargoTest {
645 return Err(RustTestRunnerError::UnsupportedCommand(
646 "libtest selection cannot be reconstructed from a nextest command".into(),
647 ));
648 }
649 let test = invocation
650 .arguments
651 .iter()
652 .position(|argument| argument == "test")
653 .ok_or_else(|| {
654 RustTestRunnerError::UnsupportedCommand(
655 "the expanded Cargo invocation lost its test subcommand".into(),
656 )
657 })?;
658 let mut cargo_filter = None;
659 let mut index = test + 1;
660 while index < invocation.arguments.len() {
661 let argument = &invocation.arguments[index];
662 if argument.starts_with('-') {
663 let takes_value = cargo_option_takes_value(argument).ok_or_else(|| {
664 RustTestRunnerError::UnsupportedCommand(format!(
665 "the pinned Cargo test contract does not recognize option {argument}"
666 ))
667 })?;
668 if takes_value {
669 index += 1;
670 if index == invocation.arguments.len() {
671 return Err(RustTestRunnerError::UnsupportedCommand(format!(
672 "Cargo option {argument} has no value"
673 )));
674 }
675 }
676 } else if cargo_filter.replace(argument.clone()).is_some() {
677 return Err(RustTestRunnerError::UnsupportedCommand(
678 "Cargo test has more than one pre-separator TESTNAME".into(),
679 ));
680 }
681 index += 1;
682 }
683
684 let mut list_arguments = cargo_filter.into_iter().collect::<Vec<_>>();
685 let mut test_threads = None;
686 let mut index = 0;
687 while index < invocation.runner_arguments.len() {
688 let argument = &invocation.runner_arguments[index];
689 match argument.as_str() {
690 "--ignored" | "--include-ignored" | "--exclude-should-panic" | "--test" | "--bench" => {
691 list_arguments.push(argument.clone());
692 }
693 "--exact" => list_arguments.push(argument.clone()),
694 "--skip" => {
695 let value = invocation.runner_arguments.get(index + 1).ok_or_else(|| {
696 RustTestRunnerError::UnsupportedCommand(
697 "libtest --skip has no filter value".into(),
698 )
699 })?;
700 list_arguments.extend([argument.clone(), value.clone()]);
701 index += 1;
702 }
703 _ if argument.starts_with("--skip=") && argument.len() > "--skip=".len() => {
704 list_arguments.push(argument.clone());
705 }
706 "--test-threads" => {
707 let value = invocation.runner_arguments.get(index + 1).ok_or_else(|| {
708 RustTestRunnerError::UnsupportedCommand(
709 "libtest --test-threads has no value".into(),
710 )
711 })?;
712 let parsed = parse_libtest_threads(value)?;
713 if test_threads.replace(parsed).is_some() {
714 return Err(RustTestRunnerError::UnsupportedCommand(
715 "libtest --test-threads was provided more than once".into(),
716 ));
717 }
718 index += 1;
719 }
720 _ if argument.starts_with("--test-threads=") => {
721 let value = &argument["--test-threads=".len()..];
722 let parsed = parse_libtest_threads(value)?;
723 if test_threads.replace(parsed).is_some() {
724 return Err(RustTestRunnerError::UnsupportedCommand(
725 "libtest --test-threads was provided more than once".into(),
726 ));
727 }
728 }
729 "-Z" => {
730 let value = invocation.runner_arguments.get(index + 1).ok_or_else(|| {
731 RustTestRunnerError::UnsupportedCommand(
732 "libtest -Z has no feature value".into(),
733 )
734 })?;
735 list_arguments.extend([argument.clone(), value.clone()]);
740 index += 1;
741 }
742 _ if argument.starts_with("-Z") && argument.len() > 2 => {
743 list_arguments.push(argument.clone());
744 }
745 "--logfile" | "--color" | "--format" | "--shuffle-seed" => {
746 if invocation.runner_arguments.get(index + 1).is_none() {
747 return Err(RustTestRunnerError::UnsupportedCommand(format!(
748 "libtest {argument} has no value"
749 )));
750 }
751 index += 1;
755 }
756 _ if ["--logfile=", "--color=", "--format=", "--shuffle-seed="]
757 .iter()
758 .any(|prefix| argument.starts_with(prefix) && argument.len() > prefix.len()) => {}
759 "--force-run-in-process"
760 | "--fail-fast"
761 | "--no-capture"
762 | "--nocapture"
763 | "-q"
764 | "--quiet"
765 | "--show-output"
766 | "--report-time"
767 | "--ensure-time"
768 | "--shuffle" => {
769 }
773 "--list" | "-h" | "--help" => {
774 return Err(RustTestRunnerError::UnsupportedCommand(format!(
775 "libtest {argument} does not execute a test suite; exact non-execution mode support is not implemented"
776 )));
777 }
778 _ if !argument.starts_with('-') => list_arguments.push(argument.clone()),
779 _ => {
780 return Err(RustTestRunnerError::UnsupportedCommand(format!(
781 "the pinned Rust 1.95 libtest discovery contract does not recognize option {argument}"
782 )));
783 }
784 }
785 index += 1;
786 }
787 Ok(RustLibtestSelection { list_arguments })
788}
789
790fn parse_libtest_threads(value: &str) -> Result<usize, RustTestRunnerError> {
791 match value.parse::<usize>() {
792 Ok(0) => Err(RustTestRunnerError::UnsupportedCommand(
793 "argument for --test-threads must not be 0".into(),
794 )),
795 Ok(value) => Ok(value),
796 Err(error) => Err(RustTestRunnerError::UnsupportedCommand(format!(
797 "argument for --test-threads must be a number > 0 (error: {error})"
798 ))),
799 }
800}
801
802pub(crate) fn rust_cargo_execution_selection(
803 invocation: &CargoTestInvocation,
804) -> Result<RustCargoExecutionSelection, RustTestRunnerError> {
805 if invocation.kind == RustCargoCommandKind::NextestRun {
806 return Ok(RustCargoExecutionSelection {
807 run_libtests: true,
808 run_doctests: false,
809 doctest_arguments: Vec::new(),
810 });
811 }
812 let test = invocation
813 .arguments
814 .iter()
815 .position(|argument| argument == "test")
816 .ok_or_else(|| {
817 RustTestRunnerError::UnsupportedCommand(
818 "the expanded Cargo invocation lost its test subcommand".into(),
819 )
820 })?;
821 let mut doc = false;
822 let mut other_target = false;
823 let mut index = test + 1;
824 while index < invocation.arguments.len() {
825 let argument = &invocation.arguments[index];
826 let name = argument
827 .split_once('=')
828 .map_or(argument.as_str(), |(name, _)| name);
829 match name {
830 "--doc" => doc = true,
831 "--lib" | "--bins" | "--bin" | "--examples" | "--example" | "--tests" | "--test"
832 | "--benches" | "--bench" | "--all-targets" => other_target = true,
833 _ => {}
834 }
835 if argument.starts_with('-') {
836 let takes_value = cargo_option_takes_value(argument).ok_or_else(|| {
837 RustTestRunnerError::UnsupportedCommand(format!(
838 "the pinned Cargo test contract does not recognize option {argument}"
839 ))
840 })?;
841 if takes_value {
842 index += 1;
843 if index == invocation.arguments.len() {
844 return Err(RustTestRunnerError::UnsupportedCommand(format!(
845 "Cargo option {argument} has no value"
846 )));
847 }
848 }
849 }
850 index += 1;
851 }
852 if doc && other_target {
853 return Err(RustTestRunnerError::UnsupportedCommand(
854 "Cargo --doc cannot be combined with another explicit target selection".into(),
855 ));
856 }
857 let run_doctests = doc || !other_target;
858 let run_libtests = !doc;
859 let mut doctest_arguments = invocation.arguments.clone();
860 if run_doctests && !doc {
861 doctest_arguments.insert(test + 1, "--doc".into());
862 }
863 if !invocation.runner_arguments.is_empty() {
864 doctest_arguments.push("--".into());
865 doctest_arguments.extend(invocation.runner_arguments.iter().cloned());
866 }
867 Ok(RustCargoExecutionSelection {
868 run_libtests,
869 run_doctests,
870 doctest_arguments,
871 })
872}
873
874pub(crate) fn relative_source(root: &Path, path: &Path) -> Result<String, RustTestRunnerError> {
875 let relative = path
876 .strip_prefix(root)
877 .map_err(|_| RustTestRunnerError::UnsafeArtifact(path.display().to_string()))?;
878 if relative.as_os_str().is_empty()
879 || relative
880 .components()
881 .any(|part| !matches!(part, Component::Normal(_)))
882 {
883 return Err(RustTestRunnerError::UnsafeArtifact(
884 path.display().to_string(),
885 ));
886 }
887 Ok(relative.to_string_lossy().replace('\\', "/"))
888}
889
890fn build_test_artifacts(
891 project: &PreparedRustProject,
892 command: &[String],
893) -> Result<Vec<TestArtifact>, RustTestRunnerError> {
894 let mut invocation = cargo_invocation(&project.workspace_root, command)?;
895 invocation
896 .arguments
897 .extend(["--no-run".into(), "--message-format=json".into()]);
898 let output = Command::new(&invocation.program)
905 .args(invocation.arguments)
906 .current_dir(&project.workspace_root)
907 .env("CARGO_TARGET_DIR", &project.target_directory)
908 .env("RUSTFLAGS", capped_rustflags())
909 .output()
910 .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
911 if !output.status.success() {
912 let rendered = output
916 .stdout
917 .split(|byte| *byte == b'\n')
918 .filter_map(|line| serde_json::from_slice::<serde_json::Value>(line).ok())
919 .filter(|message| {
920 message["reason"] == "compiler-message" && message["message"]["level"] == "error"
921 })
922 .filter_map(|message| message["message"]["rendered"].as_str().map(str::to_owned))
923 .collect::<String>();
924 return Err(RustTestRunnerError::CargoFailed(format!(
925 "{rendered}{}",
926 String::from_utf8_lossy(&output.stderr).trim()
927 )));
928 }
929 let canonical_target = fs::canonicalize(&project.target_directory)
930 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
931 let mut artifacts = Vec::new();
932 for line in output
933 .stdout
934 .split(|byte| *byte == b'\n')
935 .filter(|line| !line.is_empty())
936 {
937 let message: CargoMessage = serde_json::from_slice(line)
938 .map_err(|error| RustTestRunnerError::CargoJson(error.to_string()))?;
939 if message.reason != "compiler-artifact"
940 || !message.profile.as_ref().is_some_and(|profile| profile.test)
941 {
942 continue;
943 }
944 let (Some(executable), Some(target)) = (message.executable, message.target) else {
945 continue;
946 };
947 let executable = fs::canonicalize(&executable)
948 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
949 if !executable.starts_with(&canonical_target)
950 || !fs::metadata(&executable).is_ok_and(|metadata| metadata.is_file())
951 {
952 return Err(RustTestRunnerError::UnsafeArtifact(
953 executable.display().to_string(),
954 ));
955 }
956 let source = fs::canonicalize(target.src_path)
957 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
958 let package_directory = message
959 .manifest_path
960 .as_deref()
961 .and_then(Path::parent)
962 .map_or_else(|| project.workspace_root.clone(), Path::to_path_buf);
963 artifacts.push(TestArtifact {
964 executable,
965 package_directory,
966 name: target.name,
967 kind: if target.kind.iter().any(|kind| kind == "test") {
968 "integration".into()
969 } else {
970 "unit".into()
971 },
972 source: relative_source(&project.workspace_root, &source)?,
973 });
974 }
975 artifacts.sort_by(|left, right| left.executable.cmp(&right.executable));
976 artifacts.dedup_by(|left, right| left.executable == right.executable);
977 if artifacts.is_empty() {
978 return Err(RustTestRunnerError::CargoJson(
979 "Cargo emitted no libtest artifacts".into(),
980 ));
981 }
982 Ok(artifacts)
983}
984
985fn list_tests(
986 executable: &Path,
987 environment: &[(&'static str, OsString)],
988) -> Result<Vec<String>, RustTestRunnerError> {
989 let output = Command::new(executable)
990 .args(["--list", "--format", "terse"])
991 .envs(environment.iter().map(|(key, value)| (key, value)))
992 .output()
993 .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
994 if !output.status.success() {
995 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
999 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned();
1000 let detail = if !stderr.is_empty() {
1001 stderr
1002 } else if !stdout.is_empty() {
1003 format!("no stderr; stdout was {stdout}")
1004 } else {
1005 "no output on either stream".to_owned()
1006 };
1007 return Err(RustTestRunnerError::ListFailed(format!(
1008 "{} exited with {} when asked to --list: {detail}",
1009 executable.display(),
1010 output.status
1011 )));
1012 }
1013 let mut tests = String::from_utf8_lossy(&output.stdout)
1014 .lines()
1015 .filter_map(|line| line.strip_suffix(": test"))
1016 .map(str::to_owned)
1017 .collect::<Vec<_>>();
1018 tests.sort();
1019 tests.dedup();
1020 Ok(tests)
1021}
1022
1023pub(crate) fn snapshot(
1024 manifest: &CoverageManifest,
1025 directory: &Path,
1026) -> Result<RuntimeSnapshot, RustTestRunnerError> {
1027 let points = manifest
1028 .points
1029 .iter()
1030 .map(|point| point.id.as_str())
1031 .collect::<BTreeSet<_>>();
1032 let alternatives = manifest
1033 .branches
1034 .iter()
1035 .flat_map(|branch| {
1036 branch
1037 .alternatives
1038 .iter()
1039 .map(|alternative| alternative.id.as_str())
1040 })
1041 .collect::<BTreeSet<_>>();
1042 let decisions = manifest
1043 .decisions
1044 .iter()
1045 .map(|decision| (decision.id.as_str(), decision))
1046 .collect::<BTreeMap<_, _>>();
1047 let mut hits = BTreeSet::new();
1048 let mut vectors = BTreeMap::<String, BTreeSet<(Vec<Option<bool>>, bool)>>::new();
1049 let token = crate::rust_project::manifest_token(manifest);
1054 for (name, observations) in read_rust_probe_directory(directory)
1055 .map_err(|error| RustTestRunnerError::Probe(error.to_string()))?
1056 {
1057 if !name.starts_with(&token) {
1058 continue;
1059 }
1060 for observation in observations {
1061 match observation {
1062 RustProbeObservation::Hit { id } => {
1063 if !points.contains(id.as_str()) && !alternatives.contains(id.as_str()) {
1064 return Err(RustTestRunnerError::UnknownProbe(id));
1065 }
1066 hits.insert(id);
1067 }
1068 RustProbeObservation::Decision {
1069 id,
1070 values,
1071 outcome,
1072 } => {
1073 let Some(meta) = decisions.get(id.as_str()) else {
1074 return Err(RustTestRunnerError::UnknownProbe(id));
1075 };
1076 if values.len() != meta.conditions.len() {
1077 return Err(RustTestRunnerError::InvalidVector {
1078 id,
1079 expected: meta.conditions.len(),
1080 actual: values.len(),
1081 });
1082 }
1083 hits.insert(format!(
1084 "{}:outcome:{}",
1085 meta.id,
1086 if outcome { "true" } else { "false" }
1087 ));
1088 vectors
1089 .entry(meta.id.clone())
1090 .or_default()
1091 .insert((values, outcome));
1092 }
1093 }
1094 }
1095 }
1096 let mut decision_snapshots = Vec::new();
1097 for (id, observed) in vectors {
1098 let meta: DecisionMeta = (*decisions[id.as_str()]).clone();
1099 decision_snapshots.push(DecisionSnapshot {
1100 meta,
1101 vectors: observed
1102 .into_iter()
1103 .map(|(values, outcome)| McdcVector { values, outcome })
1104 .collect(),
1105 });
1106 }
1107 Ok(RuntimeSnapshot {
1108 decisions: decision_snapshots,
1109 hits: hits.into_iter().collect(),
1110 events: Vec::new(),
1111 })
1112}
1113
1114fn rust_coverage_model() -> CoverageModelDeclaration {
1115 CoverageModelDeclaration {
1116 language: "rust".into(),
1117 variant: "rust-owned-probes-v1".into(),
1118 name: "supercov-rust-owned-v1".into(),
1119 completeness_meaning: "Every semantics-proven Rust obligation in the owned source denominator was observed; explicit manifest limitations identify unmeasured Rust surfaces.".into(),
1120 measured: vec![
1121 "owned Rust statements and function entries".into(),
1122 "owned atomic condition vectors and decision outcomes".into(),
1123 "exact process-per-libtest attribution".into(),
1124 "exact process-per-doctest attribution".into(),
1125 ],
1126 not_measured: vec![
1127 "macro-expanded and generated Rust code".into(),
1128 "const-evaluated code and unsupported structural branch probes".into(),
1129 "causal linkage to individual actions or passing assertions".into(),
1130 "all input values, semantic partitions, paths, or concurrency interleavings".into(),
1131 "mutation score or assertion fault-detection strength".into(),
1132 ],
1133 }
1134}
1135
1136pub(crate) fn instrumented_stack_environment() -> Vec<(&'static str, &'static str)> {
1148 if std::env::var_os("RUST_MIN_STACK").is_some() {
1149 Vec::new()
1150 } else {
1151 vec![("RUST_MIN_STACK", "16777216")]
1152 }
1153}
1154
1155fn rustc_print_path(request: &str) -> Result<PathBuf, RustTestRunnerError> {
1157 let output = Command::new("rustc")
1158 .args(["--print", request])
1159 .output()
1160 .map_err(|error| {
1161 RustTestRunnerError::Launch(format!("rustc --print {request}: {error}"))
1162 })?;
1163 if !output.status.success() {
1164 return Err(RustTestRunnerError::Launch(format!(
1165 "rustc --print {request} exited with {}",
1166 output.status
1167 )));
1168 }
1169 Ok(PathBuf::from(
1170 String::from_utf8_lossy(&output.stdout).trim(),
1171 ))
1172}
1173
1174pub(crate) fn rustc_sysroot() -> Result<PathBuf, RustTestRunnerError> {
1176 rustc_print_path("sysroot")
1177}
1178
1179pub(crate) fn rustc_target_libdir() -> Result<PathBuf, RustTestRunnerError> {
1183 rustc_print_path("target-libdir")
1184}
1185
1186pub(crate) fn dynamic_library_environment(
1192 target_libdir: &Path,
1193 executable: &Path,
1194) -> Vec<(&'static str, OsString)> {
1195 let variable = if cfg!(target_os = "macos") {
1196 "DYLD_FALLBACK_LIBRARY_PATH"
1197 } else if cfg!(windows) {
1198 "PATH"
1199 } else {
1200 "LD_LIBRARY_PATH"
1201 };
1202 let mut entries = Vec::new();
1203 if let Some(deps) = executable.parent() {
1204 entries.push(deps.to_path_buf());
1205 if let Some(profile) = deps.parent() {
1206 entries.push(profile.to_path_buf());
1207 }
1208 }
1209 entries.push(target_libdir.to_path_buf());
1210 if let Some(existing) = std::env::var_os(variable) {
1211 entries.extend(std::env::split_paths(&existing));
1212 }
1213 match std::env::join_paths(entries) {
1214 Ok(value) => vec![(variable, value)],
1215 Err(_) => Vec::new(),
1216 }
1217}
1218
1219pub(crate) fn capped_rustflags() -> String {
1220 let mut rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
1221 if !rustflags.is_empty() {
1222 rustflags.push(' ');
1223 }
1224 rustflags.push_str("--cap-lints=warn");
1225 rustflags
1226}
1227
1228pub(crate) fn io_error(error: impl std::fmt::Display) -> RustTestRunnerError {
1229 RustTestRunnerError::Io(error.to_string())
1230}
1231
1232pub(crate) fn libtest_skipped(exit: i32, stdout: &str) -> bool {
1235 exit == 0 && (stdout.contains("running 0 tests") || stdout.contains("; 1 ignored;"))
1236}
1237
1238fn rust_runner_limitations(runner: &str) -> Vec<FrontendLimitation> {
1241 let prefix = if runner == "rust-libtest" {
1242 "rust".to_owned()
1243 } else {
1244 runner.to_owned()
1245 };
1246 vec![
1247 FrontendLimitation {
1248 id: format!("{prefix}-action-linkage-unavailable"),
1249 scopes: vec![FrontendLimitationScope::Action],
1250 reason: "Rust test frameworks expose no general action lifecycle".into(),
1251 },
1252 FrontendLimitation {
1253 id: format!("{prefix}-assertion-linkage-unavailable"),
1254 scopes: vec![FrontendLimitationScope::Assertion],
1255 reason: "assertion macros do not expose a stable per-assertion success lifecycle"
1256 .into(),
1257 },
1258 ]
1259}
1260
1261fn rust_runner_declaration(runner: &str) -> FrontendRunnerDeclaration {
1262 FrontendRunnerDeclaration {
1263 runner: runner.into(),
1264 execution_model: ExecutionModel::ProcessPerTest,
1265 attribution: FrontendAttribution {
1266 run: AttributionPrecision::Exact,
1267 worker: AttributionPrecision::Exact,
1268 test: AttributionPrecision::Exact,
1269 retry: AttributionPrecision::Exact,
1270 phase: AttributionPrecision::Exact,
1271 action: AttributionPrecision::Unavailable,
1272 assertion: AttributionPrecision::Unavailable,
1273 },
1274 limitations: rust_runner_limitations(runner),
1275 }
1276}
1277
1278fn structural_limitations(project: &PreparedRustProject) -> Vec<String> {
1280 project
1281 .manifest
1282 .limitations
1283 .iter()
1284 .filter_map(|item| {
1285 item.get("id")
1286 .and_then(|value| value.as_str())
1287 .map(str::to_owned)
1288 })
1289 .collect()
1290}
1291
1292pub fn run_prepared_rust_tests(
1293 project: &PreparedRustProject,
1294 command: &[String],
1295 run_id: &str,
1296 generated_at: &str,
1297 diagnostics: &mut dyn Write,
1298) -> Result<RustFrontendRun, RustTestRunnerError> {
1299 let invocation = cargo_invocation(&project.workspace_root, command)?;
1300 let selection = rust_cargo_execution_selection(&invocation)?;
1301 let build_started = Instant::now();
1302 let artifacts = if selection.run_libtests && invocation.kind == RustCargoCommandKind::CargoTest
1306 {
1307 build_test_artifacts(project, command)?
1308 } else {
1309 Vec::new()
1310 };
1311 let build_ms = build_started.elapsed().as_secs_f64() * 1000.0;
1312 let evidence_root = project
1313 .workspace_root
1314 .join(".supercov/rust-evidence")
1315 .join(run_id);
1316 fs::create_dir_all(&evidence_root)
1317 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1318 let mut results = Vec::new();
1319 let mut overall_exit = 0;
1320 let execution_started = Instant::now();
1321 if invocation.kind == RustCargoCommandKind::NextestRun {
1322 let outcome = crate::rust_owned_nextest::run_nextest(
1325 project,
1326 &invocation,
1327 &evidence_root.join("nextest"),
1328 run_id,
1329 diagnostics,
1330 )?;
1331 let artifact_count = outcome.artifact_files.len();
1332 return Ok(RustFrontendRun {
1333 declaration: FrontendRunDeclaration {
1334 protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
1335 frontend_id: "rust".into(),
1336 frontend_version: "rust-owned-v1".into(),
1337 language: "rust".into(),
1338 structural_source: StructuralSource::OwnedProbes,
1339 runners: vec![rust_runner_declaration("nextest")],
1340 structural_limitations: structural_limitations(project),
1341 },
1342 request: CoverageReportRequest {
1343 run_id: run_id.into(),
1344 manifest: project.manifest.clone(),
1345 raw_results: outcome.results,
1346 generated_at: generated_at.into(),
1347 coverage_model: Some(rust_coverage_model()),
1348 integrity: None,
1349 test_exit_code: ExitCodeInput::Present(Some(outcome.exit_code)),
1350 },
1351 exit_code: outcome.exit_code,
1352 artifacts: artifact_count,
1353 artifact_files: outcome.artifact_files,
1354 build_ms,
1355 execution_ms: execution_started.elapsed().as_secs_f64() * 1000.0,
1356 });
1357 }
1358 let mut tasks = Vec::new();
1359 let target_libdir = if artifacts.is_empty() {
1360 PathBuf::new()
1361 } else {
1362 rustc_target_libdir()?
1363 };
1364 for (artifact_index, artifact) in artifacts.iter().enumerate() {
1365 let tests = list_tests(
1366 &artifact.executable,
1367 &dynamic_library_environment(&target_libdir, &artifact.executable),
1368 )?;
1369 let contexts = preflight_rust_test_contexts(tests.clone())
1370 .map_err(|error| RustTestRunnerError::Context(error.to_string()))?;
1371 for (test_index, test) in tests.into_iter().enumerate() {
1372 let directory = evidence_root.join(format!("{artifact_index:04}-{test_index:08}"));
1373 fs::create_dir(&directory)
1374 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1375 tasks.push(ProcessTask {
1376 ordinal: tasks.len(),
1377 artifact_index,
1378 test_index,
1379 artifact: artifact.clone(),
1380 context_id: contexts[&test],
1381 test,
1382 directory,
1383 });
1384 }
1385 }
1386 let workers = std::thread::available_parallelism()
1387 .map(usize::from)
1388 .unwrap_or(1)
1389 .min(tasks.len().max(1));
1390 let next = AtomicUsize::new(0);
1391 let outcomes = Mutex::new(Vec::<Result<ProcessOutcome, String>>::with_capacity(
1392 tasks.len(),
1393 ));
1394 std::thread::scope(|scope| {
1395 for _ in 0..workers {
1396 scope.spawn(|| {
1397 loop {
1398 let index = next.fetch_add(1, Ordering::Relaxed);
1399 let Some(task) = tasks.get(index) else { break };
1400 let result = Command::new(&task.artifact.executable)
1401 .args(["--exact", &task.test])
1410 .current_dir(&task.artifact.package_directory)
1411 .envs(instrumented_stack_environment())
1412 .envs(dynamic_library_environment(
1413 &target_libdir,
1414 &task.artifact.executable,
1415 ))
1416 .env("SUPERCOV_RUST_EVIDENCE_DIR", &task.directory)
1417 .env(
1418 crate::rust_probe_transport::RUST_CONTEXT_ENV,
1419 format!("{:016x}", task.context_id),
1420 )
1421 .output()
1422 .map(|output| ProcessOutcome {
1423 task: ProcessTask {
1424 ordinal: task.ordinal,
1425 artifact_index: task.artifact_index,
1426 test_index: task.test_index,
1427 artifact: task.artifact.clone(),
1428 test: task.test.clone(),
1429 context_id: task.context_id,
1430 directory: task.directory.clone(),
1431 },
1432 output,
1433 })
1434 .map_err(|error| error.to_string());
1435 outcomes
1436 .lock()
1437 .expect("Rust test result lock poisoned")
1438 .push(result);
1439 }
1440 });
1441 }
1442 });
1443 let mut outcomes = outcomes
1444 .into_inner()
1445 .map_err(|_| RustTestRunnerError::Io("Rust test result lock poisoned".into()))?
1446 .into_iter()
1447 .map(|result| result.map_err(RustTestRunnerError::Launch))
1448 .collect::<Result<Vec<_>, _>>()?;
1449 outcomes.sort_by_key(|outcome| outcome.task.ordinal);
1450 for outcome in outcomes {
1451 let ProcessTask {
1452 artifact_index,
1453 test_index,
1454 artifact,
1455 test,
1456 directory,
1457 ..
1458 } = outcome.task;
1459 let test_id = format!("{}::{test}", artifact.source);
1463 let worker_id = format!("artifact-{artifact_index:04}");
1464 let attempt_id = format!("{run_id}:{artifact_index:04}:{test_index:08}");
1465 let output = outcome.output;
1466 let exit = output.status.code().unwrap_or(1);
1467 let stdout = String::from_utf8_lossy(&output.stdout);
1468 let skipped = libtest_skipped(exit, &stdout);
1469 if exit != 0 {
1470 writeln!(diagnostics, "[supercov] Rust test failed: {test_id}")
1471 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1472 diagnostics
1473 .write_all(&output.stdout)
1474 .and_then(|_| diagnostics.write_all(&output.stderr))
1475 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1476 }
1477 if exit != 0 {
1478 overall_exit = exit;
1479 }
1480 results.push(RawTestResult {
1481 test_id: Some(test_id.clone()),
1482 scope: Some(ExecutionScope {
1483 version: 1,
1484 run_id: run_id.into(),
1485 worker_id,
1486 test_id: test_id.clone(),
1487 test_key: format!("{}::{test}", artifact.source),
1488 retry: 0,
1489 attempt_id,
1490 }),
1491 test: test_id,
1492 test_file: Some(artifact.source.clone()),
1493 title: Some(test),
1494 retry: Some(0),
1495 status: Some(
1496 if exit != 0 {
1497 "failed"
1498 } else if skipped {
1499 "skipped"
1500 } else {
1501 "passed"
1502 }
1503 .into(),
1504 ),
1505 expected_status: Some("passed".into()),
1506 flaky: false,
1507 provenance: TestProvenance {
1508 runner: "rust-libtest".into(),
1509 kind: artifact.kind,
1510 project: Some(artifact.name),
1511 source: "supercov-owned-process-per-test".into(),
1512 },
1513 role: "test".into(),
1514 phases: Vec::new(),
1515 runtime: vec![snapshot(&project.manifest, &directory)?],
1516 browser: Vec::new(),
1517 server: Vec::new(),
1518 });
1519 }
1520 let doctest_results =
1521 if selection.run_doctests && invocation.kind == RustCargoCommandKind::CargoTest {
1522 crate::rust_owned_doctests::run_doctests(
1523 project,
1524 &invocation,
1525 &selection,
1526 &evidence_root.join("doctests"),
1527 run_id,
1528 diagnostics,
1529 &mut overall_exit,
1530 )?
1531 } else {
1532 Vec::new()
1533 };
1534 let ran_libtests = !results.is_empty();
1537 let ran_doctests = !doctest_results.is_empty();
1538 results.extend(doctest_results);
1539 let mut runners = Vec::new();
1540 if ran_libtests || !ran_doctests {
1541 runners.push(rust_runner_declaration("rust-libtest"));
1542 }
1543 if ran_doctests {
1544 runners.push(rust_runner_declaration("rustdoc"));
1545 }
1546 let structural_limitations = structural_limitations(project);
1547 Ok(RustFrontendRun {
1548 declaration: FrontendRunDeclaration {
1549 protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
1550 frontend_id: "rust".into(),
1551 frontend_version: "rust-owned-v1".into(),
1552 language: "rust".into(),
1553 structural_source: StructuralSource::OwnedProbes,
1554 runners,
1555 structural_limitations,
1556 },
1557 request: CoverageReportRequest {
1558 run_id: run_id.into(),
1559 manifest: project.manifest.clone(),
1560 raw_results: results,
1561 generated_at: generated_at.into(),
1562 coverage_model: Some(rust_coverage_model()),
1563 integrity: None,
1564 test_exit_code: ExitCodeInput::Present(Some(overall_exit)),
1565 },
1566 exit_code: overall_exit,
1567 artifacts: artifacts.len(),
1568 artifact_files: artifacts
1569 .iter()
1570 .map(|artifact| artifact.executable.clone())
1571 .collect(),
1572 build_ms,
1573 execution_ms: execution_started.elapsed().as_secs_f64() * 1000.0,
1574 })
1575}
1576
1577#[cfg(test)]
1578mod tests {
1579 use std::time::{SystemTime, UNIX_EPOCH};
1580
1581 use super::*;
1582 use crate::{
1583 coverage_report::{ArchiveReportRequest, analyze_coverage_archive},
1584 evidence_archive::write_archive,
1585 frontend_protocol::validate_frontend_report_request,
1586 rust_project::prepare_rust_project,
1587 };
1588
1589 #[test]
1590 fn cargo_and_libtest_selection_is_preserved_without_presentation_guessing() {
1591 let root = Path::new(".");
1592 let invocation = cargo_invocation(
1593 root,
1594 &[
1595 "cargo".into(),
1596 "test".into(),
1597 "-p".into(),
1598 "fixture".into(),
1599 "authored".into(),
1600 "--".into(),
1601 "generated".into(),
1602 "--skip".into(),
1603 "slow".into(),
1604 "--include-ignored".into(),
1605 ],
1606 )
1607 .unwrap();
1608 assert_eq!(invocation.arguments, ["test", "-p", "fixture", "authored"]);
1609 assert_eq!(
1610 invocation.runner_arguments,
1611 ["generated", "--skip", "slow", "--include-ignored"]
1612 );
1613 let selection = rust_libtest_selection(&invocation).unwrap();
1614 assert_eq!(
1615 selection.list_arguments,
1616 [
1617 "authored",
1618 "generated",
1619 "--skip",
1620 "slow",
1621 "--include-ignored"
1622 ]
1623 );
1624 }
1625
1626 #[test]
1627 fn direct_cargo_argv_preserves_toml_quotes_inside_config_values() {
1628 let config = "target.host.runner=[\"runner with spaces\",\"--fixed\"]";
1629 let invocation = cargo_invocation(
1630 Path::new("."),
1631 &[
1632 "cargo".into(),
1633 "test".into(),
1634 "--config".into(),
1635 config.into(),
1636 ],
1637 )
1638 .unwrap();
1639 assert_eq!(invocation.arguments, ["test", "--config", config]);
1640 }
1641
1642 #[test]
1643 fn nextest_run_is_detected_without_reclassifying_its_filters_or_retries() {
1644 let invocation = cargo_invocation(
1645 Path::new("."),
1646 &[
1647 "cargo".into(),
1648 "+1.95.0".into(),
1649 "nextest".into(),
1650 "run".into(),
1651 "--retries".into(),
1652 "2".into(),
1653 "-E".into(),
1654 "test(/flaky/)".into(),
1655 "--".into(),
1656 "--nocapture".into(),
1657 ],
1658 )
1659 .unwrap();
1660 assert_eq!(invocation.kind, RustCargoCommandKind::NextestRun);
1661 assert_eq!(
1662 invocation.arguments,
1663 [
1664 "+1.95.0",
1665 "nextest",
1666 "run",
1667 "--retries",
1668 "2",
1669 "-E",
1670 "test(/flaky/)",
1671 ]
1672 );
1673 assert_eq!(invocation.runner_arguments, ["--nocapture"]);
1674 let execution = rust_cargo_execution_selection(&invocation).unwrap();
1675 assert!(execution.run_libtests);
1676 assert!(!execution.run_doctests);
1677 assert!(execution.doctest_arguments.is_empty());
1678 assert!(rust_libtest_selection(&invocation).is_err());
1679 assert_eq!(
1680 nextest_list_invocation(&invocation).unwrap(),
1681 NextestListInvocation {
1682 arguments: vec![
1683 "+1.95.0".into(),
1684 "nextest".into(),
1685 "list".into(),
1686 "-E".into(),
1687 "test(/flaky/)".into(),
1688 "--message-format".into(),
1689 "json".into(),
1690 ],
1691 runner_arguments: vec!["--nocapture".into()],
1692 }
1693 );
1694 }
1695
1696 #[test]
1697 fn nextest_list_projection_preserves_selection_and_rejects_external_state() {
1698 let invocation = CargoTestInvocation {
1699 program: "cargo".into(),
1700 kind: RustCargoCommandKind::NextestRun,
1701 arguments: vec![
1702 "nextest".into(),
1703 "run".into(),
1704 "--package=fixture".into(),
1705 "--partition".into(),
1706 "hash:1/2".into(),
1707 "--test-threads=8".into(),
1708 "--failure-output".into(),
1709 "final".into(),
1710 "name".into(),
1711 ],
1712 runner_arguments: vec!["--exact".into(), "full::name".into()],
1713 };
1714 assert_eq!(
1715 nextest_list_invocation(&invocation).unwrap(),
1716 NextestListInvocation {
1717 arguments: vec![
1718 "nextest".into(),
1719 "list".into(),
1720 "--package=fixture".into(),
1721 "--partition".into(),
1722 "hash:1/2".into(),
1723 "name".into(),
1724 "--message-format".into(),
1725 "json".into(),
1726 ],
1727 runner_arguments: vec!["--exact".into(), "full::name".into()],
1728 }
1729 );
1730
1731 let mut rerun = invocation;
1732 rerun.arguments.extend(["--rerun".into(), "latest".into()]);
1733 assert!(
1734 nextest_list_invocation(&rerun)
1735 .unwrap_err()
1736 .to_string()
1737 .contains("cannot yet be assigned exact selected-test identity")
1738 );
1739 }
1740
1741 #[test]
1742 fn nextest_list_projection_preserves_post_separator_libtest_selection() {
1743 let invocation = cargo_invocation(
1744 Path::new("."),
1745 &[
1746 "cargo".into(),
1747 "nextest".into(),
1748 "run".into(),
1749 "--timings".into(),
1750 "-vv".into(),
1751 "--".into(),
1752 "--include-ignored".into(),
1753 "--skip".into(),
1754 "slow".into(),
1755 "--exact".into(),
1756 "tests::selected".into(),
1757 ],
1758 )
1759 .unwrap();
1760 assert_eq!(
1761 nextest_list_invocation(&invocation).unwrap(),
1762 NextestListInvocation {
1763 arguments: vec![
1764 "nextest".to_owned(),
1765 "list".to_owned(),
1766 "--timings".to_owned(),
1767 "-vv".to_owned(),
1768 "--message-format".to_owned(),
1769 "json".to_owned(),
1770 ],
1771 runner_arguments: vec![
1772 "--include-ignored".to_owned(),
1773 "--skip".to_owned(),
1774 "slow".to_owned(),
1775 "--exact".to_owned(),
1776 "tests::selected".to_owned(),
1777 ],
1778 }
1779 );
1780 }
1781
1782 #[test]
1783 fn nextest_version_handshake_preserves_the_cargo_toolchain_selector() {
1784 let invocation = CargoTestInvocation {
1785 program: "cargo".into(),
1786 kind: RustCargoCommandKind::NextestRun,
1787 arguments: vec![
1788 "+1.95.0".into(),
1789 "nextest".into(),
1790 "run".into(),
1791 "-p".into(),
1792 "fixture".into(),
1793 ],
1794 runner_arguments: Vec::new(),
1795 };
1796 assert_eq!(
1797 nextest_version_arguments(&invocation).unwrap(),
1798 ["+1.95.0", "nextest", "--version"]
1799 );
1800 }
1801
1802 #[test]
1803 fn stock_libtest_presentation_and_scheduling_options_do_not_change_discovery() {
1804 let invocation = CargoTestInvocation {
1805 program: "cargo".into(),
1806 kind: RustCargoCommandKind::CargoTest,
1807 arguments: vec!["test".into(), "cargo-filter".into()],
1808 runner_arguments: [
1809 "runner-filter",
1810 "--nocapture",
1811 "--show-output",
1812 "--format=json",
1813 "--color",
1814 "never",
1815 "--test-threads=4",
1816 "--fail-fast",
1817 "--shuffle-seed",
1818 "17",
1819 "-Zunstable-options",
1820 "--exclude-should-panic",
1821 ]
1822 .into_iter()
1823 .map(str::to_owned)
1824 .collect(),
1825 };
1826 let selection = rust_libtest_selection(&invocation).unwrap();
1827 assert_eq!(
1828 selection.list_arguments,
1829 [
1830 "cargo-filter",
1831 "runner-filter",
1832 "-Zunstable-options",
1833 "--exclude-should-panic"
1834 ]
1835 );
1836 }
1837
1838 #[test]
1839 fn cargo_test_options_are_not_mistaken_for_the_test_name_filter() {
1840 let invocation = CargoTestInvocation {
1841 program: "cargo".into(),
1842 kind: RustCargoCommandKind::CargoTest,
1843 arguments: vec![
1844 "test".into(),
1845 "--manifest-path".into(),
1846 "nested/Cargo.toml".into(),
1847 "--features=one,two".into(),
1848 "needle".into(),
1849 ],
1850 runner_arguments: vec!["--ignored".into(), "other".into()],
1851 };
1852 let selection = rust_libtest_selection(&invocation).unwrap();
1853 assert_eq!(selection.list_arguments, ["needle", "--ignored", "other"]);
1854 }
1855
1856 #[test]
1857 fn libtest_thread_count_is_preserved_as_runner_scheduling() {
1858 for arguments in [vec!["--test-threads", "1"], vec!["--test-threads=8"]] {
1859 let invocation = CargoTestInvocation {
1860 program: "cargo".into(),
1861 kind: RustCargoCommandKind::CargoTest,
1862 arguments: vec!["test".into()],
1863 runner_arguments: arguments.into_iter().map(str::to_owned).collect(),
1864 };
1865 let selection = rust_libtest_selection(&invocation).unwrap();
1866 assert!(selection.list_arguments.is_empty());
1867 }
1868 }
1869
1870 #[test]
1871 fn invalid_or_duplicate_libtest_thread_counts_fail_closed() {
1872 for arguments in [
1873 vec!["--test-threads"],
1874 vec!["--test-threads=0"],
1875 vec!["--test-threads=abc"],
1876 vec!["--test-threads", "1", "--test-threads=2"],
1877 ] {
1878 let invocation = CargoTestInvocation {
1879 program: "cargo".into(),
1880 kind: RustCargoCommandKind::CargoTest,
1881 arguments: vec!["test".into()],
1882 runner_arguments: arguments.into_iter().map(str::to_owned).collect(),
1883 };
1884 assert!(rust_libtest_selection(&invocation).is_err());
1885 }
1886 }
1887
1888 #[test]
1889 fn cargo_target_selection_reproduces_when_cargo_runs_doctests() {
1890 let invocation = CargoTestInvocation {
1891 program: "cargo".into(),
1892 kind: RustCargoCommandKind::CargoTest,
1893 arguments: vec![
1894 "test".into(),
1895 "-p".into(),
1896 "fixture".into(),
1897 "needle".into(),
1898 ],
1899 runner_arguments: vec!["--include-ignored".into()],
1900 };
1901 let selection = rust_cargo_execution_selection(&invocation).unwrap();
1902 assert!(selection.run_libtests);
1903 assert!(selection.run_doctests);
1904 assert_eq!(
1905 selection.doctest_arguments,
1906 [
1907 "test",
1908 "--doc",
1909 "-p",
1910 "fixture",
1911 "needle",
1912 "--",
1913 "--include-ignored"
1914 ]
1915 );
1916
1917 let mut explicit_doc = invocation.clone();
1918 explicit_doc.arguments.insert(1, "--doc".into());
1919 let selection = rust_cargo_execution_selection(&explicit_doc).unwrap();
1920 assert!(!selection.run_libtests);
1921 assert!(selection.run_doctests);
1922
1923 for target in ["--lib", "--tests", "--all-targets", "--example=demo"] {
1924 let mut selected = invocation.clone();
1925 selected.arguments.insert(1, target.into());
1926 let selection = rust_cargo_execution_selection(&selected).unwrap();
1927 assert!(selection.run_libtests);
1928 assert!(!selection.run_doctests);
1929 }
1930 }
1931
1932 #[test]
1933 fn libtest_processes_run_in_their_package_directory() {
1934 let nonce = SystemTime::now()
1938 .duration_since(UNIX_EPOCH)
1939 .unwrap()
1940 .as_nanos();
1941 let root = std::env::temp_dir().join(format!(
1942 "supercov-rust-runner-cwd-{}-{nonce}",
1943 std::process::id()
1944 ));
1945 fs::create_dir_all(root.join("member/src")).unwrap();
1946 fs::write(
1947 root.join("Cargo.toml"),
1948 "[workspace]\nmembers = ['member']\nresolver = '2'\n",
1949 )
1950 .unwrap();
1951 fs::write(
1952 root.join("member/Cargo.toml"),
1953 "[package]\nname='member'\nversion='0.0.0'\nedition='2024'\n",
1954 )
1955 .unwrap();
1956 fs::write(
1957 root.join("member/src/lib.rs"),
1958 r#"
1959pub fn manifest() -> String {
1960 std::fs::read_to_string("Cargo.toml").unwrap()
1961}
1962#[cfg(test)]
1963mod tests {
1964 #[test] fn reads_own_manifest() { assert!(super::manifest().contains("name='member'")); }
1965}
1966"#,
1967 )
1968 .unwrap();
1969 let project = prepare_rust_project(&root).unwrap();
1970 let run = run_prepared_rust_tests(
1971 &project,
1972 &["cargo".into(), "test".into(), "--lib".into()],
1973 "rust-fixture-cwd",
1974 "2026-08-26T00:00:00.000Z",
1975 &mut Vec::new(),
1976 )
1977 .unwrap();
1978 assert_eq!(run.exit_code, 0, "{:?}", run.request.raw_results);
1979 assert_eq!(run.request.raw_results.len(), 1);
1980 assert_eq!(run.request.raw_results[0].status.as_deref(), Some("passed"));
1981 fs::remove_dir_all(&root).ok();
1982 }
1983
1984 #[test]
1985 fn cargo_libtest_runs_produce_queryable_owned_evidence() {
1986 let nonce = SystemTime::now()
1987 .duration_since(UNIX_EPOCH)
1988 .unwrap()
1989 .as_nanos();
1990 let root = std::env::temp_dir().join(format!(
1991 "supercov-rust-runner-{}-{nonce}",
1992 std::process::id()
1993 ));
1994 fs::create_dir_all(root.join("src")).unwrap();
1995 fs::write(
1996 root.join("Cargo.toml"),
1997 "[package]\nname='fixture'\nversion='0.0.0'\nedition='2024'\n",
1998 )
1999 .unwrap();
2000 fs::write(
2001 root.join("src/lib.rs"),
2002 r#"
2003pub fn choose(left: bool, right: bool) -> i32 {
2004 if left && right { 1 } else { 0 }
2005}
2006pub fn pick(value: i32) -> &'static str {
2007 match value {
2008 0 => "zero",
2009 1 => "one",
2010 _ => "many",
2011 }
2012}
2013pub fn total(values: &[i32]) -> i32 {
2014 let mut sum = 0;
2015 for value in values {
2016 sum += value;
2017 }
2018 sum
2019}
2020pub fn first_even(values: &[i32]) -> Option<i32> {
2021 let mut index = 0;
2022 while index < values.len() {
2023 if values[index] % 2 == 0 {
2024 return Some(values[index]);
2025 }
2026 index += 1;
2027 }
2028 None
2029}
2030pub fn parse_twice(text: &str) -> Option<i32> {
2031 let value: i32 = text.parse().ok()?;
2032 Some(value * 2)
2033}
2034pub fn describe(value: Option<i32>, flag: bool) -> &'static str {
2035 if let Some(inner) = value && inner > 0 && flag {
2036 "positive"
2037 } else {
2038 "other"
2039 }
2040}
2041pub fn depth(n: u32) -> Result<u32, String> {
2042 if n == 0 {
2043 Ok(0)
2044 } else {
2045 let below = depth(n - 1)?;
2046 Ok(below + 1)
2047 }
2048}
2049#[cfg(test)]
2050mod tests {
2051 #[test] fn false_path() { assert_eq!(super::choose(false, true), 0); }
2052 #[test] fn true_path() { assert_eq!(super::choose(true, true), 1); }
2053 #[test] #[ignore] fn ignored_path() { unreachable!(); }
2054 #[test] fn pick_zero() { assert_eq!(super::pick(0), "zero"); }
2055 #[test] fn pick_many() { assert_eq!(super::pick(7), "many"); }
2056 #[test] fn total_empty() { assert_eq!(super::total(&[]), 0); }
2057 #[test] fn total_some() { assert_eq!(super::total(&[1, 2]), 3); }
2058 #[test] fn first_even_empty() { assert_eq!(super::first_even(&[]), None); }
2059 #[test] fn first_even_found() { assert_eq!(super::first_even(&[1, 4]), Some(4)); }
2060 #[test] fn parse_ok() { assert_eq!(super::parse_twice("4"), Some(8)); }
2061 #[test] fn parse_bad() { assert_eq!(super::parse_twice("x"), None); }
2062 #[test] fn chain_taken() { assert_eq!(super::describe(Some(1), true), "positive"); }
2063 #[test] fn chain_pattern_fails() { assert_eq!(super::describe(None, true), "other"); }
2064 #[test] fn chain_negative() { assert_eq!(super::describe(Some(-1), true), "other"); }
2065 #[test] fn chain_flag_fails() { assert_eq!(super::describe(Some(1), false), "other"); }
2066 #[test] fn deep_recursion() { assert_eq!(super::depth(5_000), Ok(5_000)); }
2067}
2068"#,
2069 )
2070 .unwrap();
2071 let project = prepare_rust_project(&root).unwrap();
2072 let run = run_prepared_rust_tests(
2076 &project,
2077 &["cargo".into(), "test".into(), "--lib".into()],
2078 "rust-fixture",
2079 "2026-08-26T00:00:00.000Z",
2080 &mut Vec::new(),
2081 )
2082 .unwrap();
2083 assert_eq!(run.exit_code, 0);
2084 assert_eq!(run.request.raw_results.len(), 16);
2085 let statuses = run
2086 .request
2087 .raw_results
2088 .iter()
2089 .filter_map(|result| result.status.as_deref())
2090 .collect::<Vec<_>>();
2091 assert_eq!(
2092 statuses
2093 .iter()
2094 .filter(|status| **status == "skipped")
2095 .count(),
2096 1
2097 );
2098 assert_eq!(
2099 statuses
2100 .iter()
2101 .filter(|status| **status == "passed")
2102 .count(),
2103 15
2104 );
2105
2106 let chain_vectors = |test: &str| {
2109 let result = run
2110 .request
2111 .raw_results
2112 .iter()
2113 .find(|result| result.test.ends_with(test))
2114 .unwrap_or_else(|| panic!("no test {test}"));
2115 let snapshot = result
2116 .runtime
2117 .iter()
2118 .flat_map(|snapshot| &snapshot.decisions)
2119 .find(|decision| decision.meta.source.starts_with("let Some(inner) = value"))
2120 .unwrap_or_else(|| panic!("{test} recorded no chain decision"));
2121 snapshot
2122 .vectors
2123 .iter()
2124 .map(|vector| (vector.values.clone(), vector.outcome))
2125 .collect::<Vec<_>>()
2126 };
2127 assert_eq!(
2128 chain_vectors("chain_taken"),
2129 [(vec![Some(true), Some(true), Some(true)], true)]
2130 );
2131 assert_eq!(
2132 chain_vectors("chain_pattern_fails"),
2133 [(vec![Some(false), None, None], false)]
2134 );
2135 assert_eq!(
2136 chain_vectors("chain_negative"),
2137 [(vec![Some(true), Some(false), None], false)]
2138 );
2139 assert_eq!(
2140 chain_vectors("chain_flag_fails"),
2141 [(vec![Some(true), Some(true), Some(false)], false)]
2142 );
2143 validate_frontend_report_request(&run.declaration, &run.request).unwrap();
2144 let archive = root.join("evidence.raw.gz");
2145 write_archive(run.archive_entries().unwrap(), &archive).unwrap();
2146 let report = analyze_coverage_archive(&ArchiveReportRequest {
2147 archive_path: archive,
2148 run_id: "rust-fixture".into(),
2149 generated_at: "2026-08-26T00:00:00.000Z".into(),
2150 integrity: None,
2151 test_exit_code: ExitCodeInput::Present(Some(0)),
2152 })
2153 .unwrap();
2154 assert_eq!(report.view.tests.len(), 16);
2155 assert!(report.view.summary.lines.covered > 0);
2156 assert!(report.view.summary.decisions > 0);
2157
2158 let single = |kind: &str| {
2161 let mut found = report
2162 .view
2163 .branches
2164 .iter()
2165 .filter(|branch| branch.meta.kind == kind);
2166 let branch = found.next().unwrap_or_else(|| panic!("no {kind} branch"));
2167 assert!(found.next().is_none(), "more than one {kind} branch");
2168 branch
2169 };
2170 let tests_of = |branch: &crate::coverage_report::BranchResult, label: &str| {
2171 branch
2172 .alternatives
2173 .iter()
2174 .find(|alternative| alternative.label == label)
2175 .unwrap_or_else(|| panic!("{} has no alternative {label}", branch.meta.kind))
2176 .tests
2177 .clone()
2178 };
2179 let for_loop = single("for-loop");
2180 assert_eq!(
2181 tests_of(for_loop, "zero iterations"),
2182 ["src/lib.rs::tests::total_empty"]
2183 );
2184 assert_eq!(
2185 tests_of(for_loop, "entered"),
2186 ["src/lib.rs::tests::total_some"]
2187 );
2188 let while_loop = single("while-loop");
2189 assert_eq!(
2190 tests_of(while_loop, "zero iterations"),
2191 ["src/lib.rs::tests::first_even_empty"]
2192 );
2193 assert_eq!(
2194 tests_of(while_loop, "entered"),
2195 ["src/lib.rs::tests::first_even_found"]
2196 );
2197 let try_operator = report
2200 .view
2201 .branches
2202 .iter()
2203 .find(|branch| {
2204 branch.meta.kind == "try-operator" && branch.meta.source.contains("parse().ok()")
2205 })
2206 .expect("parse_twice's try operator");
2207 assert_eq!(
2208 tests_of(try_operator, "continued"),
2209 ["src/lib.rs::tests::parse_ok"]
2210 );
2211 assert_eq!(
2212 tests_of(try_operator, "early return"),
2213 ["src/lib.rs::tests::parse_bad"]
2214 );
2215 let mut logical = report
2216 .view
2217 .branches
2218 .iter()
2219 .filter(|branch| branch.meta.kind == "logical-and")
2220 .collect::<Vec<_>>();
2221 logical.sort_by_key(|branch| (branch.meta.line, branch.meta.column));
2222 assert_eq!(logical.len(), 3);
2224 assert_eq!(
2225 tests_of(logical[0], "short-circuited"),
2226 ["src/lib.rs::tests::false_path"]
2227 );
2228 assert_eq!(
2229 tests_of(logical[0], "right operand evaluated"),
2230 ["src/lib.rs::tests::true_path"]
2231 );
2232 assert_eq!(
2233 tests_of(logical[1], "short-circuited"),
2234 ["src/lib.rs::tests::chain_pattern_fails"]
2235 );
2236 assert_eq!(
2237 tests_of(logical[1], "right operand evaluated"),
2238 [
2239 "src/lib.rs::tests::chain_flag_fails",
2240 "src/lib.rs::tests::chain_negative",
2241 "src/lib.rs::tests::chain_taken",
2242 ]
2243 );
2244 assert_eq!(
2245 tests_of(logical[2], "short-circuited"),
2246 [
2247 "src/lib.rs::tests::chain_negative",
2248 "src/lib.rs::tests::chain_pattern_fails",
2249 ]
2250 );
2251 assert_eq!(
2252 tests_of(logical[2], "right operand evaluated"),
2253 [
2254 "src/lib.rs::tests::chain_flag_fails",
2255 "src/lib.rs::tests::chain_taken",
2256 ]
2257 );
2258 assert!(for_loop.covered && while_loop.covered && try_operator.covered);
2259 assert!(logical.iter().all(|branch| branch.covered));
2260
2261 let mut arms = report
2264 .view
2265 .branches
2266 .iter()
2267 .filter(|branch| branch.meta.kind == "match-arm")
2268 .collect::<Vec<_>>();
2269 arms.sort_by_key(|branch| branch.meta.line);
2270 assert_eq!(arms.len(), 3);
2271 let alternative = |arm: usize, label: &str| {
2272 arms[arm]
2273 .alternatives
2274 .iter()
2275 .find(|alternative| alternative.label == label)
2276 .unwrap_or_else(|| panic!("arm {arm} has no alternative {label}"))
2277 };
2278 assert_eq!(
2279 alternative(0, "selected").tests,
2280 ["src/lib.rs::tests::pick_zero"]
2281 );
2282 assert_eq!(
2283 alternative(0, "not selected").tests,
2284 ["src/lib.rs::tests::pick_many"]
2285 );
2286 assert!(!alternative(1, "selected").covered);
2287 assert_eq!(
2288 alternative(1, "not selected").tests,
2289 ["src/lib.rs::tests::pick_many"]
2290 );
2291 assert_eq!(arms[2].alternatives.len(), 1);
2292 assert_eq!(
2293 alternative(2, "selected").tests,
2294 ["src/lib.rs::tests::pick_many"]
2295 );
2296 assert!(arms[0].covered && !arms[1].covered && arms[2].covered);
2297
2298 fs::remove_dir_all(root).unwrap();
2299 }
2300}