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) -> 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(
990 executable: &Path,
991 filters: &[String],
992 environment: &[(&'static str, OsString)],
993) -> Result<Vec<String>, RustTestRunnerError> {
994 let output = Command::new(executable)
995 .args(["--list", "--format", "terse"])
996 .args(filters)
997 .envs(environment.iter().map(|(key, value)| (key, value)))
998 .output()
999 .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
1000 if !output.status.success() {
1001 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
1005 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned();
1006 let detail = if !stderr.is_empty() {
1007 stderr
1008 } else if !stdout.is_empty() {
1009 format!("no stderr; stdout was {stdout}")
1010 } else {
1011 "no output on either stream".to_owned()
1012 };
1013 return Err(RustTestRunnerError::ListFailed(format!(
1014 "{} exited with {} when asked to --list: {detail}",
1015 executable.display(),
1016 output.status
1017 )));
1018 }
1019 let mut tests = String::from_utf8_lossy(&output.stdout)
1020 .lines()
1021 .filter_map(|line| line.strip_suffix(": test"))
1022 .map(str::to_owned)
1023 .collect::<Vec<_>>();
1024 tests.sort();
1025 tests.dedup();
1026 Ok(tests)
1027}
1028
1029pub(crate) struct RustEvidence {
1032 pub(crate) snapshot: RuntimeSnapshot,
1033 pub(crate) phases: Vec<CoveragePhase>,
1034}
1035
1036struct PendingRecord {
1038 event_type: &'static str,
1039 id: String,
1040 vector: Option<McdcVector>,
1041 sequence: i64,
1042}
1043
1044pub(crate) fn snapshot(
1048 manifest: &CoverageManifest,
1049 directory: &Path,
1050 attempt: &str,
1051) -> Result<RustEvidence, RustTestRunnerError> {
1052 let points = manifest
1053 .points
1054 .iter()
1055 .map(|point| (point.id.as_str(), point))
1056 .collect::<BTreeMap<_, _>>();
1057 let alternatives = manifest
1058 .branches
1059 .iter()
1060 .flat_map(|branch| {
1061 branch
1062 .alternatives
1063 .iter()
1064 .map(|alternative| alternative.id.as_str())
1065 })
1066 .collect::<BTreeSet<_>>();
1067 let decisions = manifest
1068 .decisions
1069 .iter()
1070 .map(|decision| (decision.id.as_str(), decision))
1071 .collect::<BTreeMap<_, _>>();
1072 let mut hits = BTreeSet::new();
1073 let mut vectors = BTreeMap::<String, BTreeSet<(Vec<Option<bool>>, bool)>>::new();
1074 let mut pending = BTreeMap::<u64, Vec<PendingRecord>>::new();
1078 let mut events = Vec::new();
1079 let mut phases = Vec::new();
1080 let mut sequence = 0_i64;
1083 let token = crate::rust_project::manifest_token(manifest);
1088 for (name, observations) in read_rust_probe_directory(directory)
1089 .map_err(|error| RustTestRunnerError::Probe(error.to_string()))?
1090 {
1091 if !name.starts_with(&token) {
1092 continue;
1093 }
1094 for entry in observations {
1095 sequence += 1;
1096 let thread = entry.thread;
1097 match entry.observation {
1098 RustProbeObservation::Hit { id } => {
1099 if !points.contains_key(id.as_str()) && !alternatives.contains(id.as_str()) {
1100 return Err(RustTestRunnerError::UnknownProbe(id));
1101 }
1102 hits.insert(id.clone());
1103 pending.entry(thread).or_default().push(PendingRecord {
1104 event_type: "hit",
1105 id,
1106 vector: None,
1107 sequence,
1108 });
1109 }
1110 RustProbeObservation::Decision {
1111 id,
1112 values,
1113 outcome,
1114 } => {
1115 let Some(meta) = decisions.get(id.as_str()) else {
1116 return Err(RustTestRunnerError::UnknownProbe(id));
1117 };
1118 if values.len() != meta.conditions.len() {
1119 return Err(RustTestRunnerError::InvalidVector {
1120 id,
1121 expected: meta.conditions.len(),
1122 actual: values.len(),
1123 });
1124 }
1125 hits.insert(format!(
1126 "{}:outcome:{}",
1127 meta.id,
1128 if outcome { "true" } else { "false" }
1129 ));
1130 vectors
1131 .entry(meta.id.clone())
1132 .or_default()
1133 .insert((values.clone(), outcome));
1134 pending.entry(thread).or_default().push(PendingRecord {
1135 event_type: "decision",
1136 id,
1137 vector: Some(McdcVector { values, outcome }),
1138 sequence,
1139 });
1140 }
1141 RustProbeObservation::Assertion { id } => {
1142 let Some(point) = points.get(id.as_str()) else {
1145 return Err(RustTestRunnerError::UnknownProbe(id));
1146 };
1147 let witnessed = pending.remove(&thread).unwrap_or_default();
1148 let phase_id = format!("{attempt}:assertion:{sequence}");
1149 phases.push(CoveragePhase {
1150 id: phase_id.clone(),
1151 kind: "assertion".into(),
1152 operation: format!(
1153 "Rust assertion at {}:{}:{}",
1154 point.file, point.line, point.column
1155 ),
1156 source: Some(point.source.clone()),
1157 caused_by_phase_id: None,
1158 started_at_ms: witnessed.first().map_or(sequence, |record| record.sequence),
1159 ended_at_ms: Some(sequence),
1160 status: Some("passed".into()),
1163 error: None,
1164 });
1165 for record in witnessed {
1166 events.push(RuntimeEvent {
1167 event_type: record.event_type.into(),
1168 id: record.id,
1169 vector: record.vector,
1170 timestamp_ms: record.sequence,
1171 phase_id: Some(phase_id.clone()),
1172 statement_id: None,
1173 environment: "server".into(),
1174 });
1175 }
1176 }
1177 }
1178 }
1179 }
1180 let mut decision_snapshots = Vec::new();
1181 for (id, observed) in vectors {
1182 let meta: DecisionMeta = (*decisions[id.as_str()]).clone();
1183 decision_snapshots.push(DecisionSnapshot {
1184 meta,
1185 vectors: observed
1186 .into_iter()
1187 .map(|(values, outcome)| McdcVector { values, outcome })
1188 .collect(),
1189 });
1190 }
1191 Ok(RustEvidence {
1192 snapshot: RuntimeSnapshot {
1193 decisions: decision_snapshots,
1194 hits: hits.into_iter().collect(),
1195 events,
1196 logicals: Vec::new(),
1197 phase_id: None,
1198 },
1199 phases,
1200 })
1201}
1202
1203fn rust_coverage_model() -> CoverageModelDeclaration {
1204 CoverageModelDeclaration {
1205 language: "rust".into(),
1206 variant: "rust-owned-probes-v1".into(),
1207 name: "supercov-rust-owned-v1".into(),
1208 completeness_meaning: "Every semantics-proven Rust obligation in the owned source denominator was observed; explicit manifest limitations identify unmeasured Rust surfaces.".into(),
1209 measured: vec![
1210 "owned Rust statements and function entries".into(),
1211 "owned atomic condition vectors and decision outcomes".into(),
1212 "exact process-per-libtest attribution".into(),
1213 "exact process-per-doctest attribution".into(),
1214 "evidence a passing assertion of the same thread witnessed".into(),
1215 ],
1216 not_measured: vec![
1217 "macro-expanded and generated Rust code".into(),
1218 "const-evaluated code and unsupported structural branch probes".into(),
1219 "causal linkage to individual actions".into(),
1220 "all input values, semantic partitions, paths, or concurrency interleavings".into(),
1221 "mutation score or assertion fault-detection strength".into(),
1222 ],
1223 }
1224}
1225
1226pub(crate) fn instrumented_stack_environment() -> Vec<(&'static str, &'static str)> {
1238 if std::env::var_os("RUST_MIN_STACK").is_some() {
1239 Vec::new()
1240 } else {
1241 vec![("RUST_MIN_STACK", "16777216")]
1242 }
1243}
1244
1245fn rustc_print_path(request: &str) -> Result<PathBuf, RustTestRunnerError> {
1247 let output = Command::new("rustc")
1248 .args(["--print", request])
1249 .output()
1250 .map_err(|error| {
1251 RustTestRunnerError::Launch(format!("rustc --print {request}: {error}"))
1252 })?;
1253 if !output.status.success() {
1254 return Err(RustTestRunnerError::Launch(format!(
1255 "rustc --print {request} exited with {}",
1256 output.status
1257 )));
1258 }
1259 Ok(PathBuf::from(
1260 String::from_utf8_lossy(&output.stdout).trim(),
1261 ))
1262}
1263
1264pub(crate) fn rustc_sysroot() -> Result<PathBuf, RustTestRunnerError> {
1266 rustc_print_path("sysroot")
1267}
1268
1269pub(crate) fn rustc_target_libdir() -> Result<PathBuf, RustTestRunnerError> {
1273 rustc_print_path("target-libdir")
1274}
1275
1276pub(crate) fn dynamic_library_environment(
1282 target_libdir: &Path,
1283 executable: &Path,
1284) -> Vec<(&'static str, OsString)> {
1285 let variable = if cfg!(target_os = "macos") {
1286 "DYLD_FALLBACK_LIBRARY_PATH"
1287 } else if cfg!(windows) {
1288 "PATH"
1289 } else {
1290 "LD_LIBRARY_PATH"
1291 };
1292 let mut entries = Vec::new();
1293 if let Some(deps) = executable.parent() {
1294 entries.push(deps.to_path_buf());
1295 if let Some(profile) = deps.parent() {
1296 entries.push(profile.to_path_buf());
1297 }
1298 }
1299 entries.push(target_libdir.to_path_buf());
1300 if let Some(existing) = std::env::var_os(variable) {
1301 entries.extend(std::env::split_paths(&existing));
1302 }
1303 match std::env::join_paths(entries) {
1304 Ok(value) => vec![(variable, value)],
1305 Err(_) => Vec::new(),
1306 }
1307}
1308
1309pub(crate) fn capped_rustflags() -> String {
1310 let mut rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
1311 if !rustflags.is_empty() {
1312 rustflags.push(' ');
1313 }
1314 rustflags.push_str("--cap-lints=warn");
1315 rustflags
1316}
1317
1318pub(crate) fn io_error(error: impl std::fmt::Display) -> RustTestRunnerError {
1319 RustTestRunnerError::Io(error.to_string())
1320}
1321
1322pub(crate) fn libtest_skipped(exit: i32, stdout: &str) -> bool {
1325 exit == 0 && (stdout.contains("running 0 tests") || stdout.contains("; 1 ignored;"))
1326}
1327
1328fn rust_runner_limitations(runner: &str) -> Vec<FrontendLimitation> {
1331 let prefix = if runner == "rust-libtest" {
1332 "rust".to_owned()
1333 } else {
1334 runner.to_owned()
1335 };
1336 vec![
1337 FrontendLimitation {
1338 id: format!("{prefix}-action-linkage-unavailable"),
1339 scopes: vec![FrontendLimitationScope::Action],
1340 reason: "Rust test frameworks expose no general action lifecycle".into(),
1341 },
1342 FrontendLimitation {
1343 id: format!("{prefix}-assertion-linkage-unavailable"),
1344 scopes: vec![FrontendLimitationScope::Assertion],
1345 reason: "assertion macros do not expose a stable per-assertion success lifecycle"
1346 .into(),
1347 },
1348 ]
1349}
1350
1351fn rust_runner_declaration(runner: &str) -> FrontendRunnerDeclaration {
1352 FrontendRunnerDeclaration {
1353 runner: runner.into(),
1354 execution_model: ExecutionModel::ProcessPerTest,
1355 attribution: FrontendAttribution {
1356 run: AttributionPrecision::Exact,
1357 worker: AttributionPrecision::Exact,
1358 test: AttributionPrecision::Exact,
1359 retry: AttributionPrecision::Exact,
1360 phase: AttributionPrecision::Exact,
1361 action: AttributionPrecision::Unavailable,
1362 assertion: AttributionPrecision::Unavailable,
1363 },
1364 limitations: rust_runner_limitations(runner),
1365 }
1366}
1367
1368fn decline_uncompiled_sources(
1377 project: &PreparedRustProject,
1378) -> (CoverageManifest, BTreeSet<String>) {
1379 let mut manifest = project.manifest.clone();
1380 let Some(compiled) = crate::rust_project::compiled_source_files(
1381 &project.workspace_root,
1382 &project.target_directory,
1383 ) else {
1384 return (manifest, BTreeSet::new());
1385 };
1386 let declined = project
1387 .source_files
1388 .iter()
1389 .filter(|file| !compiled.contains(*file))
1390 .cloned()
1391 .collect::<BTreeSet<_>>();
1392 if declined.is_empty()
1395 || project
1396 .crate_roots
1397 .iter()
1398 .all(|root| !compiled.contains(root))
1399 {
1400 return (manifest, BTreeSet::new());
1401 }
1402 let mut unmeasured = manifest.unmeasured.iter().cloned().collect::<BTreeSet<_>>();
1403 unmeasured.extend(
1404 manifest
1405 .points
1406 .iter()
1407 .filter(|point| declined.contains(&point.file))
1408 .map(|point| point.id.clone()),
1409 );
1410 unmeasured.extend(
1411 manifest
1412 .decisions
1413 .iter()
1414 .filter(|decision| declined.contains(&decision.file))
1415 .map(|decision| decision.id.clone()),
1416 );
1417 unmeasured.extend(
1418 manifest
1419 .branches
1420 .iter()
1421 .filter(|branch| declined.contains(&branch.file))
1422 .map(|branch| branch.id.clone()),
1423 );
1424 manifest.unmeasured = unmeasured.into_iter().collect();
1425 manifest.limitations.retain(|limitation| {
1427 limitation
1428 .get("file")
1429 .and_then(|file| file.as_str())
1430 .is_none_or(|file| !declined.contains(file))
1431 });
1432 (manifest, declined)
1433}
1434
1435fn structural_limitations(manifest: &CoverageManifest) -> Vec<String> {
1437 manifest
1438 .limitations
1439 .iter()
1440 .filter_map(|item| {
1441 item.get("id")
1442 .and_then(|value| value.as_str())
1443 .map(str::to_owned)
1444 })
1445 .collect()
1446}
1447
1448pub fn run_prepared_rust_tests(
1449 project: &PreparedRustProject,
1450 command: &[String],
1451 run_id: &str,
1452 generated_at: &str,
1453 diagnostics: &mut dyn Write,
1454) -> Result<RustFrontendRun, RustTestRunnerError> {
1455 let invocation = cargo_invocation(&project.workspace_root, command)?;
1456 let selection = rust_cargo_execution_selection(&invocation)?;
1457 let build_started = Instant::now();
1458 let artifacts = if selection.run_libtests && invocation.kind == RustCargoCommandKind::CargoTest
1462 {
1463 build_test_artifacts(project, command)?
1464 } else {
1465 Vec::new()
1466 };
1467 let build_ms = build_started.elapsed().as_secs_f64() * 1000.0;
1468 let (manifest, declined) = decline_uncompiled_sources(project);
1472 if !declined.is_empty() {
1473 writeln!(
1474 diagnostics,
1475 "[supercov] {} source file(s) this build did not compile are outside the denominator: {}{}",
1476 declined.len(),
1477 declined
1478 .iter()
1479 .take(3)
1480 .cloned()
1481 .collect::<Vec<_>>()
1482 .join(", "),
1483 if declined.len() > 3 { ", ..." } else { "" }
1484 )
1485 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1486 }
1487 let project = &PreparedRustProject {
1488 manifest,
1489 ..project.clone()
1490 };
1491 let _signal_guard = crate::child_signal_guard::ChildSignalGuard::install()
1495 .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
1496 let evidence_root = project
1497 .workspace_root
1498 .join(".supercov/rust-evidence")
1499 .join(run_id);
1500 fs::create_dir_all(&evidence_root)
1501 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1502 let mut results = Vec::new();
1503 let mut overall_exit = 0;
1504 let execution_started = Instant::now();
1505 if invocation.kind == RustCargoCommandKind::NextestRun {
1506 let outcome = crate::rust_owned_nextest::run_nextest(
1509 project,
1510 &invocation,
1511 &evidence_root.join("nextest"),
1512 run_id,
1513 diagnostics,
1514 )?;
1515 let artifact_count = outcome.artifact_files.len();
1516 return Ok(RustFrontendRun {
1517 declaration: FrontendRunDeclaration {
1518 protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
1519 frontend_id: "rust".into(),
1520 frontend_version: "rust-owned-v1".into(),
1521 language: "rust".into(),
1522 structural_source: StructuralSource::OwnedProbes,
1523 runners: vec![rust_runner_declaration("nextest")],
1524 structural_limitations: structural_limitations(&project.manifest),
1525 },
1526 request: CoverageReportRequest {
1527 run_id: run_id.into(),
1528 manifest: project.manifest.clone(),
1529 raw_results: outcome.results,
1530 generated_at: generated_at.into(),
1531 coverage_model: Some(rust_coverage_model()),
1532 integrity: None,
1533 test_exit_code: ExitCodeInput::Present(Some(outcome.exit_code)),
1534 },
1535 exit_code: outcome.exit_code,
1536 artifacts: artifact_count,
1537 artifact_files: outcome.artifact_files,
1538 build_ms,
1539 execution_ms: execution_started.elapsed().as_secs_f64() * 1000.0,
1540 });
1541 }
1542 let libtest_selection = rust_libtest_selection(&invocation)?;
1548 let ignored_mode = libtest_selection
1551 .list_arguments
1552 .iter()
1553 .filter(|argument| {
1554 matches!(
1555 argument.as_str(),
1556 "--ignored" | "--include-ignored" | "--exclude-should-panic"
1557 )
1558 })
1559 .cloned()
1560 .collect::<Vec<_>>();
1561 let mut tasks = Vec::new();
1562 let target_libdir = if artifacts.is_empty() {
1563 PathBuf::new()
1564 } else {
1565 rustc_target_libdir()?
1566 };
1567 for (artifact_index, artifact) in artifacts.iter().enumerate() {
1568 let tests = list_tests(
1569 &artifact.executable,
1570 &libtest_selection.list_arguments,
1571 &dynamic_library_environment(&target_libdir, &artifact.executable),
1572 )?;
1573 let contexts = preflight_rust_test_contexts(tests.clone())
1574 .map_err(|error| RustTestRunnerError::Context(error.to_string()))?;
1575 for (test_index, test) in tests.into_iter().enumerate() {
1576 let directory = evidence_root.join(format!("{artifact_index:04}-{test_index:08}"));
1577 fs::create_dir(&directory)
1578 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1579 tasks.push(ProcessTask {
1580 ordinal: tasks.len(),
1581 artifact_index,
1582 test_index,
1583 artifact: artifact.clone(),
1584 context_id: contexts[&test],
1585 test,
1586 directory,
1587 });
1588 }
1589 }
1590 let workers = std::thread::available_parallelism()
1591 .map(usize::from)
1592 .unwrap_or(1)
1593 .min(tasks.len().max(1));
1594 let next = AtomicUsize::new(0);
1595 let outcomes = Mutex::new(Vec::<Result<ProcessOutcome, String>>::with_capacity(
1596 tasks.len(),
1597 ));
1598 std::thread::scope(|scope| {
1599 for _ in 0..workers {
1600 scope.spawn(|| {
1601 loop {
1602 let index = next.fetch_add(1, Ordering::Relaxed);
1603 let Some(task) = tasks.get(index) else { break };
1604 let result = crate::child_signal_guard::output(
1605 Command::new(&task.artifact.executable)
1606 .args(["--exact", &task.test])
1615 .args(&ignored_mode)
1616 .current_dir(&task.artifact.package_directory)
1617 .envs(instrumented_stack_environment())
1618 .envs(dynamic_library_environment(
1619 &target_libdir,
1620 &task.artifact.executable,
1621 ))
1622 .env("SUPERCOV_RUST_EVIDENCE_DIR", &task.directory)
1623 .env(
1624 crate::rust_probe_transport::RUST_CONTEXT_ENV,
1625 format!("{:016x}", task.context_id),
1626 ),
1627 )
1628 .map(|output| ProcessOutcome {
1629 task: ProcessTask {
1630 ordinal: task.ordinal,
1631 artifact_index: task.artifact_index,
1632 test_index: task.test_index,
1633 artifact: task.artifact.clone(),
1634 test: task.test.clone(),
1635 context_id: task.context_id,
1636 directory: task.directory.clone(),
1637 },
1638 output,
1639 })
1640 .map_err(|error| error.to_string());
1641 outcomes
1642 .lock()
1643 .expect("Rust test result lock poisoned")
1644 .push(result);
1645 }
1646 });
1647 }
1648 });
1649 let mut outcomes = outcomes
1650 .into_inner()
1651 .map_err(|_| RustTestRunnerError::Io("Rust test result lock poisoned".into()))?
1652 .into_iter()
1653 .map(|result| result.map_err(RustTestRunnerError::Launch))
1654 .collect::<Result<Vec<_>, _>>()?;
1655 outcomes.sort_by_key(|outcome| outcome.task.ordinal);
1656 for outcome in outcomes {
1657 let ProcessTask {
1658 artifact_index,
1659 test_index,
1660 artifact,
1661 test,
1662 directory,
1663 ..
1664 } = outcome.task;
1665 let test_id = format!("{}::{test}", artifact.source);
1669 let worker_id = format!("artifact-{artifact_index:04}");
1670 let attempt_id = format!("{run_id}:{artifact_index:04}:{test_index:08}");
1671 let output = outcome.output;
1672 let exit = output.status.code().unwrap_or(1);
1673 let stdout = String::from_utf8_lossy(&output.stdout);
1674 let skipped = libtest_skipped(exit, &stdout);
1675 if exit != 0 {
1676 writeln!(diagnostics, "[supercov] Rust test failed: {test_id}")
1677 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1678 diagnostics
1679 .write_all(&output.stdout)
1680 .and_then(|_| diagnostics.write_all(&output.stderr))
1681 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1682 }
1683 if exit != 0 {
1684 overall_exit = exit;
1685 }
1686 let evidence = snapshot(&project.manifest, &directory, &attempt_id)?;
1687 results.push(RawTestResult {
1688 test_id: Some(test_id.clone()),
1689 scope: Some(ExecutionScope {
1690 version: 1,
1691 run_id: run_id.into(),
1692 worker_id,
1693 test_id: test_id.clone(),
1694 test_key: format!("{}::{test}", artifact.source),
1695 retry: 0,
1696 attempt_id,
1697 }),
1698 test: test_id,
1699 test_file: Some(artifact.source.clone()),
1700 title: Some(test),
1701 retry: Some(0),
1702 status: Some(
1703 if exit != 0 {
1704 "failed"
1705 } else if skipped {
1706 "skipped"
1707 } else {
1708 "passed"
1709 }
1710 .into(),
1711 ),
1712 expected_status: Some("passed".into()),
1713 flaky: false,
1714 provenance: TestProvenance {
1715 runner: "rust-libtest".into(),
1716 kind: artifact.kind,
1717 project: Some(artifact.name),
1718 source: "supercov-owned-process-per-test".into(),
1719 },
1720 role: "test".into(),
1721 attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
1722 phases: evidence.phases,
1723 runtime: vec![evidence.snapshot],
1724 browser: Vec::new(),
1725 server: Vec::new(),
1726 });
1727 }
1728 let doctest_results =
1729 if selection.run_doctests && invocation.kind == RustCargoCommandKind::CargoTest {
1730 crate::rust_owned_doctests::run_doctests(
1731 project,
1732 &invocation,
1733 &selection,
1734 &evidence_root.join("doctests"),
1735 run_id,
1736 diagnostics,
1737 &mut overall_exit,
1738 )?
1739 } else {
1740 Vec::new()
1741 };
1742 let ran_libtests = !results.is_empty();
1745 let ran_doctests = !doctest_results.is_empty();
1746 results.extend(doctest_results);
1747 let mut runners = Vec::new();
1748 if ran_libtests || !ran_doctests {
1749 runners.push(rust_runner_declaration("rust-libtest"));
1750 }
1751 if ran_doctests {
1752 runners.push(rust_runner_declaration("rustdoc"));
1753 }
1754 let structural_limitations = structural_limitations(&project.manifest);
1755 Ok(RustFrontendRun {
1756 declaration: FrontendRunDeclaration {
1757 protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
1758 frontend_id: "rust".into(),
1759 frontend_version: "rust-owned-v1".into(),
1760 language: "rust".into(),
1761 structural_source: StructuralSource::OwnedProbes,
1762 runners,
1763 structural_limitations,
1764 },
1765 request: CoverageReportRequest {
1766 run_id: run_id.into(),
1767 manifest: project.manifest.clone(),
1768 raw_results: results,
1769 generated_at: generated_at.into(),
1770 coverage_model: Some(rust_coverage_model()),
1771 integrity: None,
1772 test_exit_code: ExitCodeInput::Present(Some(overall_exit)),
1773 },
1774 exit_code: overall_exit,
1775 artifacts: artifacts.len(),
1776 artifact_files: artifacts
1777 .iter()
1778 .map(|artifact| artifact.executable.clone())
1779 .collect(),
1780 build_ms,
1781 execution_ms: execution_started.elapsed().as_secs_f64() * 1000.0,
1782 })
1783}
1784
1785#[cfg(test)]
1786mod tests {
1787 use std::time::{SystemTime, UNIX_EPOCH};
1788
1789 use super::*;
1790 use crate::{
1791 coverage_report::{ArchiveReportRequest, analyze_coverage_archive},
1792 evidence_archive::write_archive,
1793 frontend_protocol::validate_frontend_report_request,
1794 rust_project::prepare_rust_project,
1795 };
1796
1797 #[test]
1798 fn cargo_and_libtest_selection_is_preserved_without_presentation_guessing() {
1799 let root = Path::new(".");
1800 let invocation = cargo_invocation(
1801 root,
1802 &[
1803 "cargo".into(),
1804 "test".into(),
1805 "-p".into(),
1806 "fixture".into(),
1807 "authored".into(),
1808 "--".into(),
1809 "generated".into(),
1810 "--skip".into(),
1811 "slow".into(),
1812 "--include-ignored".into(),
1813 ],
1814 )
1815 .unwrap();
1816 assert_eq!(invocation.arguments, ["test", "-p", "fixture", "authored"]);
1817 assert_eq!(
1818 invocation.runner_arguments,
1819 ["generated", "--skip", "slow", "--include-ignored"]
1820 );
1821 let selection = rust_libtest_selection(&invocation).unwrap();
1822 assert_eq!(
1823 selection.list_arguments,
1824 [
1825 "authored",
1826 "generated",
1827 "--skip",
1828 "slow",
1829 "--include-ignored"
1830 ]
1831 );
1832 }
1833
1834 #[test]
1835 fn direct_cargo_argv_preserves_toml_quotes_inside_config_values() {
1836 let config = "target.host.runner=[\"runner with spaces\",\"--fixed\"]";
1837 let invocation = cargo_invocation(
1838 Path::new("."),
1839 &[
1840 "cargo".into(),
1841 "test".into(),
1842 "--config".into(),
1843 config.into(),
1844 ],
1845 )
1846 .unwrap();
1847 assert_eq!(invocation.arguments, ["test", "--config", config]);
1848 }
1849
1850 #[test]
1851 fn nextest_run_is_detected_without_reclassifying_its_filters_or_retries() {
1852 let invocation = cargo_invocation(
1853 Path::new("."),
1854 &[
1855 "cargo".into(),
1856 "+1.95.0".into(),
1857 "nextest".into(),
1858 "run".into(),
1859 "--retries".into(),
1860 "2".into(),
1861 "-E".into(),
1862 "test(/flaky/)".into(),
1863 "--".into(),
1864 "--nocapture".into(),
1865 ],
1866 )
1867 .unwrap();
1868 assert_eq!(invocation.kind, RustCargoCommandKind::NextestRun);
1869 assert_eq!(
1870 invocation.arguments,
1871 [
1872 "+1.95.0",
1873 "nextest",
1874 "run",
1875 "--retries",
1876 "2",
1877 "-E",
1878 "test(/flaky/)",
1879 ]
1880 );
1881 assert_eq!(invocation.runner_arguments, ["--nocapture"]);
1882 let execution = rust_cargo_execution_selection(&invocation).unwrap();
1883 assert!(execution.run_libtests);
1884 assert!(!execution.run_doctests);
1885 assert!(execution.doctest_arguments.is_empty());
1886 assert!(rust_libtest_selection(&invocation).is_err());
1887 assert_eq!(
1888 nextest_list_invocation(&invocation).unwrap(),
1889 NextestListInvocation {
1890 arguments: vec![
1891 "+1.95.0".into(),
1892 "nextest".into(),
1893 "list".into(),
1894 "-E".into(),
1895 "test(/flaky/)".into(),
1896 "--message-format".into(),
1897 "json".into(),
1898 ],
1899 runner_arguments: vec!["--nocapture".into()],
1900 }
1901 );
1902 }
1903
1904 #[test]
1905 fn nextest_list_projection_preserves_selection_and_rejects_external_state() {
1906 let invocation = CargoTestInvocation {
1907 program: "cargo".into(),
1908 kind: RustCargoCommandKind::NextestRun,
1909 arguments: vec![
1910 "nextest".into(),
1911 "run".into(),
1912 "--package=fixture".into(),
1913 "--partition".into(),
1914 "hash:1/2".into(),
1915 "--test-threads=8".into(),
1916 "--failure-output".into(),
1917 "final".into(),
1918 "name".into(),
1919 ],
1920 runner_arguments: vec!["--exact".into(), "full::name".into()],
1921 };
1922 assert_eq!(
1923 nextest_list_invocation(&invocation).unwrap(),
1924 NextestListInvocation {
1925 arguments: vec![
1926 "nextest".into(),
1927 "list".into(),
1928 "--package=fixture".into(),
1929 "--partition".into(),
1930 "hash:1/2".into(),
1931 "name".into(),
1932 "--message-format".into(),
1933 "json".into(),
1934 ],
1935 runner_arguments: vec!["--exact".into(), "full::name".into()],
1936 }
1937 );
1938
1939 let mut rerun = invocation;
1940 rerun.arguments.extend(["--rerun".into(), "latest".into()]);
1941 assert!(
1942 nextest_list_invocation(&rerun)
1943 .unwrap_err()
1944 .to_string()
1945 .contains("cannot yet be assigned exact selected-test identity")
1946 );
1947 }
1948
1949 #[test]
1950 fn nextest_list_projection_preserves_post_separator_libtest_selection() {
1951 let invocation = cargo_invocation(
1952 Path::new("."),
1953 &[
1954 "cargo".into(),
1955 "nextest".into(),
1956 "run".into(),
1957 "--timings".into(),
1958 "-vv".into(),
1959 "--".into(),
1960 "--include-ignored".into(),
1961 "--skip".into(),
1962 "slow".into(),
1963 "--exact".into(),
1964 "tests::selected".into(),
1965 ],
1966 )
1967 .unwrap();
1968 assert_eq!(
1969 nextest_list_invocation(&invocation).unwrap(),
1970 NextestListInvocation {
1971 arguments: vec![
1972 "nextest".to_owned(),
1973 "list".to_owned(),
1974 "--timings".to_owned(),
1975 "-vv".to_owned(),
1976 "--message-format".to_owned(),
1977 "json".to_owned(),
1978 ],
1979 runner_arguments: vec![
1980 "--include-ignored".to_owned(),
1981 "--skip".to_owned(),
1982 "slow".to_owned(),
1983 "--exact".to_owned(),
1984 "tests::selected".to_owned(),
1985 ],
1986 }
1987 );
1988 }
1989
1990 #[test]
1991 fn nextest_version_handshake_preserves_the_cargo_toolchain_selector() {
1992 let invocation = CargoTestInvocation {
1993 program: "cargo".into(),
1994 kind: RustCargoCommandKind::NextestRun,
1995 arguments: vec![
1996 "+1.95.0".into(),
1997 "nextest".into(),
1998 "run".into(),
1999 "-p".into(),
2000 "fixture".into(),
2001 ],
2002 runner_arguments: Vec::new(),
2003 };
2004 assert_eq!(
2005 nextest_version_arguments(&invocation).unwrap(),
2006 ["+1.95.0", "nextest", "--version"]
2007 );
2008 }
2009
2010 #[test]
2011 fn stock_libtest_presentation_and_scheduling_options_do_not_change_discovery() {
2012 let invocation = CargoTestInvocation {
2013 program: "cargo".into(),
2014 kind: RustCargoCommandKind::CargoTest,
2015 arguments: vec!["test".into(), "cargo-filter".into()],
2016 runner_arguments: [
2017 "runner-filter",
2018 "--nocapture",
2019 "--show-output",
2020 "--format=json",
2021 "--color",
2022 "never",
2023 "--test-threads=4",
2024 "--fail-fast",
2025 "--shuffle-seed",
2026 "17",
2027 "-Zunstable-options",
2028 "--exclude-should-panic",
2029 ]
2030 .into_iter()
2031 .map(str::to_owned)
2032 .collect(),
2033 };
2034 let selection = rust_libtest_selection(&invocation).unwrap();
2035 assert_eq!(
2036 selection.list_arguments,
2037 [
2038 "cargo-filter",
2039 "runner-filter",
2040 "-Zunstable-options",
2041 "--exclude-should-panic"
2042 ]
2043 );
2044 }
2045
2046 #[test]
2047 fn cargo_test_options_are_not_mistaken_for_the_test_name_filter() {
2048 let invocation = CargoTestInvocation {
2049 program: "cargo".into(),
2050 kind: RustCargoCommandKind::CargoTest,
2051 arguments: vec![
2052 "test".into(),
2053 "--manifest-path".into(),
2054 "nested/Cargo.toml".into(),
2055 "--features=one,two".into(),
2056 "needle".into(),
2057 ],
2058 runner_arguments: vec!["--ignored".into(), "other".into()],
2059 };
2060 let selection = rust_libtest_selection(&invocation).unwrap();
2061 assert_eq!(selection.list_arguments, ["needle", "--ignored", "other"]);
2062 }
2063
2064 #[test]
2065 fn libtest_thread_count_is_preserved_as_runner_scheduling() {
2066 for arguments in [vec!["--test-threads", "1"], vec!["--test-threads=8"]] {
2067 let invocation = CargoTestInvocation {
2068 program: "cargo".into(),
2069 kind: RustCargoCommandKind::CargoTest,
2070 arguments: vec!["test".into()],
2071 runner_arguments: arguments.into_iter().map(str::to_owned).collect(),
2072 };
2073 let selection = rust_libtest_selection(&invocation).unwrap();
2074 assert!(selection.list_arguments.is_empty());
2075 }
2076 }
2077
2078 #[test]
2079 fn invalid_or_duplicate_libtest_thread_counts_fail_closed() {
2080 for arguments in [
2081 vec!["--test-threads"],
2082 vec!["--test-threads=0"],
2083 vec!["--test-threads=abc"],
2084 vec!["--test-threads", "1", "--test-threads=2"],
2085 ] {
2086 let invocation = CargoTestInvocation {
2087 program: "cargo".into(),
2088 kind: RustCargoCommandKind::CargoTest,
2089 arguments: vec!["test".into()],
2090 runner_arguments: arguments.into_iter().map(str::to_owned).collect(),
2091 };
2092 assert!(rust_libtest_selection(&invocation).is_err());
2093 }
2094 }
2095
2096 #[test]
2097 fn cargo_target_selection_reproduces_when_cargo_runs_doctests() {
2098 let invocation = CargoTestInvocation {
2099 program: "cargo".into(),
2100 kind: RustCargoCommandKind::CargoTest,
2101 arguments: vec![
2102 "test".into(),
2103 "-p".into(),
2104 "fixture".into(),
2105 "needle".into(),
2106 ],
2107 runner_arguments: vec!["--include-ignored".into()],
2108 };
2109 let selection = rust_cargo_execution_selection(&invocation).unwrap();
2110 assert!(selection.run_libtests);
2111 assert!(selection.run_doctests);
2112 assert_eq!(
2113 selection.doctest_arguments,
2114 [
2115 "test",
2116 "--doc",
2117 "-p",
2118 "fixture",
2119 "needle",
2120 "--",
2121 "--include-ignored"
2122 ]
2123 );
2124
2125 let mut explicit_doc = invocation.clone();
2126 explicit_doc.arguments.insert(1, "--doc".into());
2127 let selection = rust_cargo_execution_selection(&explicit_doc).unwrap();
2128 assert!(!selection.run_libtests);
2129 assert!(selection.run_doctests);
2130
2131 for target in ["--lib", "--tests", "--all-targets", "--example=demo"] {
2132 let mut selected = invocation.clone();
2133 selected.arguments.insert(1, target.into());
2134 let selection = rust_cargo_execution_selection(&selected).unwrap();
2135 assert!(selection.run_libtests);
2136 assert!(!selection.run_doctests);
2137 }
2138 }
2139
2140 #[test]
2141 fn a_module_the_build_never_compiles_leaves_the_denominator() {
2142 let nonce = SystemTime::now()
2146 .duration_since(UNIX_EPOCH)
2147 .unwrap()
2148 .as_nanos();
2149 let root = std::env::temp_dir().join(format!(
2150 "supercov-rust-runner-cfg-{}-{nonce}",
2151 std::process::id()
2152 ));
2153 fs::create_dir_all(root.join("src")).unwrap();
2154 fs::write(
2155 root.join("Cargo.toml"),
2156 "[package]\nname='fixture'\nversion='0.0.0'\nedition='2024'\n\n[features]\nextra=[]\n",
2157 )
2158 .unwrap();
2159 fs::write(
2160 root.join("src/lib.rs"),
2161 r#"
2162#[cfg(feature = "extra")]
2163mod extra;
2164
2165pub fn kept(value: i32) -> i32 {
2166 if value > 0 { value } else { -value }
2167}
2168#[cfg(test)]
2169mod tests {
2170 #[test] fn positive() { assert_eq!(super::kept(2), 2); }
2171 #[test] fn negative() { assert_eq!(super::kept(-2), 2); }
2172}
2173"#,
2174 )
2175 .unwrap();
2176 fs::write(
2177 root.join("src/extra.rs"),
2178 "pub fn never_built(value: i32) -> i32 {\n if value > 0 { 1 } else { 0 }\n}\n",
2179 )
2180 .unwrap();
2181 let project = prepare_rust_project(&root, None).unwrap();
2182 assert!(
2184 project
2185 .source_files
2186 .iter()
2187 .any(|file| file == "src/extra.rs"),
2188 "{:?}",
2189 project.source_files
2190 );
2191 let run = run_prepared_rust_tests(
2192 &project,
2193 &["cargo".into(), "test".into(), "--lib".into()],
2194 "rust-fixture-cfg",
2195 "2026-08-26T00:00:00.000Z",
2196 &mut Vec::new(),
2197 )
2198 .unwrap();
2199 assert_eq!(run.exit_code, 0);
2200 let manifest = &run.request.manifest;
2203 assert!(
2204 manifest
2205 .points
2206 .iter()
2207 .any(|point| point.file == "src/extra.rs"),
2208 "the obligations must stay in the manifest"
2209 );
2210 let declined = manifest.unmeasured.iter().collect::<BTreeSet<_>>();
2211 for point in &manifest.points {
2212 assert_eq!(
2213 declined.contains(&point.id),
2214 point.file == "src/extra.rs",
2215 "{} in {}",
2216 point.id,
2217 point.file
2218 );
2219 }
2220 for decision in &manifest.decisions {
2221 assert_eq!(
2222 declined.contains(&decision.id),
2223 decision.file == "src/extra.rs"
2224 );
2225 }
2226 let report =
2228 crate::frontend_protocol::analyze_frontend_results(&run.declaration, &run.request)
2229 .unwrap();
2230 assert_eq!(
2231 report.view.summary.lines.percentage, 100.0,
2232 "declined obligations must not read as uncovered"
2233 );
2234 fs::remove_dir_all(&root).ok();
2235 }
2236
2237 #[test]
2238 fn assertion_occurrences_are_retained_without_automatic_asserted_credit() {
2239 let nonce = SystemTime::now()
2244 .duration_since(UNIX_EPOCH)
2245 .unwrap()
2246 .as_nanos();
2247 let root = std::env::temp_dir().join(format!(
2248 "supercov-rust-runner-assert-{}-{nonce}",
2249 std::process::id()
2250 ));
2251 fs::create_dir_all(root.join("src")).unwrap();
2252 fs::write(
2253 root.join("Cargo.toml"),
2254 "[package]\nname='fixture'\nversion='0.0.0'\nedition='2024'\n",
2255 )
2256 .unwrap();
2257 fs::write(
2258 root.join("src/lib.rs"),
2259 r#"
2260pub fn checked(value: i32) -> i32 {
2261 value * 2
2262}
2263pub fn unchecked(value: i32) -> i32 {
2264 value + 1
2265}
2266#[cfg(test)]
2267mod tests {
2268 #[test]
2269 fn asserts() {
2270 let doubled = super::checked(2);
2271 assert_eq!(doubled, 4);
2272 }
2273 #[test]
2274 fn asserts_nothing() {
2275 let _ = super::unchecked(1);
2276 }
2277}
2278"#,
2279 )
2280 .unwrap();
2281 let project = prepare_rust_project(&root, None).unwrap();
2282 let run = run_prepared_rust_tests(
2283 &project,
2284 &["cargo".into(), "test".into(), "--lib".into()],
2285 "rust-fixture-assert",
2286 "2026-08-26T00:00:00.000Z",
2287 &mut Vec::new(),
2288 )
2289 .unwrap();
2290 assert_eq!(run.exit_code, 0);
2291
2292 let phases = |test: &str| {
2295 run.request
2296 .raw_results
2297 .iter()
2298 .find(|result| result.test.ends_with(test))
2299 .unwrap_or_else(|| panic!("no test {test}"))
2300 .phases
2301 .clone()
2302 };
2303 let asserting = phases("asserts");
2304 assert!(
2305 !asserting.is_empty(),
2306 "the asserting test recorded no phase"
2307 );
2308 assert!(
2309 asserting
2310 .iter()
2311 .all(|phase| phase.kind == "assertion" && phase.status.as_deref() == Some("passed"))
2312 );
2313 assert!(
2314 phases("asserts_nothing").is_empty(),
2315 "a test that checks nothing witnesses nothing"
2316 );
2317
2318 let report =
2319 crate::frontend_protocol::analyze_frontend_results(&run.declaration, &run.request)
2320 .unwrap();
2321 let level = |line: usize| {
2322 report
2323 .view
2324 .lines
2325 .iter()
2326 .find(|entry| entry.file == "src/lib.rs" && entry.line == line)
2327 .map(|entry| entry.confidence.level.clone())
2328 .unwrap_or_else(|| panic!("no line {line}"))
2329 };
2330 assert_eq!(level(3), "executed");
2332 assert_eq!(level(6), "executed");
2333 fs::remove_dir_all(&root).ok();
2334 }
2335
2336 #[test]
2337 fn libtest_filters_select_the_tests_plain_cargo_would_run() {
2338 let nonce = SystemTime::now()
2342 .duration_since(UNIX_EPOCH)
2343 .unwrap()
2344 .as_nanos();
2345 let root = std::env::temp_dir().join(format!(
2346 "supercov-rust-runner-filters-{}-{nonce}",
2347 std::process::id()
2348 ));
2349 fs::create_dir_all(root.join("src")).unwrap();
2350 fs::write(
2351 root.join("Cargo.toml"),
2352 "[package]\nname='fixture'\nversion='0.0.0'\nedition='2024'\n",
2353 )
2354 .unwrap();
2355 fs::write(
2356 root.join("src/lib.rs"),
2357 r#"
2358pub fn value(flag: bool) -> i32 { if flag { 1 } else { 0 } }
2359#[cfg(test)]
2360mod tests {
2361 #[test] fn alpha() { assert_eq!(super::value(true), 1); }
2362 #[test] fn beta() { assert_eq!(super::value(false), 0); }
2363 #[test] #[ignore] fn gamma_slow() { assert_eq!(super::value(true), 1); }
2364}
2365"#,
2366 )
2367 .unwrap();
2368 let project = prepare_rust_project(&root, None).unwrap();
2369 let names = |run: &str, command: &[&str]| {
2370 let run = run_prepared_rust_tests(
2371 &project,
2372 &command
2373 .iter()
2374 .map(|word| (*word).into())
2375 .collect::<Vec<String>>(),
2376 run,
2377 "2026-08-26T00:00:00.000Z",
2378 &mut Vec::new(),
2379 )
2380 .unwrap();
2381 assert_eq!(run.exit_code, 0, "{:?}", run.request.raw_results);
2382 let mut names = run
2383 .request
2384 .raw_results
2385 .iter()
2386 .map(|result| {
2387 (
2388 result.test.rsplit("::").next().unwrap().to_owned(),
2389 result.status.clone().unwrap(),
2390 )
2391 })
2392 .collect::<Vec<_>>();
2393 names.sort();
2394 names
2395 };
2396 assert_eq!(
2398 names("rust-fixture-filters-1", &["cargo", "test", "--lib"]),
2399 [
2400 ("alpha".to_owned(), "passed".to_owned()),
2401 ("beta".to_owned(), "passed".to_owned()),
2402 ("gamma_slow".to_owned(), "skipped".to_owned()),
2403 ]
2404 );
2405 assert_eq!(
2407 names(
2408 "rust-fixture-filters-2",
2409 &["cargo", "test", "--lib", "alpha"]
2410 ),
2411 [("alpha".to_owned(), "passed".to_owned())]
2412 );
2413 assert_eq!(
2414 names(
2415 "rust-fixture-filters-3",
2416 &["cargo", "test", "--lib", "--", "--skip", "beta"]
2417 ),
2418 [
2419 ("alpha".to_owned(), "passed".to_owned()),
2420 ("gamma_slow".to_owned(), "skipped".to_owned()),
2421 ]
2422 );
2423 assert_eq!(
2425 names(
2426 "rust-fixture-filters-4",
2427 &["cargo", "test", "--lib", "--", "--ignored"]
2428 ),
2429 [("gamma_slow".to_owned(), "passed".to_owned())]
2430 );
2431 fs::remove_dir_all(&root).ok();
2432 }
2433
2434 #[test]
2435 fn libtest_processes_run_in_their_package_directory() {
2436 let nonce = SystemTime::now()
2440 .duration_since(UNIX_EPOCH)
2441 .unwrap()
2442 .as_nanos();
2443 let root = std::env::temp_dir().join(format!(
2444 "supercov-rust-runner-cwd-{}-{nonce}",
2445 std::process::id()
2446 ));
2447 fs::create_dir_all(root.join("member/src")).unwrap();
2448 fs::write(
2449 root.join("Cargo.toml"),
2450 "[workspace]\nmembers = ['member']\nresolver = '2'\n",
2451 )
2452 .unwrap();
2453 fs::write(
2454 root.join("member/Cargo.toml"),
2455 "[package]\nname='member'\nversion='0.0.0'\nedition='2024'\n",
2456 )
2457 .unwrap();
2458 fs::write(
2459 root.join("member/src/lib.rs"),
2460 r#"
2461pub fn manifest() -> String {
2462 std::fs::read_to_string("Cargo.toml").unwrap()
2463}
2464#[cfg(test)]
2465mod tests {
2466 #[test] fn reads_own_manifest() { assert!(super::manifest().contains("name='member'")); }
2467}
2468"#,
2469 )
2470 .unwrap();
2471 let project = prepare_rust_project(&root, None).unwrap();
2472 let run = run_prepared_rust_tests(
2473 &project,
2474 &["cargo".into(), "test".into(), "--lib".into()],
2475 "rust-fixture-cwd",
2476 "2026-08-26T00:00:00.000Z",
2477 &mut Vec::new(),
2478 )
2479 .unwrap();
2480 assert_eq!(run.exit_code, 0, "{:?}", run.request.raw_results);
2481 assert_eq!(run.request.raw_results.len(), 1);
2482 assert_eq!(run.request.raw_results[0].status.as_deref(), Some("passed"));
2483 fs::remove_dir_all(&root).ok();
2484 }
2485
2486 #[test]
2487 fn cargo_libtest_runs_produce_queryable_owned_evidence() {
2488 let nonce = SystemTime::now()
2489 .duration_since(UNIX_EPOCH)
2490 .unwrap()
2491 .as_nanos();
2492 let root = std::env::temp_dir().join(format!(
2493 "supercov-rust-runner-{}-{nonce}",
2494 std::process::id()
2495 ));
2496 fs::create_dir_all(root.join("src")).unwrap();
2497 fs::write(
2498 root.join("Cargo.toml"),
2499 "[package]\nname='fixture'\nversion='0.0.0'\nedition='2024'\n",
2500 )
2501 .unwrap();
2502 fs::write(
2503 root.join("src/lib.rs"),
2504 r#"
2505pub fn choose(left: bool, right: bool) -> i32 {
2506 if left && right { 1 } else { 0 }
2507}
2508pub fn pick(value: i32) -> &'static str {
2509 match value {
2510 0 => "zero",
2511 1 => "one",
2512 _ => "many",
2513 }
2514}
2515pub fn total(values: &[i32]) -> i32 {
2516 let mut sum = 0;
2517 for value in values {
2518 sum += value;
2519 }
2520 sum
2521}
2522pub fn first_even(values: &[i32]) -> Option<i32> {
2523 let mut index = 0;
2524 while index < values.len() {
2525 if values[index] % 2 == 0 {
2526 return Some(values[index]);
2527 }
2528 index += 1;
2529 }
2530 None
2531}
2532pub fn parse_twice(text: &str) -> Option<i32> {
2533 let value: i32 = text.parse().ok()?;
2534 Some(value * 2)
2535}
2536pub fn describe(value: Option<i32>, flag: bool) -> &'static str {
2537 if let Some(inner) = value && inner > 0 && flag {
2538 "positive"
2539 } else {
2540 "other"
2541 }
2542}
2543pub fn depth(n: u32) -> Result<u32, String> {
2544 if n == 0 {
2545 Ok(0)
2546 } else {
2547 let below = depth(n - 1)?;
2548 Ok(below + 1)
2549 }
2550}
2551#[cfg(test)]
2552mod tests {
2553 #[test] fn false_path() { assert_eq!(super::choose(false, true), 0); }
2554 #[test] fn true_path() { assert_eq!(super::choose(true, true), 1); }
2555 #[test] #[ignore] fn ignored_path() { unreachable!(); }
2556 #[test] fn pick_zero() { assert_eq!(super::pick(0), "zero"); }
2557 #[test] fn pick_many() { assert_eq!(super::pick(7), "many"); }
2558 #[test] fn total_empty() { assert_eq!(super::total(&[]), 0); }
2559 #[test] fn total_some() { assert_eq!(super::total(&[1, 2]), 3); }
2560 #[test] fn first_even_empty() { assert_eq!(super::first_even(&[]), None); }
2561 #[test] fn first_even_found() { assert_eq!(super::first_even(&[1, 4]), Some(4)); }
2562 #[test] fn parse_ok() { assert_eq!(super::parse_twice("4"), Some(8)); }
2563 #[test] fn parse_bad() { assert_eq!(super::parse_twice("x"), None); }
2564 #[test] fn chain_taken() { assert_eq!(super::describe(Some(1), true), "positive"); }
2565 #[test] fn chain_pattern_fails() { assert_eq!(super::describe(None, true), "other"); }
2566 #[test] fn chain_negative() { assert_eq!(super::describe(Some(-1), true), "other"); }
2567 #[test] fn chain_flag_fails() { assert_eq!(super::describe(Some(1), false), "other"); }
2568 #[test] fn deep_recursion() { assert_eq!(super::depth(5_000), Ok(5_000)); }
2569}
2570"#,
2571 )
2572 .unwrap();
2573 let project = prepare_rust_project(&root, None).unwrap();
2574 let run = run_prepared_rust_tests(
2578 &project,
2579 &["cargo".into(), "test".into(), "--lib".into()],
2580 "rust-fixture",
2581 "2026-08-26T00:00:00.000Z",
2582 &mut Vec::new(),
2583 )
2584 .unwrap();
2585 assert_eq!(run.exit_code, 0);
2586 assert_eq!(run.request.raw_results.len(), 16);
2587 let statuses = run
2588 .request
2589 .raw_results
2590 .iter()
2591 .filter_map(|result| result.status.as_deref())
2592 .collect::<Vec<_>>();
2593 assert_eq!(
2594 statuses
2595 .iter()
2596 .filter(|status| **status == "skipped")
2597 .count(),
2598 1
2599 );
2600 assert_eq!(
2601 statuses
2602 .iter()
2603 .filter(|status| **status == "passed")
2604 .count(),
2605 15
2606 );
2607
2608 let chain_vectors = |test: &str| {
2611 let result = run
2612 .request
2613 .raw_results
2614 .iter()
2615 .find(|result| result.test.ends_with(test))
2616 .unwrap_or_else(|| panic!("no test {test}"));
2617 let snapshot = result
2618 .runtime
2619 .iter()
2620 .flat_map(|snapshot| &snapshot.decisions)
2621 .find(|decision| decision.meta.source.starts_with("let Some(inner) = value"))
2622 .unwrap_or_else(|| panic!("{test} recorded no chain decision"));
2623 snapshot
2624 .vectors
2625 .iter()
2626 .map(|vector| (vector.values.clone(), vector.outcome))
2627 .collect::<Vec<_>>()
2628 };
2629 assert_eq!(
2630 chain_vectors("chain_taken"),
2631 [(vec![Some(true), Some(true), Some(true)], true)]
2632 );
2633 assert_eq!(
2634 chain_vectors("chain_pattern_fails"),
2635 [(vec![Some(false), None, None], false)]
2636 );
2637 assert_eq!(
2638 chain_vectors("chain_negative"),
2639 [(vec![Some(true), Some(false), None], false)]
2640 );
2641 assert_eq!(
2642 chain_vectors("chain_flag_fails"),
2643 [(vec![Some(true), Some(true), Some(false)], false)]
2644 );
2645 validate_frontend_report_request(&run.declaration, &run.request).unwrap();
2646 let archive = root.join("evidence.raw.gz");
2647 write_archive(run.archive_entries().unwrap(), &archive).unwrap();
2648 let report = analyze_coverage_archive(&ArchiveReportRequest {
2649 archive_path: archive,
2650 run_id: "rust-fixture".into(),
2651 generated_at: "2026-08-26T00:00:00.000Z".into(),
2652 integrity: None,
2653 test_exit_code: ExitCodeInput::Present(Some(0)),
2654 })
2655 .unwrap();
2656 assert_eq!(report.view.tests.len(), 16);
2657 assert!(report.view.summary.lines.covered > 0);
2658 assert!(report.view.summary.decisions > 0);
2659
2660 let single = |kind: &str| {
2663 let mut found = report
2664 .view
2665 .branches
2666 .iter()
2667 .filter(|branch| branch.meta.kind == kind);
2668 let branch = found.next().unwrap_or_else(|| panic!("no {kind} branch"));
2669 assert!(found.next().is_none(), "more than one {kind} branch");
2670 branch
2671 };
2672 let tests_of = |branch: &crate::coverage_report::BranchResult, label: &str| {
2673 branch
2674 .alternatives
2675 .iter()
2676 .find(|alternative| alternative.label == label)
2677 .unwrap_or_else(|| panic!("{} has no alternative {label}", branch.meta.kind))
2678 .tests
2679 .clone()
2680 };
2681 let for_loop = single("for-loop");
2682 assert_eq!(
2683 tests_of(for_loop, "zero iterations"),
2684 ["src/lib.rs::tests::total_empty"]
2685 );
2686 assert_eq!(
2687 tests_of(for_loop, "entered"),
2688 ["src/lib.rs::tests::total_some"]
2689 );
2690 let while_loop = single("while-loop");
2691 assert_eq!(
2692 tests_of(while_loop, "zero iterations"),
2693 ["src/lib.rs::tests::first_even_empty"]
2694 );
2695 assert_eq!(
2696 tests_of(while_loop, "entered"),
2697 ["src/lib.rs::tests::first_even_found"]
2698 );
2699 let try_operator = report
2702 .view
2703 .branches
2704 .iter()
2705 .find(|branch| {
2706 branch.meta.kind == "try-operator" && branch.meta.source.contains("parse().ok()")
2707 })
2708 .expect("parse_twice's try operator");
2709 assert_eq!(
2710 tests_of(try_operator, "continued"),
2711 ["src/lib.rs::tests::parse_ok"]
2712 );
2713 assert_eq!(
2714 tests_of(try_operator, "early return"),
2715 ["src/lib.rs::tests::parse_bad"]
2716 );
2717 let mut logical = report
2718 .view
2719 .branches
2720 .iter()
2721 .filter(|branch| branch.meta.kind == "logical-and")
2722 .collect::<Vec<_>>();
2723 logical.sort_by_key(|branch| (branch.meta.line, branch.meta.column));
2724 assert_eq!(logical.len(), 3);
2726 assert_eq!(
2727 tests_of(logical[0], "short-circuited"),
2728 ["src/lib.rs::tests::false_path"]
2729 );
2730 assert_eq!(
2731 tests_of(logical[0], "right operand evaluated"),
2732 ["src/lib.rs::tests::true_path"]
2733 );
2734 assert_eq!(
2735 tests_of(logical[1], "short-circuited"),
2736 ["src/lib.rs::tests::chain_pattern_fails"]
2737 );
2738 assert_eq!(
2739 tests_of(logical[1], "right operand evaluated"),
2740 [
2741 "src/lib.rs::tests::chain_flag_fails",
2742 "src/lib.rs::tests::chain_negative",
2743 "src/lib.rs::tests::chain_taken",
2744 ]
2745 );
2746 assert_eq!(
2747 tests_of(logical[2], "short-circuited"),
2748 [
2749 "src/lib.rs::tests::chain_negative",
2750 "src/lib.rs::tests::chain_pattern_fails",
2751 ]
2752 );
2753 assert_eq!(
2754 tests_of(logical[2], "right operand evaluated"),
2755 [
2756 "src/lib.rs::tests::chain_flag_fails",
2757 "src/lib.rs::tests::chain_taken",
2758 ]
2759 );
2760 assert!(for_loop.covered && while_loop.covered && try_operator.covered);
2761 assert!(logical.iter().all(|branch| branch.covered));
2762
2763 let mut arms = report
2766 .view
2767 .branches
2768 .iter()
2769 .filter(|branch| branch.meta.kind == "match-arm")
2770 .collect::<Vec<_>>();
2771 arms.sort_by_key(|branch| branch.meta.line);
2772 assert_eq!(arms.len(), 3);
2773 let alternative = |arm: usize, label: &str| {
2774 arms[arm]
2775 .alternatives
2776 .iter()
2777 .find(|alternative| alternative.label == label)
2778 .unwrap_or_else(|| panic!("arm {arm} has no alternative {label}"))
2779 };
2780 assert_eq!(
2781 alternative(0, "selected").tests,
2782 ["src/lib.rs::tests::pick_zero"]
2783 );
2784 assert_eq!(
2785 alternative(0, "not selected").tests,
2786 ["src/lib.rs::tests::pick_many"]
2787 );
2788 assert!(!alternative(1, "selected").covered);
2789 assert_eq!(
2790 alternative(1, "not selected").tests,
2791 ["src/lib.rs::tests::pick_many"]
2792 );
2793 assert_eq!(arms[2].alternatives.len(), 1);
2794 assert_eq!(
2795 alternative(2, "selected").tests,
2796 ["src/lib.rs::tests::pick_many"]
2797 );
2798 assert!(arms[0].covered && !arms[1].covered && arms[2].covered);
2799
2800 fs::remove_dir_all(root).unwrap();
2801 }
2802}