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_region_directory},
42 rust_test_context::preflight_rust_test_contexts,
43};
44
45#[derive(Debug, Clone, PartialEq)]
46pub struct RustFrontendRun {
47 pub declaration: FrontendRunDeclaration,
48 pub request: CoverageReportRequest,
49 pub exit_code: i32,
50 pub artifacts: usize,
51 pub artifact_files: Vec<PathBuf>,
52 pub build_ms: f64,
53 pub execution_ms: f64,
54}
55
56impl RustFrontendRun {
57 pub fn archive_entries(&self) -> Result<Vec<EvidenceArchiveEntry>, serde_json::Error> {
58 let model = PersistedCoverageModel::from_declaration(
59 self.request
60 .coverage_model
61 .as_ref()
62 .expect("Rust frontend always declares a coverage model"),
63 )
64 .expect("Rust coverage model is contract-valid");
65 let mut entries = vec![
66 EvidenceArchiveEntry {
67 path: "coverage-model.json".into(),
68 contents: serde_json::to_vec(&model)?,
69 },
70 EvidenceArchiveEntry {
71 path: "frontend.json".into(),
72 contents: serde_json::to_vec(&self.declaration)?,
73 },
74 EvidenceArchiveEntry {
75 path: "manifest.json".into(),
76 contents: serde_json::to_vec(&self.request.manifest)?,
77 },
78 ];
79 for (index, result) in self.request.raw_results.iter().enumerate() {
80 entries.push(EvidenceArchiveEntry {
81 path: format!("results/{index:08}/mcdc.json"),
82 contents: serde_json::to_vec(result)?,
83 });
84 }
85 Ok(entries)
86 }
87}
88
89#[derive(Debug)]
90pub enum RustTestRunnerError {
91 UnsupportedCommand(String),
92 Launch(String),
93 CargoFailed(String),
94 CargoJson(String),
95 UnsafeArtifact(String),
96 ListFailed(String),
97 Probe(String),
98 Context(String),
99 UnknownProbe(String),
100 InvalidVector {
101 id: String,
102 expected: usize,
103 actual: usize,
104 },
105 Json(serde_json::Error),
106 Io(String),
107}
108
109impl std::fmt::Display for RustTestRunnerError {
110 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 match self {
112 Self::UnsupportedCommand(reason) => formatter.write_str(reason),
113 Self::Launch(reason) => {
114 write!(formatter, "could not launch Rust test process: {reason}")
115 }
116 Self::CargoFailed(reason) => write!(formatter, "Cargo test build failed: {reason}"),
117 Self::CargoJson(reason) => write!(formatter, "invalid Cargo JSON output: {reason}"),
118 Self::UnsafeArtifact(path) => {
119 write!(formatter, "Cargo emitted an unsafe test artifact: {path}")
120 }
121 Self::ListFailed(reason) => {
122 write!(formatter, "could not enumerate Rust tests: {reason}")
123 }
124 Self::Probe(reason) => write!(formatter, "invalid Rust probe evidence: {reason}"),
125 Self::Context(reason) => write!(formatter, "invalid Rust test context: {reason}"),
126 Self::UnknownProbe(id) => write!(
127 formatter,
128 "Rust runtime emitted an unknown obligation: {id}"
129 ),
130 Self::InvalidVector {
131 id,
132 expected,
133 actual,
134 } => write!(
135 formatter,
136 "Rust decision {id} emitted vector width {actual}; expected {expected}"
137 ),
138 Self::Json(error) => write!(formatter, "could not encode Rust evidence: {error}"),
139 Self::Io(reason) => formatter.write_str(reason),
140 }
141 }
142}
143
144impl std::error::Error for RustTestRunnerError {}
145
146impl From<serde_json::Error> for RustTestRunnerError {
147 fn from(value: serde_json::Error) -> Self {
148 Self::Json(value)
149 }
150}
151
152#[derive(Debug, Deserialize)]
153struct CargoMessage {
154 reason: String,
155 #[serde(default)]
156 target: Option<CargoArtifactTarget>,
157 #[serde(default)]
158 profile: Option<CargoArtifactProfile>,
159 executable: Option<PathBuf>,
160 manifest_path: Option<PathBuf>,
161}
162
163#[derive(Debug, Deserialize)]
164struct CargoArtifactTarget {
165 name: String,
166 kind: Vec<String>,
167 src_path: PathBuf,
168}
169
170#[derive(Debug, Deserialize)]
171struct CargoArtifactProfile {
172 test: bool,
173}
174
175#[derive(Debug, Clone)]
176struct TestArtifact {
177 executable: PathBuf,
178 name: String,
179 kind: String,
180 source: String,
181 package_directory: PathBuf,
184}
185
186#[derive(Debug)]
187struct ProcessTask {
188 ordinal: usize,
189 artifact_index: usize,
190 test_index: usize,
191 artifact: TestArtifact,
192 test: String,
193 context_id: u64,
194 directory: PathBuf,
195}
196
197#[derive(Debug)]
198struct ProcessOutcome {
199 task: ProcessTask,
200 output: Output,
201}
202
203fn shell_words(value: &str) -> Result<Vec<String>, RustTestRunnerError> {
207 let mut words = Vec::new();
208 let mut current = String::new();
209 let mut started = false;
211 let mut quote = None;
212 let mut characters = value.chars().peekable();
213 while let Some(character) = characters.next() {
214 match (quote, character) {
215 (Some('\''), '\'') | (Some('"'), '"') => quote = None,
216 (Some('\''), _) => current.push(character),
217 (Some(_), '\\') => match characters.peek() {
218 Some('\n') => {
220 characters.next();
221 }
222 Some('$' | '`' | '"' | '\\') => {
223 current.extend(characters.next());
224 }
225 Some(_) => current.push('\\'),
226 None => quote = Some('\\'),
227 },
228 (Some(_), _) => current.push(character),
229 (None, '\\') => match characters.next() {
230 Some('\n') => {}
231 Some(escaped) => {
232 current.push(escaped);
233 started = true;
234 }
235 None => quote = Some('\\'),
236 },
237 (None, '\'' | '"') => {
238 quote = Some(character);
239 started = true;
240 }
241 (None, _) if character.is_whitespace() => {
242 if std::mem::take(&mut started) {
243 words.push(std::mem::take(&mut current));
244 }
245 }
246 (None, _) => {
247 current.push(character);
248 started = true;
249 }
250 }
251 }
252 if quote.is_some() {
253 return Err(RustTestRunnerError::UnsupportedCommand(
254 "the expanded Cargo command contains an incomplete quote or escape".into(),
255 ));
256 }
257 if started {
258 words.push(current);
259 }
260 Ok(words)
261}
262
263fn executable_name(value: &str) -> &str {
264 Path::new(value)
265 .file_name()
266 .and_then(|name| name.to_str())
267 .unwrap_or(value)
268 .trim_end_matches(".exe")
269 .trim_end_matches(".cmd")
270}
271
272#[derive(Debug, Clone, PartialEq, Eq)]
273pub(crate) struct CargoTestInvocation {
274 pub program: String,
275 pub kind: RustCargoCommandKind,
276 pub arguments: Vec<String>,
277 pub runner_arguments: Vec<String>,
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq)]
281pub(crate) enum RustCargoCommandKind {
282 CargoTest,
283 NextestRun,
284}
285
286impl CargoTestInvocation {
287 pub(crate) fn command_position(&self) -> usize {
291 match self.kind {
292 RustCargoCommandKind::CargoTest => self
293 .arguments
294 .iter()
295 .position(|argument| argument == "test"),
296 RustCargoCommandKind::NextestRun => self
297 .arguments
298 .windows(2)
299 .position(|pair| pair == ["nextest", "run"]),
300 }
301 .expect("cargo_invocation keeps the test subcommand")
302 }
303}
304
305#[derive(Debug, Clone, PartialEq, Eq)]
306pub(crate) struct RustLibtestSelection {
307 pub list_arguments: Vec<String>,
308}
309
310#[derive(Debug, Clone, PartialEq, Eq)]
311pub(crate) struct RustCargoExecutionSelection {
312 pub run_libtests: bool,
313 pub run_doctests: bool,
314 pub implicit_doctests: bool,
317 pub doctest_arguments: Vec<String>,
318}
319
320#[derive(Debug, Clone, PartialEq, Eq)]
321pub(crate) struct NextestListInvocation {
322 pub arguments: Vec<String>,
323 pub runner_arguments: Vec<String>,
324}
325
326pub(crate) fn nextest_version_arguments(invocation: &CargoTestInvocation) -> Vec<String> {
327 let command = invocation.command_position();
328 let mut arguments = invocation.arguments[..command].to_vec();
329 arguments.extend(["nextest".into(), "--version".into()]);
330 arguments
331}
332
333fn nextest_run_only_option(argument: &str) -> Option<bool> {
334 let name = argument.split_once('=').map_or(argument, |(name, _)| name);
335 match name {
336 "-j"
337 | "--jobs"
338 | "--test-threads"
339 | "--retries"
340 | "--flaky-result"
341 | "--max-fail"
342 | "--no-tests"
343 | "--failure-output"
344 | "--success-output"
345 | "--status-level"
346 | "--final-status-level"
347 | "--show-progress"
348 | "--max-progress-running"
349 | "--message-format"
350 | "--message-format-version" => Some(!argument.contains('=')),
351 "--fail-fast"
352 | "--ff"
353 | "--no-fail-fast"
354 | "--nff"
355 | "--no-capture"
356 | "--nocapture"
357 | "--no-output-indent"
358 | "--hide-progress-bar"
359 | "--no-input-handler" => Some(false),
360 _ if argument.starts_with("-j") && argument.len() > 2 => Some(false),
361 _ => None,
362 }
363}
364
365fn nextest_unsupported_run_option(argument: &str) -> Option<bool> {
366 let name = argument.split_once('=').map_or(argument, |(name, _)| name);
367 match name {
368 "-R"
369 | "--rerun"
370 | "--debugger"
371 | "--tracer"
372 | "--stress-count"
373 | "--stress-duration"
374 | "--archive-file"
375 | "--archive-format"
376 | "--extract-to"
377 | "--cargo-metadata"
378 | "--workspace-remap"
379 | "--binaries-metadata"
380 | "--target-dir-remap"
381 | "--build-dir-remap" => Some(!argument.contains('=')),
382 "--no-run" | "--extract-overwrite" | "--persist-extract-tempdir" => Some(false),
383 _ => None,
384 }
385}
386
387fn nextest_shared_option(argument: &str) -> Option<bool> {
388 let name = argument.split_once('=').map_or(argument, |(name, _)| name);
389 match name {
390 "--color"
391 | "-p"
392 | "--package"
393 | "--exclude"
394 | "--bin"
395 | "--example"
396 | "--test"
397 | "--bench"
398 | "-F"
399 | "--features"
400 | "--build-jobs"
401 | "--cargo-profile"
402 | "--target"
403 | "--target-dir"
404 | "--cargo-message-format"
405 | "--config"
406 | "--timings"
407 | "-Z"
408 | "--run-ignored"
409 | "--partition"
410 | "--platform-filter"
411 | "-E"
412 | "--filterset"
413 | "--filter-expr"
414 | "--manifest-path"
415 | "--config-file"
416 | "--user-config-file"
417 | "--tool-config-file"
418 | "-P"
419 | "--profile" => Some(!argument.contains('=')),
420 "--no-pager"
421 | "-v"
422 | "--verbose"
423 | "--workspace"
424 | "--all"
425 | "--lib"
426 | "--bins"
427 | "--examples"
428 | "--tests"
429 | "--benches"
430 | "--all-targets"
431 | "--all-features"
432 | "--no-default-features"
433 | "-r"
434 | "--release"
435 | "--unit-graph"
436 | "--frozen"
437 | "--locked"
438 | "--offline"
439 | "--cargo-quiet"
440 | "--cargo-verbose"
441 | "--ignore-rust-version"
442 | "--future-incompat-report"
443 | "--ignore-default-filter"
444 | "--override-version-check" => Some(false),
445 _ if argument.starts_with("-p") && argument.len() > 2 => Some(false),
446 _ if argument.starts_with("-F") && argument.len() > 2 => Some(false),
447 _ if argument.starts_with("-E") && argument.len() > 2 => Some(false),
448 _ if argument.starts_with("-P") && argument.len() > 2 => Some(false),
449 _ if argument.starts_with("-Z") && argument.len() > 2 => Some(false),
450 _ if argument.len() > 2
451 && argument.starts_with('-')
452 && argument[1..].bytes().all(|byte| byte == b'v') =>
453 {
454 Some(false)
455 }
456 _ if argument.starts_with("--timings=") => Some(false),
457 _ => None,
458 }
459}
460
461pub(crate) fn nextest_list_invocation(
467 invocation: &CargoTestInvocation,
468) -> Result<NextestListInvocation, RustTestRunnerError> {
469 let command = invocation.command_position();
470 let mut arguments = invocation.arguments[..command].to_vec();
471 arguments.extend(["nextest".into(), "list".into()]);
472 let mut index = command + 2;
473 while index < invocation.arguments.len() {
474 let argument = &invocation.arguments[index];
475 if argument == "--" {
476 arguments.extend(invocation.arguments[index..].iter().cloned());
477 break;
478 }
479 if let Some(takes_value) = nextest_unsupported_run_option(argument) {
480 if takes_value && invocation.arguments.get(index + 1).is_none() {
481 return Err(RustTestRunnerError::UnsupportedCommand(format!(
482 "nextest option {argument} has no value"
483 )));
484 }
485 return Err(RustTestRunnerError::UnsupportedCommand(format!(
486 "nextest option {argument} cannot yet be assigned exact selected-test identity"
487 )));
488 }
489 if let Some(takes_value) = nextest_run_only_option(argument) {
490 if takes_value {
491 index += 1;
492 if index == invocation.arguments.len() {
493 return Err(RustTestRunnerError::UnsupportedCommand(format!(
494 "nextest option {argument} has no value"
495 )));
496 }
497 }
498 } else if let Some(takes_value) = nextest_shared_option(argument) {
499 arguments.push(argument.clone());
500 if takes_value {
501 index += 1;
502 let value = invocation.arguments.get(index).ok_or_else(|| {
503 RustTestRunnerError::UnsupportedCommand(format!(
504 "nextest option {argument} has no value"
505 ))
506 })?;
507 arguments.push(value.clone());
508 }
509 } else if argument.starts_with('-') {
510 return Err(RustTestRunnerError::UnsupportedCommand(format!(
511 "the pinned nextest run contract does not recognize option {argument}"
512 )));
513 } else {
514 arguments.push(argument.clone());
515 }
516 index += 1;
517 }
518 arguments.extend(["--message-format".into(), "json".into()]);
519 Ok(NextestListInvocation {
520 arguments,
521 runner_arguments: invocation.runner_arguments.clone(),
522 })
523}
524
525pub(crate) fn cargo_invocation(
526 root: &Path,
527 command: &[String],
528) -> Result<CargoTestInvocation, RustTestRunnerError> {
529 let words = if command.iter().any(|word| executable_name(word) == "cargo") {
534 command.to_vec()
535 } else {
536 let expanded = crate::project_discovery::script_command(root, command)
539 .unwrap_or_else(|| crate::project_discovery::expanded_command(root, command));
540 shell_words(&expanded)?
541 };
542 let cargo = words
543 .iter()
544 .position(|word| executable_name(word) == "cargo")
545 .ok_or_else(|| RustTestRunnerError::UnsupportedCommand(
546 "Rust was detected, but the expanded command does not expose a stable Cargo invocation".into(),
547 ))?;
548 let cargo_test = words[cargo + 1..]
549 .iter()
550 .position(|word| word == "test")
551 .map(|position| cargo + 1 + position);
552 let nextest = words[cargo + 1..]
553 .windows(2)
554 .position(|pair| pair == ["nextest", "run"])
555 .map(|position| cargo + 1 + position);
556 let (kind, command) = match (cargo_test, nextest) {
557 (Some(test), None) => (RustCargoCommandKind::CargoTest, test),
558 (None, Some(nextest)) => (RustCargoCommandKind::NextestRun, nextest),
559 (Some(_), Some(_)) => {
560 return Err(RustTestRunnerError::UnsupportedCommand(
561 "the Cargo invocation ambiguously contains both test and nextest run".into(),
562 ));
563 }
564 (None, None) => {
565 return Err(RustTestRunnerError::UnsupportedCommand(
566 "the owned Rust runner currently requires `cargo test` or `cargo nextest run`; cross remains explicitly unsupported"
567 .into(),
568 ));
569 }
570 };
571 if words[cargo + 1..command]
572 .iter()
573 .any(|word| matches!(word.as_str(), "&&" | "||" | ";" | "|"))
574 {
575 return Err(RustTestRunnerError::UnsupportedCommand(
576 "the Cargo invocation contains a shell boundary before `test`".into(),
577 ));
578 }
579 let command_end = command
580 + if kind == RustCargoCommandKind::NextestRun {
581 1
582 } else {
583 0
584 };
585 let mut arguments = words[cargo + 1..=command_end].to_vec();
586 let mut runner_arguments = Vec::new();
587 let mut after_separator = false;
588 for argument in &words[command_end + 1..] {
589 if argument == "--" && !after_separator {
590 after_separator = true;
591 continue;
592 }
593 if matches!(argument.as_str(), "&&" | "||" | ";" | "|") {
594 return Err(RustTestRunnerError::UnsupportedCommand(
595 "the Cargo test command contains an unsupported shell boundary".into(),
596 ));
597 }
598 if after_separator {
599 runner_arguments.push(argument.clone());
600 } else {
601 arguments.push(argument.clone());
602 }
603 }
604 Ok(CargoTestInvocation {
605 program: words[cargo].clone(),
606 kind,
607 arguments,
608 runner_arguments,
609 })
610}
611
612fn cargo_option_takes_value(argument: &str) -> Option<bool> {
613 let name = argument.split_once('=').map_or(argument, |(name, _)| name);
614 match name {
615 "-p" | "--package" | "--exclude" | "--bin" | "--example" | "--test" | "--bench" | "-F"
616 | "--features" | "-j" | "--jobs" | "--profile" | "--target" | "--target-dir"
617 | "--message-format" | "--color" | "--config" | "-Z" | "--manifest-path" => {
618 Some(!argument.contains('='))
619 }
620 "--no-run"
621 | "--no-fail-fast"
622 | "--future-incompat-report"
623 | "-q"
624 | "--quiet"
625 | "-v"
626 | "--verbose"
627 | "--workspace"
628 | "--all"
629 | "--lib"
630 | "--bins"
631 | "--examples"
632 | "--tests"
633 | "--benches"
634 | "--all-targets"
635 | "--doc"
636 | "--all-features"
637 | "--no-default-features"
638 | "-r"
639 | "--release"
640 | "--timings"
641 | "--ignore-rust-version"
642 | "--locked"
643 | "--offline"
644 | "--frozen" => Some(false),
645 _ if argument.starts_with("-vv") => Some(false),
646 _ if argument.starts_with("-p") && argument.len() > 2 => Some(false),
647 _ if argument.starts_with("-F") && argument.len() > 2 => Some(false),
648 _ if argument.starts_with("-j") && argument.len() > 2 => Some(false),
649 _ => None,
650 }
651}
652
653pub(crate) fn rust_libtest_selection(
654 invocation: &CargoTestInvocation,
655) -> Result<RustLibtestSelection, RustTestRunnerError> {
656 let mut cargo_filter = None;
659 let mut index = invocation.command_position() + 1;
660 while index < invocation.arguments.len() {
661 let argument = &invocation.arguments[index];
662 if argument.starts_with('-') {
663 if cargo_option_takes_value(argument) == Some(true) {
664 index += 1;
665 }
666 } else {
667 cargo_filter = Some(argument.clone());
669 }
670 index += 1;
671 }
672
673 let mut list_arguments = cargo_filter.into_iter().collect::<Vec<_>>();
674 let mut test_threads = None;
675 let mut index = 0;
676 while index < invocation.runner_arguments.len() {
677 let argument = &invocation.runner_arguments[index];
678 match argument.as_str() {
679 "--ignored" | "--include-ignored" | "--exclude-should-panic" | "--test" | "--bench" => {
680 list_arguments.push(argument.clone());
681 }
682 "--exact" => list_arguments.push(argument.clone()),
683 "--skip" => {
684 let value = invocation.runner_arguments.get(index + 1).ok_or_else(|| {
685 RustTestRunnerError::UnsupportedCommand(
686 "libtest --skip has no filter value".into(),
687 )
688 })?;
689 list_arguments.extend([argument.clone(), value.clone()]);
690 index += 1;
691 }
692 _ if argument.starts_with("--skip=") && argument.len() > "--skip=".len() => {
693 list_arguments.push(argument.clone());
694 }
695 "--test-threads" => {
696 let value = invocation.runner_arguments.get(index + 1).ok_or_else(|| {
697 RustTestRunnerError::UnsupportedCommand(
698 "libtest --test-threads has no value".into(),
699 )
700 })?;
701 let parsed = parse_libtest_threads(value)?;
702 if test_threads.replace(parsed).is_some() {
703 return Err(RustTestRunnerError::UnsupportedCommand(
704 "libtest --test-threads was provided more than once".into(),
705 ));
706 }
707 index += 1;
708 }
709 _ if argument.starts_with("--test-threads=") => {
710 let value = &argument["--test-threads=".len()..];
711 let parsed = parse_libtest_threads(value)?;
712 if test_threads.replace(parsed).is_some() {
713 return Err(RustTestRunnerError::UnsupportedCommand(
714 "libtest --test-threads was provided more than once".into(),
715 ));
716 }
717 }
718 "-Z" => {
719 let value = invocation.runner_arguments.get(index + 1).ok_or_else(|| {
720 RustTestRunnerError::UnsupportedCommand(
721 "libtest -Z has no feature value".into(),
722 )
723 })?;
724 list_arguments.extend([argument.clone(), value.clone()]);
729 index += 1;
730 }
731 _ if argument.starts_with("-Z") && argument.len() > 2 => {
732 list_arguments.push(argument.clone());
733 }
734 "--logfile" | "--color" | "--format" | "--shuffle-seed" => {
735 if invocation.runner_arguments.get(index + 1).is_none() {
736 return Err(RustTestRunnerError::UnsupportedCommand(format!(
737 "libtest {argument} has no value"
738 )));
739 }
740 index += 1;
744 }
745 _ if ["--logfile=", "--color=", "--format=", "--shuffle-seed="]
746 .iter()
747 .any(|prefix| argument.starts_with(prefix) && argument.len() > prefix.len()) => {}
748 "--force-run-in-process"
749 | "--fail-fast"
750 | "--no-capture"
751 | "--nocapture"
752 | "-q"
753 | "--quiet"
754 | "--show-output"
755 | "--report-time"
756 | "--ensure-time"
757 | "--shuffle" => {
758 }
762 "--list" | "-h" | "--help" => {
763 return Err(RustTestRunnerError::UnsupportedCommand(format!(
764 "libtest {argument} does not execute a test suite; exact non-execution mode support is not implemented"
765 )));
766 }
767 _ if !argument.starts_with('-') => list_arguments.push(argument.clone()),
768 _ => {
769 return Err(RustTestRunnerError::UnsupportedCommand(format!(
770 "the pinned Rust 1.95 libtest discovery contract does not recognize option {argument}"
771 )));
772 }
773 }
774 index += 1;
775 }
776 Ok(RustLibtestSelection { list_arguments })
777}
778
779fn parse_libtest_threads(value: &str) -> Result<usize, RustTestRunnerError> {
780 match value.parse::<usize>() {
781 Ok(0) => Err(RustTestRunnerError::UnsupportedCommand(
782 "argument for --test-threads must not be 0".into(),
783 )),
784 Ok(value) => Ok(value),
785 Err(error) => Err(RustTestRunnerError::UnsupportedCommand(format!(
786 "argument for --test-threads must be a number > 0 (error: {error})"
787 ))),
788 }
789}
790
791pub(crate) fn rust_cargo_execution_selection(
792 invocation: &CargoTestInvocation,
793) -> Result<RustCargoExecutionSelection, RustTestRunnerError> {
794 if invocation.kind == RustCargoCommandKind::NextestRun {
795 return Ok(RustCargoExecutionSelection {
796 run_libtests: true,
797 run_doctests: false,
798 implicit_doctests: false,
799 doctest_arguments: Vec::new(),
800 });
801 }
802 let test = invocation.command_position();
803 let mut doc = false;
804 let mut other_target = false;
805 let mut index = test + 1;
806 while index < invocation.arguments.len() {
807 let argument = &invocation.arguments[index];
808 let name = argument
809 .split_once('=')
810 .map_or(argument.as_str(), |(name, _)| name);
811 match name {
812 "--doc" => doc = true,
813 "--lib" | "--bins" | "--bin" | "--examples" | "--example" | "--tests" | "--test"
814 | "--benches" | "--bench" | "--all-targets" => other_target = true,
815 _ => {}
816 }
817 if argument.starts_with('-') {
818 let takes_value = cargo_option_takes_value(argument).ok_or_else(|| {
819 RustTestRunnerError::UnsupportedCommand(format!(
820 "the pinned Cargo test contract does not recognize option {argument}"
821 ))
822 })?;
823 if takes_value {
824 index += 1;
825 if index == invocation.arguments.len() {
826 return Err(RustTestRunnerError::UnsupportedCommand(format!(
827 "Cargo option {argument} has no value"
828 )));
829 }
830 }
831 }
832 index += 1;
833 }
834 if doc && other_target {
835 return Err(RustTestRunnerError::UnsupportedCommand(
836 "Cargo --doc cannot be combined with another explicit target selection".into(),
837 ));
838 }
839 let run_doctests = doc || !other_target;
840 let run_libtests = !doc;
841 let mut doctest_arguments = invocation.arguments.clone();
842 if run_doctests && !doc {
843 doctest_arguments.insert(test + 1, "--doc".into());
844 }
845 if !invocation.runner_arguments.is_empty() {
846 doctest_arguments.push("--".into());
847 doctest_arguments.extend(invocation.runner_arguments.iter().cloned());
848 }
849 Ok(RustCargoExecutionSelection {
850 run_libtests,
851 run_doctests,
852 implicit_doctests: run_doctests && !doc,
853 doctest_arguments,
854 })
855}
856
857pub(crate) fn relative_source(root: &Path, path: &Path) -> Result<String, RustTestRunnerError> {
858 let relative = path
859 .strip_prefix(root)
860 .map_err(|_| RustTestRunnerError::UnsafeArtifact(path.display().to_string()))?;
861 if relative.as_os_str().is_empty()
862 || relative
863 .components()
864 .any(|part| !matches!(part, Component::Normal(_)))
865 {
866 return Err(RustTestRunnerError::UnsafeArtifact(
867 path.display().to_string(),
868 ));
869 }
870 Ok(relative.to_string_lossy().replace('\\', "/"))
871}
872
873fn build_owned_arguments(arguments: Vec<String>) -> Result<Vec<String>, RustTestRunnerError> {
879 let mut kept = Vec::with_capacity(arguments.len());
880 let mut arguments = arguments.into_iter();
881 while let Some(argument) = arguments.next() {
882 let name = argument
883 .split_once('=')
884 .map_or(argument.as_str(), |(name, _)| name);
885 match name {
886 "--no-run" => {
887 return Err(RustTestRunnerError::UnsupportedCommand(
888 "`--no-run` builds the tests without running any, so there is nothing to measure; run the command without it".into(),
889 ));
890 }
891 "--message-format" | "--target-dir" => {
892 if !argument.contains('=') {
893 arguments.next();
894 }
895 }
896 _ => kept.push(argument),
897 }
898 }
899 Ok(kept)
900}
901
902fn build_test_artifacts(
903 project: &PreparedRustProject,
904 command: &[String],
905) -> Result<Vec<TestArtifact>, RustTestRunnerError> {
906 let mut invocation = cargo_invocation(&project.workspace_root, command)?;
907 let lint_invocation = invocation.clone();
908 invocation.arguments = build_owned_arguments(invocation.arguments)?;
909 invocation
910 .arguments
911 .extend(["--no-run".into(), "--message-format=json".into()]);
912 let output = Command::new(&invocation.program)
919 .args(invocation.arguments)
920 .current_dir(&project.workspace_root)
921 .env("CARGO_TARGET_DIR", &project.target_directory)
922 .env("RUSTFLAGS", capped_rustflags(project, &lint_invocation))
923 .output()
924 .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
925 if !output.status.success() {
926 let rendered = output
930 .stdout
931 .split(|byte| *byte == b'\n')
932 .filter_map(|line| serde_json::from_slice::<serde_json::Value>(line).ok())
933 .filter(|message| {
934 message["reason"] == "compiler-message" && message["message"]["level"] == "error"
935 })
936 .filter_map(|message| message["message"]["rendered"].as_str().map(str::to_owned))
937 .collect::<String>();
938 return Err(RustTestRunnerError::CargoFailed(format!(
939 "{rendered}{}",
940 String::from_utf8_lossy(&output.stderr).trim()
941 )));
942 }
943 let canonical_target = fs::canonicalize(&project.target_directory)
944 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
945 let mut artifacts = Vec::new();
946 for line in output
947 .stdout
948 .split(|byte| *byte == b'\n')
949 .filter(|line| !line.is_empty())
950 {
951 let message: CargoMessage = serde_json::from_slice(line)
952 .map_err(|error| RustTestRunnerError::CargoJson(error.to_string()))?;
953 if message.reason != "compiler-artifact"
954 || !message.profile.as_ref().is_some_and(|profile| profile.test)
955 {
956 continue;
957 }
958 let (Some(executable), Some(target)) = (message.executable, message.target) else {
959 continue;
960 };
961 let executable = fs::canonicalize(&executable)
962 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
963 if !executable.starts_with(&canonical_target)
964 || !fs::metadata(&executable).is_ok_and(|metadata| metadata.is_file())
965 {
966 return Err(RustTestRunnerError::UnsafeArtifact(
967 executable.display().to_string(),
968 ));
969 }
970 let source = fs::canonicalize(target.src_path)
971 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
972 let package_directory = message
973 .manifest_path
974 .as_deref()
975 .and_then(Path::parent)
976 .map_or_else(|| project.workspace_root.clone(), Path::to_path_buf);
977 artifacts.push(TestArtifact {
978 executable,
979 package_directory,
980 name: target.name,
981 kind: if target.kind.iter().any(|kind| kind == "test") {
982 "integration".into()
983 } else {
984 "unit".into()
985 },
986 source: relative_source(&project.workspace_root, &source)?,
987 });
988 }
989 artifacts.sort_by(|left, right| left.executable.cmp(&right.executable));
990 artifacts.dedup_by(|left, right| left.executable == right.executable);
991 if artifacts.is_empty() {
992 return Err(RustTestRunnerError::CargoJson(
993 "Cargo emitted no libtest artifacts".into(),
994 ));
995 }
996 Ok(artifacts)
997}
998
999#[derive(Debug, Clone, Default)]
1004pub(crate) struct ArtifactLauncher {
1005 runner: Option<crate::rust_cargo_configuration::RustCargoResolvedRunner>,
1006 environment: Vec<(String, String)>,
1009 browser: Option<std::sync::Arc<crate::rust_wasm_browser::BrowserReader>>,
1012 unmeasured_browser: Option<String>,
1014 token: String,
1016 wasm_bindgen_runner: bool,
1018 node_options: Option<OsString>,
1022}
1023
1024impl ArtifactLauncher {
1025 pub(crate) fn for_invocation(
1027 project: &PreparedRustProject,
1028 invocation: &CargoTestInvocation,
1029 hook_directory: &Path,
1030 ) -> Result<Self, RustTestRunnerError> {
1031 let Ok(plan) = crate::rust_cargo_configuration::resolve_cargo_runner_plan(
1032 &project.workspace_root,
1033 &project.workspace_root,
1034 invocation,
1035 ) else {
1036 return Ok(Self::default());
1037 };
1038 let [target] = plan.targets.as_slice() else {
1039 if plan
1040 .targets
1041 .iter()
1042 .any(|target| target.underlying_runner.is_some())
1043 {
1044 return Err(RustTestRunnerError::UnsupportedCommand(
1045 "a command that builds for several targets, one of them with a target runner, cannot be run test by test".into(),
1046 ));
1047 }
1048 return Ok(Self::default());
1049 };
1050 let Some(runner) = &target.underlying_runner else {
1051 return Ok(Self::default());
1052 };
1053 let node_options = if target.target.starts_with("wasm32-") {
1054 fs::create_dir_all(hook_directory).map_err(io_error)?;
1055 let hook = hook_directory.join("rust-wasm-node.mjs");
1056 fs::write(&hook, RUST_WASM_NODE_HOOK).map_err(io_error)?;
1057 let mut options = std::env::var_os("NODE_OPTIONS").unwrap_or_default();
1058 if !options.is_empty() {
1059 options.push(" ");
1060 }
1061 options.push(format!("--import={}", file_url(&hook)));
1062 Some(options)
1063 } else {
1064 None
1065 };
1066 let runner = runner.resolve(&project.workspace_root);
1067 let wasm_bindgen_runner = Path::new(&runner.program)
1068 .file_stem()
1069 .is_some_and(|name| name == "wasm-bindgen-test-runner");
1070 Ok(Self {
1071 runner: Some(runner),
1072 environment: Vec::new(),
1073 browser: None,
1074 unmeasured_browser: None,
1075 token: project.probes.token.clone(),
1076 wasm_bindgen_runner,
1077 node_options,
1078 })
1079 }
1080
1081 pub(crate) fn with_environment(mut self, environment: &[(String, String)]) -> Self {
1084 self.environment = environment.to_vec();
1085 let in_node = environment
1086 .iter()
1087 .any(|(name, _)| name == "WASM_BINDGEN_TEST_ONLY_NODE")
1088 || std::env::var_os("WASM_BINDGEN_TEST_ONLY_NODE").is_some();
1089 if !self.wasm_bindgen_runner || in_node {
1090 return self;
1091 }
1092 match crate::rust_wasm_browser::browser(environment) {
1093 Some(crate::rust_wasm_browser::Browser::Chrome(driver, arguments)) => {
1094 match crate::rust_wasm_browser::BrowserReader::start(
1095 &driver,
1096 &arguments,
1097 &self.token,
1098 ) {
1099 Ok(reader) => self.browser = Some(std::sync::Arc::new(reader)),
1100 Err(error) => self.unmeasured_browser = Some(format!("Chrome ({error})")),
1101 }
1102 }
1103 Some(crate::rust_wasm_browser::Browser::Other(name)) => {
1104 let named =
1107 ["GECKODRIVER", "SAFARIDRIVER", "MSEDGEDRIVER"]
1108 .iter()
1109 .any(|variable| {
1110 environment.iter().any(|(key, _)| key == variable)
1111 || std::env::var_os(variable).is_some()
1112 });
1113 if named {
1114 self.unmeasured_browser = Some(name);
1115 }
1116 }
1117 None => {}
1118 }
1119 self
1120 }
1121
1122 pub(crate) fn unmeasured_browser(&self) -> Option<&str> {
1124 self.unmeasured_browser.as_deref()
1125 }
1126
1127 pub(crate) fn command(&self, executable: &Path, evidence: Option<&Path>) -> Command {
1130 let mut command = match &self.runner {
1131 Some(runner) => {
1132 let mut command = Command::new(&runner.program);
1133 command.args(&runner.arguments);
1134 if let Some(evidence) = evidence
1138 && is_wasi_runtime(&runner.program)
1139 {
1140 command
1141 .arg(format!("--dir={}", evidence.display()))
1142 .arg(format!(
1143 "--env=SUPERCOV_RUST_EVIDENCE_DIR={}",
1144 evidence.display()
1145 ));
1146 }
1147 command.arg(executable);
1148 command
1149 }
1150 None => Command::new(executable),
1151 };
1152 if let Some(options) = &self.node_options {
1153 command.env("NODE_OPTIONS", options);
1154 }
1155 command.envs(self.environment.iter().map(|(name, value)| (name, value)));
1156 if let (Some(browser), Some(evidence)) = (&self.browser, evidence) {
1157 command.env("CHROMEDRIVER_REMOTE", browser.remote_url(evidence));
1158 }
1159 command
1160 }
1161}
1162
1163fn is_wasi_runtime(program: &Path) -> bool {
1166 program
1167 .file_stem()
1168 .and_then(|name| name.to_str())
1169 .is_some_and(|name| matches!(name, "wasmtime" | "wasmer"))
1170}
1171
1172const RUST_WASM_NODE_HOOK: &str = include_str!("../runtime-assets/rust-wasm-node.mjs");
1173
1174fn file_url(path: &Path) -> String {
1177 let mut url = String::from("file://");
1178 let text = path.to_string_lossy().replace('\\', "/");
1179 if !text.starts_with('/') {
1180 url.push('/');
1181 }
1182 for byte in text.bytes() {
1183 if byte.is_ascii_alphanumeric() || b"-._~/:".contains(&byte) {
1184 url.push(byte as char);
1185 } else {
1186 url.push_str(&format!("%{byte:02X}"));
1187 }
1188 }
1189 url
1190}
1191
1192fn list_tests(
1197 launcher: &ArtifactLauncher,
1198 executable: &Path,
1199 filters: &[String],
1200 environment: &[(&'static str, OsString)],
1201) -> Result<Vec<String>, RustTestRunnerError> {
1202 let output = launcher
1203 .command(executable, None)
1204 .args(["--list", "--format", "terse"])
1205 .args(filters)
1206 .envs(environment.iter().map(|(key, value)| (key, value)))
1207 .output()
1208 .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
1209 if !output.status.success() {
1210 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
1214 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned();
1215 let detail = if !stderr.is_empty() {
1216 stderr
1217 } else if !stdout.is_empty() {
1218 format!("no stderr; stdout was {stdout}")
1219 } else {
1220 "no output on either stream".to_owned()
1221 };
1222 return Err(RustTestRunnerError::ListFailed(format!(
1223 "{} exited with {} when asked to --list: {detail}",
1224 executable.display(),
1225 output.status
1226 )));
1227 }
1228 let mut tests = String::from_utf8_lossy(&output.stdout)
1229 .lines()
1230 .filter_map(|line| line.strip_suffix(": test"))
1231 .map(str::to_owned)
1232 .collect::<Vec<_>>();
1233 tests.sort();
1234 tests.dedup();
1235 Ok(tests)
1236}
1237
1238pub(crate) struct RustEvidence {
1241 pub(crate) snapshot: RuntimeSnapshot,
1242 pub(crate) phases: Vec<CoveragePhase>,
1243}
1244
1245struct PendingRecord {
1247 event_type: &'static str,
1248 id: String,
1249 vector: Option<McdcVector>,
1250 sequence: i64,
1251}
1252
1253pub(crate) fn snapshot(
1257 manifest: &CoverageManifest,
1258 probes: &crate::rust_instrumenter::RustProbeLayout,
1259 directory: &Path,
1260 attempt: &str,
1261) -> Result<RustEvidence, RustTestRunnerError> {
1262 let regions = read_rust_region_directory(directory, &probes.token, probes)
1267 .map_err(|error| RustTestRunnerError::Probe(error.to_string()))?
1268 .into_iter()
1269 .map(|(_, observations)| observations);
1270 snapshot_regions(manifest, regions, attempt)
1271}
1272
1273pub(crate) fn snapshot_regions(
1276 manifest: &CoverageManifest,
1277 regions: impl IntoIterator<Item = Vec<crate::rust_runtime::RustProbeEntry>>,
1278 attempt: &str,
1279) -> Result<RustEvidence, RustTestRunnerError> {
1280 let points = manifest
1281 .points
1282 .iter()
1283 .map(|point| (point.id.as_str(), point))
1284 .collect::<BTreeMap<_, _>>();
1285 let asserting = manifest
1287 .points
1288 .iter()
1289 .chain(&manifest.assertion_sites)
1290 .map(|point| (point.id.as_str(), point))
1291 .collect::<BTreeMap<_, _>>();
1292 let alternatives = manifest
1293 .branches
1294 .iter()
1295 .flat_map(|branch| {
1296 branch
1297 .alternatives
1298 .iter()
1299 .map(|alternative| alternative.id.as_str())
1300 })
1301 .collect::<BTreeSet<_>>();
1302 let decisions = manifest
1303 .decisions
1304 .iter()
1305 .map(|decision| (decision.id.as_str(), decision))
1306 .collect::<BTreeMap<_, _>>();
1307 let mut hits = BTreeSet::new();
1308 let mut vectors = BTreeMap::<String, BTreeSet<(Vec<Option<bool>>, bool)>>::new();
1309 let mut pending = BTreeMap::<u64, Vec<PendingRecord>>::new();
1313 let mut events = Vec::new();
1314 let mut phases = Vec::new();
1315 let mut sequence = 0_i64;
1318 for observations in regions {
1319 for entry in observations {
1320 sequence += 1;
1321 let thread = entry.thread;
1322 match entry.observation {
1323 RustProbeObservation::Hit { id } => {
1324 if !points.contains_key(id.as_str()) && !alternatives.contains(id.as_str()) {
1325 return Err(RustTestRunnerError::UnknownProbe(id));
1326 }
1327 hits.insert(id.clone());
1328 pending.entry(thread).or_default().push(PendingRecord {
1329 event_type: "hit",
1330 id,
1331 vector: None,
1332 sequence,
1333 });
1334 }
1335 RustProbeObservation::Decision {
1336 id,
1337 values,
1338 outcome,
1339 } => {
1340 let Some(meta) = decisions.get(id.as_str()) else {
1341 return Err(RustTestRunnerError::UnknownProbe(id));
1342 };
1343 if values.len() != meta.conditions.len() {
1344 return Err(RustTestRunnerError::InvalidVector {
1345 id,
1346 expected: meta.conditions.len(),
1347 actual: values.len(),
1348 });
1349 }
1350 hits.insert(format!(
1351 "{}:outcome:{}",
1352 meta.id,
1353 if outcome { "true" } else { "false" }
1354 ));
1355 vectors
1356 .entry(meta.id.clone())
1357 .or_default()
1358 .insert((values.clone(), outcome));
1359 pending.entry(thread).or_default().push(PendingRecord {
1360 event_type: "decision",
1361 id,
1362 vector: Some(McdcVector { values, outcome }),
1363 sequence,
1364 });
1365 }
1366 RustProbeObservation::Assertion { id } => {
1367 let Some(point) = asserting.get(id.as_str()) else {
1370 return Err(RustTestRunnerError::UnknownProbe(id));
1371 };
1372 let witnessed = pending.remove(&thread).unwrap_or_default();
1373 let phase_id = format!("{attempt}:assertion:{sequence}");
1374 phases.push(CoveragePhase {
1375 id: phase_id.clone(),
1376 kind: "assertion".into(),
1377 operation: format!(
1378 "Rust assertion at {}:{}:{}",
1379 point.file, point.line, point.column
1380 ),
1381 source: Some(point.source.clone()),
1382 caused_by_phase_id: None,
1383 started_at_ms: witnessed.first().map_or(sequence, |record| record.sequence),
1384 ended_at_ms: Some(sequence),
1385 status: Some("passed".into()),
1388 error: None,
1389 });
1390 for record in witnessed {
1391 events.push(RuntimeEvent {
1392 event_type: record.event_type.into(),
1393 id: record.id,
1394 vector: record.vector,
1395 timestamp_ms: record.sequence,
1396 phase_id: Some(phase_id.clone()),
1397 statement_id: None,
1398 environment: "server".into(),
1399 });
1400 }
1401 }
1402 }
1403 }
1404 }
1405 let mut decision_snapshots = Vec::new();
1406 for (id, observed) in vectors {
1407 let meta: DecisionMeta = (*decisions[id.as_str()]).clone();
1408 decision_snapshots.push(DecisionSnapshot {
1409 meta,
1410 vectors: observed
1411 .into_iter()
1412 .map(|(values, outcome)| McdcVector { values, outcome })
1413 .collect(),
1414 });
1415 }
1416 Ok(RustEvidence {
1417 snapshot: RuntimeSnapshot {
1418 decisions: decision_snapshots,
1419 hits: hits.into_iter().collect(),
1420 events,
1421 logicals: Vec::new(),
1422 phase_id: None,
1423 },
1424 phases,
1425 })
1426}
1427
1428pub(crate) fn rust_coverage_model() -> CoverageModelDeclaration {
1429 CoverageModelDeclaration {
1430 language: "rust".into(),
1431 variant: "rust-owned-probes-v1".into(),
1432 name: "supercov-rust-owned-v1".into(),
1433 completeness_meaning: "Every semantics-proven Rust obligation in the owned source denominator was observed; explicit manifest limitations identify unmeasured Rust surfaces.".into(),
1434 measured: vec![
1435 "owned Rust statements and function entries".into(),
1436 "owned atomic condition vectors and decision outcomes".into(),
1437 "exact process-per-libtest attribution".into(),
1438 "exact process-per-doctest attribution".into(),
1439 "evidence a passing assertion of the same thread witnessed".into(),
1440 ],
1441 not_measured: vec![
1442 "macro-expanded and generated Rust code".into(),
1443 "const-evaluated code and unsupported structural branch probes".into(),
1444 "causal linkage to individual actions".into(),
1445 "all input values, semantic partitions, paths, or concurrency interleavings".into(),
1446 "mutation score or assertion fault-detection strength".into(),
1447 ],
1448 }
1449}
1450
1451pub(crate) fn instrumented_stack_environment() -> Vec<(&'static str, &'static str)> {
1463 if std::env::var_os("RUST_MIN_STACK").is_some() {
1464 Vec::new()
1465 } else {
1466 vec![("RUST_MIN_STACK", "16777216")]
1467 }
1468}
1469
1470fn rustc_print_path(request: &str) -> Result<PathBuf, RustTestRunnerError> {
1472 let output = Command::new("rustc")
1473 .args(["--print", request])
1474 .output()
1475 .map_err(|error| {
1476 RustTestRunnerError::Launch(format!("rustc --print {request}: {error}"))
1477 })?;
1478 if !output.status.success() {
1479 return Err(RustTestRunnerError::Launch(format!(
1480 "rustc --print {request} exited with {}",
1481 output.status
1482 )));
1483 }
1484 Ok(PathBuf::from(
1485 String::from_utf8_lossy(&output.stdout).trim(),
1486 ))
1487}
1488
1489pub(crate) fn rustc_sysroot() -> Result<PathBuf, RustTestRunnerError> {
1491 rustc_print_path("sysroot")
1492}
1493
1494pub(crate) fn rustc_target_libdir() -> Result<PathBuf, RustTestRunnerError> {
1498 rustc_print_path("target-libdir")
1499}
1500
1501pub(crate) fn dynamic_library_environment(
1507 target_libdir: &Path,
1508 executable: &Path,
1509) -> Vec<(&'static str, OsString)> {
1510 let variable = if cfg!(target_os = "macos") {
1511 "DYLD_FALLBACK_LIBRARY_PATH"
1512 } else if cfg!(windows) {
1513 "PATH"
1514 } else {
1515 "LD_LIBRARY_PATH"
1516 };
1517 let mut entries = Vec::new();
1518 if let Some(deps) = executable.parent() {
1519 entries.push(deps.to_path_buf());
1520 if let Some(profile) = deps.parent() {
1521 entries.push(profile.to_path_buf());
1522 }
1523 }
1524 entries.push(target_libdir.to_path_buf());
1525 if let Some(existing) = std::env::var_os(variable) {
1526 entries.extend(std::env::split_paths(&existing));
1527 }
1528 match std::env::join_paths(entries) {
1529 Ok(value) => vec![(variable, value)],
1530 Err(_) => Vec::new(),
1531 }
1532}
1533
1534pub(crate) fn capped_rustflags(
1546 project: &PreparedRustProject,
1547 invocation: &CargoTestInvocation,
1548) -> String {
1549 let wasm = crate::rust_cargo_configuration::resolve_cargo_runner_plan(
1550 &project.workspace_root,
1551 &project.workspace_root,
1552 invocation,
1553 )
1554 .is_ok_and(|plan| {
1555 plan.targets
1556 .iter()
1557 .any(|target| target.target.starts_with("wasm"))
1558 });
1559 let mut rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
1560 if !rustflags.is_empty() {
1561 rustflags.push(' ');
1562 }
1563 rustflags.push_str(if wasm {
1564 "--cap-lints=warn -A unused_parens"
1565 } else {
1566 "--cap-lints=allow"
1567 });
1568 rustflags
1569}
1570
1571pub(crate) fn io_error(error: impl std::fmt::Display) -> RustTestRunnerError {
1572 RustTestRunnerError::Io(error.to_string())
1573}
1574
1575fn running_line(
1581 workspace: &Path,
1582 target: &Path,
1583 artifact: &TestArtifact,
1584 coloured: bool,
1585) -> String {
1586 let text = |path: &Path| {
1591 let text = path.to_string_lossy().replace('\\', "/");
1592 text.strip_prefix("//?/").unwrap_or(&text).to_owned()
1593 };
1594 let package = text(&artifact.package_directory);
1595 let package = package
1596 .strip_prefix(&text(workspace))
1597 .map_or("", |inside| inside.trim_matches('/'));
1598 let source = artifact
1599 .source
1600 .strip_prefix(package)
1601 .map_or(artifact.source.as_str(), |local| {
1602 local.trim_start_matches('/')
1603 })
1604 .split('/')
1605 .collect::<PathBuf>();
1606 let executable = artifact.executable.strip_prefix(target).map_or_else(
1609 |_| artifact.executable.clone(),
1610 |built| Path::new("target").join(built),
1611 );
1612 format!(
1613 "{} {}{} ({})",
1614 if coloured {
1615 "\u{1b}[1m\u{1b}[92m Running\u{1b}[0m"
1616 } else {
1617 " Running"
1618 },
1619 if artifact.kind == "integration" {
1620 ""
1621 } else {
1622 "unittests "
1623 },
1624 source.display(),
1625 executable.display()
1626 )
1627}
1628
1629fn cargo_colours(invocation: &CargoTestInvocation) -> bool {
1633 use std::io::IsTerminal;
1634 let arguments = &invocation.arguments;
1635 let asked = arguments
1636 .iter()
1637 .enumerate()
1638 .find_map(|(index, argument)| {
1639 argument
1640 .strip_prefix("--color=")
1641 .map(str::to_owned)
1642 .or_else(|| (argument == "--color").then(|| arguments.get(index + 1).cloned())?)
1643 })
1644 .or_else(|| std::env::var("CARGO_TERM_COLOR").ok());
1645 let set = |name: &str| std::env::var(name).ok().filter(|value| !value.is_empty());
1646 match asked.as_deref() {
1647 Some("always") => true,
1648 Some("never") => false,
1649 _ if set("CLICOLOR_FORCE").is_some_and(|value| value != "0") => true,
1652 _ if set("NO_COLOR").is_some() || set("CLICOLOR").is_some_and(|value| value == "0") => {
1653 false
1654 }
1655 _ => {
1656 std::io::stderr().is_terminal()
1657 && (cfg!(windows) || set("TERM").is_some_and(|term| term != "dumb"))
1658 }
1659 }
1660}
1661
1662fn libtest_colours(invocation: &CargoTestInvocation) -> bool {
1666 use std::io::IsTerminal;
1667 let arguments = &invocation.runner_arguments;
1668 let asked = arguments.iter().enumerate().find_map(|(index, argument)| {
1669 argument
1670 .strip_prefix("--color=")
1671 .map(str::to_owned)
1672 .or_else(|| (argument == "--color").then(|| arguments.get(index + 1).cloned())?)
1673 });
1674 match asked.as_deref() {
1675 Some("always") => true,
1676 Some("never") => false,
1677 _ => {
1678 std::io::stdout().is_terminal()
1679 && (cfg!(windows)
1680 || std::env::var("TERM").is_ok_and(|term| !term.is_empty() && term != "dumb"))
1681 }
1682 }
1683}
1684
1685pub(crate) fn cargo_quiet(invocation: &CargoTestInvocation) -> bool {
1687 invocation.arguments[invocation.command_position() + 1..]
1688 .iter()
1689 .any(|argument| matches!(argument.as_str(), "-q" | "--quiet"))
1690}
1691
1692pub(crate) fn libtest_quiet(invocation: &CargoTestInvocation) -> bool {
1694 let runner = &invocation.runner_arguments;
1695 runner
1696 .iter()
1697 .any(|argument| matches!(argument.as_str(), "-q" | "--quiet" | "--format=terse"))
1698 || runner
1699 .windows(2)
1700 .any(|pair| pair[0] == "--format" && pair[1] == "terse")
1701}
1702
1703pub(crate) fn libtest_skipped(exit: i32, stdout: &str) -> bool {
1704 exit == 0 && (stdout.contains("running 0 tests") || stdout.contains("; 1 ignored;"))
1705}
1706
1707fn rust_runner_limitations(runner: &str) -> Vec<FrontendLimitation> {
1710 let prefix = if runner == "rust-libtest" {
1711 "rust".to_owned()
1712 } else {
1713 runner.to_owned()
1714 };
1715 vec![
1716 FrontendLimitation {
1717 id: format!("{prefix}-action-linkage-unavailable"),
1718 scopes: vec![FrontendLimitationScope::Action],
1719 reason: "Rust test frameworks expose no general action lifecycle".into(),
1720 },
1721 FrontendLimitation {
1722 id: format!("{prefix}-assertion-linkage-unavailable"),
1723 scopes: vec![FrontendLimitationScope::Assertion],
1724 reason: "assertion macros do not expose a stable per-assertion success lifecycle"
1725 .into(),
1726 },
1727 ]
1728}
1729
1730pub(crate) fn rust_runner_declaration(runner: &str) -> FrontendRunnerDeclaration {
1731 FrontendRunnerDeclaration {
1732 runner: runner.into(),
1733 execution_model: ExecutionModel::ProcessPerTest,
1734 attribution: FrontendAttribution {
1735 run: AttributionPrecision::Exact,
1736 worker: AttributionPrecision::Exact,
1737 test: AttributionPrecision::Exact,
1738 retry: AttributionPrecision::Exact,
1739 phase: AttributionPrecision::Exact,
1740 action: AttributionPrecision::Unavailable,
1741 assertion: AttributionPrecision::Unavailable,
1742 },
1743 limitations: rust_runner_limitations(runner),
1744 }
1745}
1746
1747fn decline_uncompiled_sources(
1756 project: &PreparedRustProject,
1757 also_compiled: &BTreeSet<String>,
1758) -> (CoverageManifest, BTreeSet<String>) {
1759 let mut manifest = project.manifest.clone();
1760 let Some(compiled) = crate::rust_project::compiled_source_files(
1761 &project.workspace_root,
1762 &project.target_directory,
1763 ) else {
1764 return (manifest, BTreeSet::new());
1765 };
1766 let declined = project
1767 .source_files
1768 .iter()
1769 .filter(|file| !compiled.contains(*file) && !also_compiled.contains(*file))
1770 .cloned()
1771 .collect::<BTreeSet<_>>();
1772 if declined.is_empty()
1775 || project
1776 .crate_roots
1777 .iter()
1778 .all(|root| !compiled.contains(root))
1779 {
1780 return (manifest, BTreeSet::new());
1781 }
1782 let mut unmeasured = manifest.unmeasured.iter().cloned().collect::<BTreeSet<_>>();
1783 unmeasured.extend(
1784 manifest
1785 .points
1786 .iter()
1787 .filter(|point| declined.contains(&point.file))
1788 .map(|point| point.id.clone()),
1789 );
1790 unmeasured.extend(
1791 manifest
1792 .decisions
1793 .iter()
1794 .filter(|decision| declined.contains(&decision.file))
1795 .map(|decision| decision.id.clone()),
1796 );
1797 unmeasured.extend(
1798 manifest
1799 .branches
1800 .iter()
1801 .filter(|branch| declined.contains(&branch.file))
1802 .map(|branch| branch.id.clone()),
1803 );
1804 manifest.unmeasured = unmeasured.into_iter().collect();
1805 manifest.limitations.retain(|limitation| {
1807 limitation
1808 .get("file")
1809 .and_then(|file| file.as_str())
1810 .is_none_or(|file| !declined.contains(file))
1811 });
1812 if let Some(entries) = manifest
1814 .scope
1815 .as_mut()
1816 .and_then(|scope| scope.get_mut("entries"))
1817 .and_then(serde_json::Value::as_array_mut)
1818 {
1819 for entry in entries {
1820 let named = entry
1821 .get("file")
1822 .and_then(serde_json::Value::as_str)
1823 .is_some_and(|file| declined.contains(file));
1824 if named && let Some(entry) = entry.as_object_mut() {
1825 entry.insert("status".into(), "excluded".into());
1826 entry.insert(
1827 "reason".into(),
1828 crate::rust_project::RUST_NOT_COMPILED.into(),
1829 );
1830 }
1831 }
1832 }
1833 (manifest, declined)
1834}
1835
1836fn settle_cfg_gates(
1842 project: &PreparedRustProject,
1843 evidence_root: &Path,
1844 diagnostics: &mut dyn Write,
1845) -> Result<CoverageManifest, RustTestRunnerError> {
1846 let probes = &project.probes;
1847 if probes.cfgs.is_empty() {
1848 return Ok(project.manifest.clone());
1849 }
1850 let settled =
1851 crate::rust_runtime::read_rust_cfg_settlement(evidence_root, &probes.token, probes)
1852 .map_err(|error| RustTestRunnerError::Probe(error.to_string()))?;
1853 settle_cfg_gates_with(&project.manifest, probes, &settled, diagnostics)
1854}
1855
1856pub(crate) fn settle_cfg_gates_with(
1859 manifest: &CoverageManifest,
1860 probes: &crate::rust_instrumenter::RustProbeLayout,
1861 settled: &[u8],
1862 diagnostics: &mut dyn Write,
1863) -> Result<CoverageManifest, RustTestRunnerError> {
1864 let mut manifest = manifest.clone();
1865 let left_out = probes
1866 .cfgs
1867 .iter()
1868 .zip(settled)
1869 .filter(|(_, bits)| **bits == 1)
1870 .map(|(gate, _)| gate)
1871 .collect::<Vec<_>>();
1872 let removed = left_out
1873 .iter()
1874 .flat_map(|gate| gate.obligations.iter().map(String::as_str))
1875 .collect::<BTreeSet<_>>();
1876 let unanswered = probes
1877 .cfgs
1878 .iter()
1879 .zip(settled)
1880 .filter(|(gate, bits)| {
1881 **bits == 0
1882 && gate
1883 .obligations
1884 .iter()
1885 .any(|id| !removed.contains(id.as_str()))
1886 })
1887 .map(|(gate, _)| gate)
1888 .collect::<Vec<_>>();
1889 let place = |gate: &&crate::rust_instrumenter::RustCfgGate| {
1890 format!("{}:{} cfg({})", gate.file, gate.line, gate.predicate)
1891 };
1892 let listed = |gates: &[&crate::rust_instrumenter::RustCfgGate]| {
1893 let mut text = gates
1894 .iter()
1895 .take(3)
1896 .map(place)
1897 .collect::<Vec<_>>()
1898 .join(", ");
1899 if gates.len() > 3 {
1900 text.push_str(", ...");
1901 }
1902 text
1903 };
1904 if !left_out.is_empty() {
1905 writeln!(
1906 diagnostics,
1907 "[supercov] {} item(s) behind a #[cfg] no build compiled are outside the denominator: {}",
1908 left_out.len(),
1909 listed(&left_out)
1910 )
1911 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1912 }
1913 if !unanswered.is_empty() {
1914 writeln!(
1915 diagnostics,
1916 "[supercov] {} item(s) behind a #[cfg] are counted though no test ran code of their crate to show whether it was built: {}",
1917 unanswered.len(),
1918 listed(&unanswered)
1919 )
1920 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
1921 }
1922 if removed.is_empty() {
1923 return Ok(manifest);
1924 }
1925 manifest
1926 .points
1927 .retain(|point| !removed.contains(point.id.as_str()));
1928 manifest
1929 .decisions
1930 .retain(|decision| !removed.contains(decision.id.as_str()));
1931 manifest
1932 .branches
1933 .retain(|branch| !removed.contains(branch.id.as_str()));
1934 manifest
1935 .unmeasured
1936 .retain(|id| !removed.contains(id.as_str()));
1937 manifest.limitations.retain(|limitation| {
1939 let file = limitation.get("file").and_then(serde_json::Value::as_str);
1940 let line = limitation
1941 .get("line")
1942 .and_then(serde_json::Value::as_u64)
1943 .map(|line| line as usize);
1944 !left_out.iter().any(|gate| {
1945 file == Some(gate.file.as_str())
1946 && line.is_some_and(|line| (gate.line..=gate.end_line).contains(&line))
1947 })
1948 });
1949 Ok(manifest)
1950}
1951
1952pub(crate) fn structural_limitations(manifest: &CoverageManifest) -> Vec<String> {
1954 manifest
1955 .limitations
1956 .iter()
1957 .filter_map(|item| {
1958 item.get("id")
1959 .and_then(|value| value.as_str())
1960 .map(str::to_owned)
1961 })
1962 .collect()
1963}
1964
1965fn redecline_for_workers(
1969 original: &PreparedRustProject,
1970 project: &PreparedRustProject,
1971 declined: BTreeSet<String>,
1972 built_crates: &BTreeSet<String>,
1973) -> (PreparedRustProject, BTreeSet<String>) {
1974 if built_crates.is_empty() {
1975 return (project.clone(), declined);
1976 }
1977 let built = built_crates
1978 .iter()
1979 .filter_map(|key| original.probes.crates.get(key))
1980 .flatten()
1981 .cloned()
1982 .collect::<BTreeSet<_>>();
1983 let (manifest, declined) = decline_uncompiled_sources(original, &built);
1984 (
1985 PreparedRustProject {
1986 manifest,
1987 ..project.clone()
1988 },
1989 declined,
1990 )
1991}
1992
1993fn note_unrun_workers(
1998 project: &mut PreparedRustProject,
1999 original: &PreparedRustProject,
2000 declined: &BTreeSet<String>,
2001 built_crates: &BTreeSet<String>,
2002 remote: bool,
2003) {
2004 let directories = crate::rust_worker::worker_crate_directories(&project.workspace_root);
2005 let reason = if remote {
2006 "A Rust Worker started with `wrangler dev --remote`: it ran on Cloudflare's edge, where wrangler opens no inspector, so what it ran cannot be read; a test that starts it with `wrangler dev` measures it"
2007 } else {
2008 "A Rust Worker: its code runs in Cloudflare's runtime, when a test starts it with `wrangler dev`, and no test of this run did"
2009 };
2010 if directories.is_empty() {
2011 return;
2012 }
2013 let ran = built_crates
2014 .iter()
2015 .filter_map(|key| original.probes.crates.get(key))
2016 .flatten()
2017 .collect::<BTreeSet<_>>();
2018 for file in &project.source_files {
2019 let in_worker = directories
2020 .iter()
2021 .any(|directory| directory.is_empty() || file.starts_with(&format!("{directory}/")));
2022 if !in_worker || declined.contains(file) || ran.contains(file) {
2023 continue;
2024 }
2025 let id = format!("rust-worker-not-run#{file}");
2026 if project.manifest.limitations.iter().any(|limitation| {
2027 limitation.get("id").and_then(serde_json::Value::as_str) == Some(id.as_str())
2028 }) {
2029 continue;
2030 }
2031 project.manifest.limitations.push(serde_json::json!({
2032 "id": id,
2033 "kind": "source-scope",
2034 "file": file,
2035 "line": 1,
2036 "column": 0,
2037 "source": "",
2038 "reason": reason,
2039 "blocking": false,
2040 }));
2041 }
2042}
2043
2044fn report_declined(
2046 declined: &BTreeSet<String>,
2047 diagnostics: &mut dyn Write,
2048) -> Result<(), RustTestRunnerError> {
2049 if declined.is_empty() {
2050 return Ok(());
2051 }
2052 writeln!(
2053 diagnostics,
2054 "[supercov] {} source file(s) this build did not compile are outside the denominator: {}{}",
2055 declined.len(),
2056 declined
2057 .iter()
2058 .take(3)
2059 .cloned()
2060 .collect::<Vec<_>>()
2061 .join(", "),
2062 if declined.len() > 3 { ", ..." } else { "" }
2063 )
2064 .map_err(|error| RustTestRunnerError::Io(error.to_string()))
2065}
2066
2067pub fn run_prepared_rust_tests(
2070 project: &PreparedRustProject,
2071 command: &[String],
2072 run_id: &str,
2073 generated_at: &str,
2074 rust_workers: Option<&crate::rust_worker::CargoTestWorkers>,
2075 runner_environment: &[(String, String)],
2076 diagnostics: &mut dyn Write,
2077) -> Result<RustFrontendRun, RustTestRunnerError> {
2078 let invocation = cargo_invocation(&project.workspace_root, command)?;
2079 let selection = rust_cargo_execution_selection(&invocation)?;
2080 let build_started = Instant::now();
2081 let artifacts = if selection.run_libtests && invocation.kind == RustCargoCommandKind::CargoTest
2085 {
2086 build_test_artifacts(project, command)?
2087 } else {
2088 Vec::new()
2089 };
2090 let build_ms = build_started.elapsed().as_secs_f64() * 1000.0;
2091 let (manifest, declined) = decline_uncompiled_sources(project, &BTreeSet::new());
2095 let original = project;
2096 let project = &PreparedRustProject {
2097 manifest,
2098 ..project.clone()
2099 };
2100 let _signal_guard = crate::child_signal_guard::ChildSignalGuard::install()
2104 .map_err(|error| RustTestRunnerError::Launch(error.to_string()))?;
2105 let evidence_root = project
2106 .workspace_root
2107 .join(".supercov/rust-evidence")
2108 .join(run_id);
2109 fs::create_dir_all(&evidence_root)
2110 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
2111 let mut results = Vec::new();
2112 let mut overall_exit = 0;
2113 let execution_started = Instant::now();
2114 if invocation.kind == RustCargoCommandKind::NextestRun {
2115 let outcome = crate::rust_owned_nextest::run_nextest(
2118 project,
2119 &invocation,
2120 &evidence_root.join("nextest"),
2121 run_id,
2122 rust_workers,
2123 diagnostics,
2124 )?;
2125 let (mut project, declined) =
2126 redecline_for_workers(original, project, declined, &outcome.built_crates);
2127 note_unrun_workers(
2128 &mut project,
2129 original,
2130 &declined,
2131 &outcome.built_crates,
2132 outcome.remote_workers,
2133 );
2134 report_declined(&declined, diagnostics)?;
2135 let project = &project;
2136 let artifact_count = outcome.artifact_files.len();
2137 let project = &PreparedRustProject {
2138 manifest: settle_cfg_gates(project, &evidence_root, diagnostics)?,
2139 ..project.clone()
2140 };
2141 return Ok(RustFrontendRun {
2142 declaration: FrontendRunDeclaration {
2143 protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
2144 frontend_id: "rust".into(),
2145 frontend_version: "rust-owned-v1".into(),
2146 language: "rust".into(),
2147 structural_source: StructuralSource::OwnedProbes,
2148 runners: vec![rust_runner_declaration("nextest")],
2149 structural_limitations: structural_limitations(&project.manifest),
2150 },
2151 request: CoverageReportRequest {
2152 run_id: run_id.into(),
2153 manifest: project.manifest.clone(),
2154 raw_results: outcome.results,
2155 generated_at: generated_at.into(),
2156 coverage_model: Some(rust_coverage_model()),
2157 integrity: None,
2158 test_exit_code: ExitCodeInput::Present(Some(outcome.exit_code)),
2159 },
2160 exit_code: outcome.exit_code,
2161 artifacts: artifact_count,
2162 artifact_files: outcome.artifact_files,
2163 build_ms,
2164 execution_ms: execution_started.elapsed().as_secs_f64() * 1000.0,
2165 });
2166 }
2167 let libtest_selection = rust_libtest_selection(&invocation)?;
2173 let coloured_results = libtest_colours(&invocation);
2176 let ignored_mode = libtest_selection
2177 .list_arguments
2178 .iter()
2179 .filter(|argument| {
2180 matches!(
2181 argument.as_str(),
2182 "--ignored" | "--include-ignored" | "--exclude-should-panic"
2183 )
2184 })
2185 .cloned()
2186 .collect::<Vec<_>>();
2187 let mut tasks = Vec::new();
2188 let launcher =
2190 ArtifactLauncher::for_invocation(project, &invocation, &evidence_root.join("hooks"))?
2191 .with_environment(runner_environment);
2192 if let Some(browser) = launcher.unmeasured_browser() {
2193 writeln!(
2194 diagnostics,
2195 "[supercov] wasm tests run in {browser} are not measured: Supercov reads a browser's wasm through Chrome only (`wasm-pack test --chrome`)"
2196 )
2197 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
2198 }
2199 let target_libdir = if artifacts.is_empty() {
2200 PathBuf::new()
2201 } else {
2202 rustc_target_libdir()?
2203 };
2204 let mut unasked = Vec::new();
2206 for (artifact_index, artifact) in artifacts.iter().enumerate() {
2207 let tests = list_tests(
2208 &launcher,
2209 &artifact.executable,
2210 &libtest_selection.list_arguments,
2211 &dynamic_library_environment(&target_libdir, &artifact.executable),
2212 )?;
2213 if tests.is_empty() {
2214 let all = list_tests(
2215 &launcher,
2216 &artifact.executable,
2217 &["--include-ignored".to_owned()],
2218 &dynamic_library_environment(&target_libdir, &artifact.executable),
2219 )?;
2220 unasked.push((artifact, all.len()));
2221 }
2222 let contexts = preflight_rust_test_contexts(tests.clone())
2223 .map_err(|error| RustTestRunnerError::Context(error.to_string()))?;
2224 for (test_index, test) in tests.into_iter().enumerate() {
2225 let directory = evidence_root.join(format!("{artifact_index:04}-{test_index:08}"));
2226 fs::create_dir(&directory)
2227 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
2228 tasks.push(ProcessTask {
2229 ordinal: tasks.len(),
2230 artifact_index,
2231 test_index,
2232 artifact: artifact.clone(),
2233 context_id: contexts[&test],
2234 test,
2235 directory,
2236 });
2237 }
2238 }
2239 let workers = std::thread::available_parallelism()
2240 .map(usize::from)
2241 .unwrap_or(1)
2242 .min(tasks.len().max(1));
2243 let next = AtomicUsize::new(0);
2244 let outcomes = Mutex::new(Vec::<Result<ProcessOutcome, String>>::with_capacity(
2245 tasks.len(),
2246 ));
2247 std::thread::scope(|scope| {
2248 for _ in 0..workers {
2249 scope.spawn(|| {
2250 loop {
2251 let index = next.fetch_add(1, Ordering::Relaxed);
2252 let Some(task) = tasks.get(index) else { break };
2253 let result = crate::child_signal_guard::output(
2254 launcher
2255 .command(&task.artifact.executable, Some(&task.directory))
2256 .args(["--exact", &task.test])
2265 .args(&ignored_mode)
2266 .args(coloured_results.then_some("--color=always"))
2267 .current_dir(&task.artifact.package_directory)
2268 .envs(instrumented_stack_environment())
2269 .envs(dynamic_library_environment(
2270 &target_libdir,
2271 &task.artifact.executable,
2272 ))
2273 .env("SUPERCOV_RUST_EVIDENCE_DIR", &task.directory)
2274 .envs(
2275 rust_workers
2276 .iter()
2277 .flat_map(|workers| workers.environment()),
2278 ),
2279 )
2280 .map(|output| ProcessOutcome {
2281 task: ProcessTask {
2282 ordinal: task.ordinal,
2283 artifact_index: task.artifact_index,
2284 test_index: task.test_index,
2285 artifact: task.artifact.clone(),
2286 test: task.test.clone(),
2287 context_id: task.context_id,
2288 directory: task.directory.clone(),
2289 },
2290 output,
2291 })
2292 .map_err(|error| error.to_string());
2293 outcomes
2294 .lock()
2295 .expect("Rust test result lock poisoned")
2296 .push(result);
2297 }
2298 });
2299 }
2300 });
2301 let mut outcomes = outcomes
2302 .into_inner()
2303 .map_err(|_| RustTestRunnerError::Io("Rust test result lock poisoned".into()))?
2304 .into_iter()
2305 .map(|result| result.map_err(RustTestRunnerError::Launch))
2306 .collect::<Result<Vec<_>, _>>()?;
2307 outcomes.sort_by_key(|outcome| outcome.task.ordinal);
2308 let mut built_crates = BTreeSet::new();
2312 let mut remote = false;
2313 if rust_workers.is_some() {
2314 for outcome in &outcomes {
2315 remote |= crate::rust_worker::started_remote(&outcome.task.directory);
2316 built_crates.extend(
2317 crate::rust_worker::materialize_worker_windows(
2318 &outcome.task.directory,
2319 &project.probes.token,
2320 )
2321 .map_err(RustTestRunnerError::Io)?,
2322 );
2323 }
2324 }
2325 let (mut project, declined) = redecline_for_workers(original, project, declined, &built_crates);
2326 note_unrun_workers(&mut project, original, &declined, &built_crates, remote);
2327 report_declined(&declined, diagnostics)?;
2328 let project = &project;
2329 let unannounced = cargo_quiet(&invocation);
2334 let terse = unannounced || libtest_quiet(&invocation);
2335 let coloured = cargo_colours(&invocation);
2336 let announced = |artifact: &TestArtifact, report: String| {
2337 (
2338 (artifact.kind == "integration", artifact.name.clone()),
2339 crate::rust_libtest_report::Announced {
2340 heading: (!unannounced).then(|| {
2341 running_line(
2342 &project.workspace_root,
2343 &project.target_directory,
2344 artifact,
2345 coloured,
2346 )
2347 }),
2348 report,
2349 },
2350 )
2351 };
2352 let mut reports = Vec::new();
2354 for binary in
2355 outcomes.chunk_by(|left, right| left.task.artifact_index == right.task.artifact_index)
2356 {
2357 let texts = binary
2358 .iter()
2359 .map(|outcome| {
2360 (
2361 String::from_utf8_lossy(&outcome.output.stdout),
2362 String::from_utf8_lossy(&outcome.output.stderr),
2363 )
2364 })
2365 .collect::<Vec<_>>();
2366 let cases = binary
2367 .iter()
2368 .zip(&texts)
2369 .map(
2370 |(outcome, (stdout, stderr))| crate::rust_libtest_report::CaseOutput {
2371 name: &outcome.task.test,
2372 exit: outcome.output.status.code().unwrap_or(1),
2373 stdout,
2374 stderr,
2375 },
2376 )
2377 .collect::<Vec<_>>();
2378 let artifact = &binary[0].task.artifact;
2379 reports.push(announced(
2380 artifact,
2381 crate::rust_libtest_report::libtest_report(&cases, terse),
2382 ));
2383 }
2384 for (artifact, all) in unasked {
2385 reports.push(announced(
2386 artifact,
2387 crate::rust_libtest_report::unasked_report(all),
2388 ));
2389 }
2390 reports.sort_by(|(left, _), (right, _)| left.cmp(right));
2391 let reports = reports
2392 .into_iter()
2393 .map(|(_, report)| report)
2394 .collect::<Vec<_>>();
2395 crate::rust_libtest_report::write_reports(diagnostics, &reports)
2396 .map_err(|error| RustTestRunnerError::Io(error.to_string()))?;
2397 for outcome in outcomes {
2398 let ProcessTask {
2399 artifact_index,
2400 test_index,
2401 artifact,
2402 test,
2403 directory,
2404 ..
2405 } = outcome.task;
2406 let test_id = format!("{}::{test}", artifact.source);
2410 let worker_id = format!("artifact-{artifact_index:04}");
2411 let attempt_id = format!("{run_id}:{artifact_index:04}:{test_index:08}");
2412 let output = outcome.output;
2413 let exit = output.status.code().unwrap_or(1);
2414 let stdout = String::from_utf8_lossy(&output.stdout);
2415 let skipped = libtest_skipped(exit, &stdout);
2416 if exit != 0 {
2417 overall_exit = exit;
2418 }
2419 let evidence = snapshot(&project.manifest, &project.probes, &directory, &attempt_id)?;
2420 results.push(RawTestResult {
2421 span: None,
2422 test_id: Some(test_id.clone()),
2423 scope: Some(ExecutionScope {
2424 version: 1,
2425 run_id: run_id.into(),
2426 worker_id,
2427 test_id: test_id.clone(),
2428 test_key: format!("{}::{test}", artifact.source),
2429 retry: 0,
2430 attempt_id,
2431 }),
2432 test: test_id,
2433 test_file: Some(artifact.source.clone()),
2434 title: Some(test),
2435 retry: Some(0),
2436 status: Some(
2437 if exit != 0 {
2438 "failed"
2439 } else if skipped {
2440 "skipped"
2441 } else {
2442 "passed"
2443 }
2444 .into(),
2445 ),
2446 expected_status: Some("passed".into()),
2447 flaky: false,
2448 provenance: TestProvenance {
2449 runner: "rust-libtest".into(),
2450 kind: artifact.kind,
2451 project: Some(artifact.name),
2452 source: "supercov-owned-process-per-test".into(),
2453 tags: Vec::new(),
2454 },
2455 role: "test".into(),
2456 attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
2457 phases: evidence.phases,
2458 runtime: vec![evidence.snapshot],
2459 browser: Vec::new(),
2460 server: Vec::new(),
2461 });
2462 }
2463 let doctest_results =
2464 if selection.run_doctests && invocation.kind == RustCargoCommandKind::CargoTest {
2465 crate::rust_owned_doctests::run_doctests(
2466 project,
2467 &invocation,
2468 &selection,
2469 &evidence_root.join("doctests"),
2470 run_id,
2471 diagnostics,
2472 &mut overall_exit,
2473 )?
2474 } else {
2475 Vec::new()
2476 };
2477 let project = &PreparedRustProject {
2480 manifest: settle_cfg_gates(project, &evidence_root, diagnostics)?,
2481 ..project.clone()
2482 };
2483 let ran_libtests = !results.is_empty();
2484 let ran_doctests = !doctest_results.is_empty();
2485 results.extend(doctest_results);
2486 let mut runners = Vec::new();
2487 if ran_libtests || !ran_doctests {
2488 runners.push(rust_runner_declaration("rust-libtest"));
2489 }
2490 if ran_doctests {
2491 runners.push(rust_runner_declaration("rustdoc"));
2492 }
2493 let structural_limitations = structural_limitations(&project.manifest);
2494 Ok(RustFrontendRun {
2495 declaration: FrontendRunDeclaration {
2496 protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
2497 frontend_id: "rust".into(),
2498 frontend_version: "rust-owned-v1".into(),
2499 language: "rust".into(),
2500 structural_source: StructuralSource::OwnedProbes,
2501 runners,
2502 structural_limitations,
2503 },
2504 request: CoverageReportRequest {
2505 run_id: run_id.into(),
2506 manifest: project.manifest.clone(),
2507 raw_results: results,
2508 generated_at: generated_at.into(),
2509 coverage_model: Some(rust_coverage_model()),
2510 integrity: None,
2511 test_exit_code: ExitCodeInput::Present(Some(overall_exit)),
2512 },
2513 exit_code: overall_exit,
2514 artifacts: artifacts.len(),
2515 artifact_files: artifacts
2516 .iter()
2517 .map(|artifact| artifact.executable.clone())
2518 .collect(),
2519 build_ms,
2520 execution_ms: execution_started.elapsed().as_secs_f64() * 1000.0,
2521 })
2522}
2523
2524#[cfg(test)]
2525mod tests {
2526
2527 #[test]
2528 fn node_takes_a_hook_by_a_file_url_with_its_bytes_encoded() {
2529 assert_eq!(
2530 file_url(Path::new("/tmp/a b/#hook.mjs")),
2531 "file:///tmp/a%20b/%23hook.mjs"
2532 );
2533 assert_eq!(file_url(Path::new("/srv/hook.mjs")), "file:///srv/hook.mjs");
2534 }
2535
2536 #[test]
2537 fn a_wasi_runtime_is_known_by_its_name() {
2538 assert!(is_wasi_runtime(Path::new("wasmtime")));
2539 assert!(is_wasi_runtime(Path::new("/opt/homebrew/bin/wasmtime")));
2540 assert!(is_wasi_runtime(Path::new("wasmer")));
2541 assert!(!is_wasi_runtime(Path::new("wasm-bindgen-test-runner")));
2542 assert!(!is_wasi_runtime(Path::new("qemu-aarch64")));
2543 }
2544
2545 use std::time::{SystemTime, UNIX_EPOCH};
2546
2547 use super::*;
2548 use crate::{
2549 coverage_report::{ArchiveReportRequest, analyze_coverage_archive},
2550 evidence_archive::write_archive,
2551 frontend_protocol::validate_frontend_report_request,
2552 rust_project::prepare_rust_project,
2553 };
2554
2555 #[test]
2556 fn a_test_binary_is_announced_as_cargo_announces_it() {
2557 let artifact = |package: &str, source: &str, kind: &str| TestArtifact {
2561 executable: PathBuf::from(
2562 "/work/copy/.supercov/cache/rust-target/debug/deps/shop-0123456789abcdef",
2563 ),
2564 name: "shop".into(),
2565 kind: kind.into(),
2566 source: source.into(),
2567 package_directory: PathBuf::from(package),
2568 };
2569 let line = |artifact: &TestArtifact| {
2570 running_line(
2571 Path::new("/work/copy"),
2572 Path::new("/work/copy/.supercov/cache/rust-target"),
2573 artifact,
2574 false,
2575 )
2576 .replace('\\', "/")
2577 };
2578 assert_eq!(
2579 line(&artifact("/work/copy", "src/lib.rs", "unit")),
2580 " Running unittests src/lib.rs (target/debug/deps/shop-0123456789abcdef)"
2581 );
2582 assert_eq!(
2583 line(&artifact(
2584 "/work/copy/crates/shop",
2585 "crates/shop/tests/outside.rs",
2586 "integration"
2587 )),
2588 " Running tests/outside.rs (target/debug/deps/shop-0123456789abcdef)"
2589 );
2590 let windows = running_line(
2592 Path::new("C:\\work\\copy"),
2593 Path::new("C:\\work\\copy\\target"),
2594 &artifact(
2595 "\\\\?\\C:\\work\\copy\\crates\\shop",
2596 "crates/shop/src/lib.rs",
2597 "unit",
2598 ),
2599 false,
2600 )
2601 .replace('\\', "/");
2602 assert!(
2603 windows.starts_with(" Running unittests src/lib.rs ("),
2604 "{windows}"
2605 );
2606 assert!(
2607 running_line(
2608 Path::new("/work/copy"),
2609 Path::new("/work/copy/.supercov/cache/rust-target"),
2610 &artifact("/work/copy", "src/lib.rs", "unit"),
2611 true,
2612 )
2613 .starts_with("\u{1b}[1m\u{1b}[92m Running\u{1b}[0m unittests ")
2614 );
2615 }
2616
2617 #[test]
2620 fn a_filter_given_to_a_package_script_reaches_cargo() {
2621 let root = std::env::temp_dir().join(format!(
2622 "supercov-npm-cargo-{}-{}",
2623 std::process::id(),
2624 std::time::SystemTime::now()
2625 .duration_since(std::time::UNIX_EPOCH)
2626 .unwrap()
2627 .as_nanos()
2628 ));
2629 std::fs::create_dir(&root).unwrap();
2630 std::fs::write(
2631 root.join("package.json"),
2632 r#"{ "scripts": { "test": "cargo test --lib" } }"#,
2633 )
2634 .unwrap();
2635 let invocation = cargo_invocation(
2636 &root,
2637 &["npm".into(), "test".into(), "--".into(), "adds".into()],
2638 )
2639 .unwrap();
2640 std::fs::remove_dir_all(&root).unwrap();
2641 assert_eq!(invocation.kind, RustCargoCommandKind::CargoTest);
2642 assert_eq!(invocation.arguments, ["test", "--lib", "adds"]);
2643 }
2644
2645 #[test]
2646 fn cargo_and_libtest_selection_is_preserved_without_presentation_guessing() {
2647 let root = Path::new(".");
2648 let invocation = cargo_invocation(
2649 root,
2650 &[
2651 "cargo".into(),
2652 "test".into(),
2653 "-p".into(),
2654 "fixture".into(),
2655 "authored".into(),
2656 "--".into(),
2657 "generated".into(),
2658 "--skip".into(),
2659 "slow".into(),
2660 "--include-ignored".into(),
2661 ],
2662 )
2663 .unwrap();
2664 assert_eq!(invocation.arguments, ["test", "-p", "fixture", "authored"]);
2665 assert_eq!(
2666 invocation.runner_arguments,
2667 ["generated", "--skip", "slow", "--include-ignored"]
2668 );
2669 let selection = rust_libtest_selection(&invocation).unwrap();
2670 assert_eq!(
2671 selection.list_arguments,
2672 [
2673 "authored",
2674 "generated",
2675 "--skip",
2676 "slow",
2677 "--include-ignored"
2678 ]
2679 );
2680 }
2681
2682 #[test]
2683 fn direct_cargo_argv_preserves_toml_quotes_inside_config_values() {
2684 let config = "target.host.runner=[\"runner with spaces\",\"--fixed\"]";
2685 let invocation = cargo_invocation(
2686 Path::new("."),
2687 &[
2688 "cargo".into(),
2689 "test".into(),
2690 "--config".into(),
2691 config.into(),
2692 ],
2693 )
2694 .unwrap();
2695 assert_eq!(invocation.arguments, ["test", "--config", config]);
2696 }
2697
2698 #[test]
2699 fn nextest_run_is_detected_without_reclassifying_its_filters_or_retries() {
2700 let invocation = cargo_invocation(
2701 Path::new("."),
2702 &[
2703 "cargo".into(),
2704 "+1.95.0".into(),
2705 "nextest".into(),
2706 "run".into(),
2707 "--retries".into(),
2708 "2".into(),
2709 "-E".into(),
2710 "test(/flaky/)".into(),
2711 "--".into(),
2712 "--nocapture".into(),
2713 ],
2714 )
2715 .unwrap();
2716 assert_eq!(invocation.kind, RustCargoCommandKind::NextestRun);
2717 assert_eq!(
2718 invocation.arguments,
2719 [
2720 "+1.95.0",
2721 "nextest",
2722 "run",
2723 "--retries",
2724 "2",
2725 "-E",
2726 "test(/flaky/)",
2727 ]
2728 );
2729 assert_eq!(invocation.runner_arguments, ["--nocapture"]);
2730 let execution = rust_cargo_execution_selection(&invocation).unwrap();
2731 assert!(execution.run_libtests);
2732 assert!(!execution.run_doctests);
2733 assert!(execution.doctest_arguments.is_empty());
2734 assert_eq!(
2735 nextest_list_invocation(&invocation).unwrap(),
2736 NextestListInvocation {
2737 arguments: vec![
2738 "+1.95.0".into(),
2739 "nextest".into(),
2740 "list".into(),
2741 "-E".into(),
2742 "test(/flaky/)".into(),
2743 "--message-format".into(),
2744 "json".into(),
2745 ],
2746 runner_arguments: vec!["--nocapture".into()],
2747 }
2748 );
2749 }
2750
2751 #[test]
2752 fn nextest_list_projection_preserves_selection_and_rejects_external_state() {
2753 let invocation = CargoTestInvocation {
2754 program: "cargo".into(),
2755 kind: RustCargoCommandKind::NextestRun,
2756 arguments: vec![
2757 "nextest".into(),
2758 "run".into(),
2759 "--package=fixture".into(),
2760 "--partition".into(),
2761 "hash:1/2".into(),
2762 "--test-threads=8".into(),
2763 "--failure-output".into(),
2764 "final".into(),
2765 "name".into(),
2766 ],
2767 runner_arguments: vec!["--exact".into(), "full::name".into()],
2768 };
2769 assert_eq!(
2770 nextest_list_invocation(&invocation).unwrap(),
2771 NextestListInvocation {
2772 arguments: vec![
2773 "nextest".into(),
2774 "list".into(),
2775 "--package=fixture".into(),
2776 "--partition".into(),
2777 "hash:1/2".into(),
2778 "name".into(),
2779 "--message-format".into(),
2780 "json".into(),
2781 ],
2782 runner_arguments: vec!["--exact".into(), "full::name".into()],
2783 }
2784 );
2785
2786 let mut rerun = invocation;
2787 rerun.arguments.extend(["--rerun".into(), "latest".into()]);
2788 assert!(
2789 nextest_list_invocation(&rerun)
2790 .unwrap_err()
2791 .to_string()
2792 .contains("cannot yet be assigned exact selected-test identity")
2793 );
2794 }
2795
2796 #[test]
2798 fn a_script_command_splits_the_way_sh_splits_it() {
2799 let words = |text: &str| shell_words(text).unwrap();
2800 assert_eq!(
2801 words("cargo test --workspace"),
2802 ["cargo", "test", "--workspace"]
2803 );
2804 assert_eq!(
2805 words(r#"cargo test --config 'target.x.runner="a b"' -- "two words""#),
2806 [
2807 "cargo",
2808 "test",
2809 "--config",
2810 r#"target.x.runner="a b""#,
2811 "--",
2812 "two words"
2813 ]
2814 );
2815 assert_eq!(
2817 words(r#"cargo test -- '' """#),
2818 ["cargo", "test", "--", "", ""]
2819 );
2820 assert_eq!(
2822 words(r#""C:\work\app" "say \"hi\" \$HOME""#),
2823 [r"C:\work\app", r#"say "hi" $HOME"#]
2824 );
2825 assert_eq!(words(r"a\ b 'c\d'"), ["a b", r"c\d"]);
2827 assert_eq!(words("joined'' x\"\"y"), ["joined", "xy"]);
2828 assert_eq!(
2829 words("cargo test \\\n --workspace \"a\\\nb\""),
2830 ["cargo", "test", "--workspace", "ab"]
2831 );
2832 for broken in ["cargo test 'open", "cargo test \"open", r"cargo test \"] {
2833 assert!(shell_words(broken).is_err(), "{broken}");
2834 }
2835 }
2836
2837 #[test]
2838 fn nextest_list_projection_keeps_short_options_with_attached_values() {
2839 let invocation = CargoTestInvocation {
2840 program: "cargo".into(),
2841 kind: RustCargoCommandKind::NextestRun,
2842 arguments: [
2843 "nextest",
2844 "run",
2845 "-pfixture",
2846 "-Ffast",
2847 "-Etest(=a)",
2848 "-Pci",
2849 "-Zbuild-std",
2850 "-vv",
2851 "name",
2852 ]
2853 .map(String::from)
2854 .to_vec(),
2855 runner_arguments: Vec::new(),
2856 };
2857 let listed = nextest_list_invocation(&invocation).unwrap();
2858 assert_eq!(
2861 listed.arguments,
2862 [
2863 "nextest",
2864 "list",
2865 "-pfixture",
2866 "-Ffast",
2867 "-Etest(=a)",
2868 "-Pci",
2869 "-Zbuild-std",
2870 "-vv",
2871 "name",
2872 "--message-format",
2873 "json",
2874 ]
2875 );
2876 }
2877
2878 #[test]
2879 fn nextest_list_projection_preserves_post_separator_libtest_selection() {
2880 let invocation = cargo_invocation(
2881 Path::new("."),
2882 &[
2883 "cargo".into(),
2884 "nextest".into(),
2885 "run".into(),
2886 "--timings".into(),
2887 "-vv".into(),
2888 "--".into(),
2889 "--include-ignored".into(),
2890 "--skip".into(),
2891 "slow".into(),
2892 "--exact".into(),
2893 "tests::selected".into(),
2894 ],
2895 )
2896 .unwrap();
2897 assert_eq!(
2898 nextest_list_invocation(&invocation).unwrap(),
2899 NextestListInvocation {
2900 arguments: vec![
2901 "nextest".to_owned(),
2902 "list".to_owned(),
2903 "--timings".to_owned(),
2904 "-vv".to_owned(),
2905 "--message-format".to_owned(),
2906 "json".to_owned(),
2907 ],
2908 runner_arguments: vec![
2909 "--include-ignored".to_owned(),
2910 "--skip".to_owned(),
2911 "slow".to_owned(),
2912 "--exact".to_owned(),
2913 "tests::selected".to_owned(),
2914 ],
2915 }
2916 );
2917 }
2918
2919 #[test]
2920 fn nextest_version_handshake_preserves_the_cargo_toolchain_selector() {
2921 let invocation = CargoTestInvocation {
2922 program: "cargo".into(),
2923 kind: RustCargoCommandKind::NextestRun,
2924 arguments: vec![
2925 "+1.95.0".into(),
2926 "nextest".into(),
2927 "run".into(),
2928 "-p".into(),
2929 "fixture".into(),
2930 ],
2931 runner_arguments: Vec::new(),
2932 };
2933 assert_eq!(
2934 nextest_version_arguments(&invocation),
2935 ["+1.95.0", "nextest", "--version"]
2936 );
2937 }
2938
2939 #[test]
2940 fn stock_libtest_presentation_and_scheduling_options_do_not_change_discovery() {
2941 let invocation = CargoTestInvocation {
2942 program: "cargo".into(),
2943 kind: RustCargoCommandKind::CargoTest,
2944 arguments: vec!["test".into(), "cargo-filter".into()],
2945 runner_arguments: [
2946 "runner-filter",
2947 "--nocapture",
2948 "--show-output",
2949 "--format=json",
2950 "--color",
2951 "never",
2952 "--test-threads=4",
2953 "--fail-fast",
2954 "--shuffle-seed",
2955 "17",
2956 "-Zunstable-options",
2957 "--exclude-should-panic",
2958 ]
2959 .into_iter()
2960 .map(str::to_owned)
2961 .collect(),
2962 };
2963 let selection = rust_libtest_selection(&invocation).unwrap();
2964 assert_eq!(
2965 selection.list_arguments,
2966 [
2967 "cargo-filter",
2968 "runner-filter",
2969 "-Zunstable-options",
2970 "--exclude-should-panic"
2971 ]
2972 );
2973 }
2974
2975 #[test]
2976 fn cargo_test_options_are_not_mistaken_for_the_test_name_filter() {
2977 let invocation = CargoTestInvocation {
2978 program: "cargo".into(),
2979 kind: RustCargoCommandKind::CargoTest,
2980 arguments: vec![
2981 "test".into(),
2982 "--manifest-path".into(),
2983 "nested/Cargo.toml".into(),
2984 "--features=one,two".into(),
2985 "needle".into(),
2986 ],
2987 runner_arguments: vec!["--ignored".into(), "other".into()],
2988 };
2989 let selection = rust_libtest_selection(&invocation).unwrap();
2990 assert_eq!(selection.list_arguments, ["needle", "--ignored", "other"]);
2991 }
2992
2993 #[test]
2994 fn libtest_thread_count_is_preserved_as_runner_scheduling() {
2995 for arguments in [vec!["--test-threads", "1"], vec!["--test-threads=8"]] {
2996 let invocation = CargoTestInvocation {
2997 program: "cargo".into(),
2998 kind: RustCargoCommandKind::CargoTest,
2999 arguments: vec!["test".into()],
3000 runner_arguments: arguments.into_iter().map(str::to_owned).collect(),
3001 };
3002 let selection = rust_libtest_selection(&invocation).unwrap();
3003 assert!(selection.list_arguments.is_empty());
3004 }
3005 }
3006
3007 #[test]
3008 fn invalid_or_duplicate_libtest_thread_counts_fail_closed() {
3009 for arguments in [
3010 vec!["--test-threads"],
3011 vec!["--test-threads=0"],
3012 vec!["--test-threads=abc"],
3013 vec!["--test-threads", "1", "--test-threads=2"],
3014 ] {
3015 let invocation = CargoTestInvocation {
3016 program: "cargo".into(),
3017 kind: RustCargoCommandKind::CargoTest,
3018 arguments: vec!["test".into()],
3019 runner_arguments: arguments.into_iter().map(str::to_owned).collect(),
3020 };
3021 assert!(rust_libtest_selection(&invocation).is_err());
3022 }
3023 }
3024
3025 #[test]
3026 fn cargo_target_selection_reproduces_when_cargo_runs_doctests() {
3027 let invocation = CargoTestInvocation {
3028 program: "cargo".into(),
3029 kind: RustCargoCommandKind::CargoTest,
3030 arguments: vec![
3031 "test".into(),
3032 "-p".into(),
3033 "fixture".into(),
3034 "needle".into(),
3035 ],
3036 runner_arguments: vec!["--include-ignored".into()],
3037 };
3038 let selection = rust_cargo_execution_selection(&invocation).unwrap();
3039 assert!(selection.run_libtests);
3040 assert!(selection.run_doctests);
3041 assert_eq!(
3042 selection.doctest_arguments,
3043 [
3044 "test",
3045 "--doc",
3046 "-p",
3047 "fixture",
3048 "needle",
3049 "--",
3050 "--include-ignored"
3051 ]
3052 );
3053
3054 let mut explicit_doc = invocation.clone();
3055 explicit_doc.arguments.insert(1, "--doc".into());
3056 let selection = rust_cargo_execution_selection(&explicit_doc).unwrap();
3057 assert!(!selection.run_libtests);
3058 assert!(selection.run_doctests);
3059
3060 for target in ["--lib", "--tests", "--all-targets", "--example=demo"] {
3061 let mut selected = invocation.clone();
3062 selected.arguments.insert(1, target.into());
3063 let selection = rust_cargo_execution_selection(&selected).unwrap();
3064 assert!(selection.run_libtests);
3065 assert!(!selection.run_doctests);
3066 }
3067 }
3068
3069 #[test]
3070 fn a_module_the_build_never_compiles_leaves_the_denominator() {
3071 let nonce = SystemTime::now()
3075 .duration_since(UNIX_EPOCH)
3076 .unwrap()
3077 .as_nanos();
3078 let root = std::env::temp_dir().join(format!(
3079 "supercov-rust-runner-cfg-{}-{nonce}",
3080 std::process::id()
3081 ));
3082 fs::create_dir_all(root.join("src")).unwrap();
3083 fs::write(
3084 root.join("Cargo.toml"),
3085 "[package]\nname='fixture'\nversion='0.0.0'\nedition='2024'\n\n[features]\nextra=[]\n",
3086 )
3087 .unwrap();
3088 fs::write(
3089 root.join("src/lib.rs"),
3090 r#"
3091#[cfg(feature = "extra")]
3092mod extra;
3093
3094pub fn kept(value: i32) -> i32 {
3095 if value > 0 { value } else { -value }
3096}
3097#[cfg(test)]
3098mod tests {
3099 #[test] fn positive() { assert_eq!(super::kept(2), 2); }
3100 #[test] fn negative() { assert_eq!(super::kept(-2), 2); }
3101}
3102"#,
3103 )
3104 .unwrap();
3105 fs::write(
3106 root.join("src/extra.rs"),
3107 "pub fn never_built(value: i32) -> i32 {\n if value > 0 { 1 } else { 0 }\n}\n",
3108 )
3109 .unwrap();
3110 let project = prepare_rust_project(&root, None).unwrap();
3111 assert!(
3113 project
3114 .source_files
3115 .iter()
3116 .any(|file| file == "src/extra.rs"),
3117 "{:?}",
3118 project.source_files
3119 );
3120 let run = run_prepared_rust_tests(
3121 &project,
3122 &["cargo".into(), "test".into(), "--lib".into()],
3123 "rust-fixture-cfg",
3124 "2026-08-26T00:00:00.000Z",
3125 None,
3126 &[],
3127 &mut Vec::new(),
3128 )
3129 .unwrap();
3130 assert_eq!(run.exit_code, 0);
3131 let manifest = &run.request.manifest;
3134 assert!(
3135 manifest
3136 .points
3137 .iter()
3138 .any(|point| point.file == "src/extra.rs"),
3139 "the obligations must stay in the manifest"
3140 );
3141 let declined = manifest.unmeasured.iter().collect::<BTreeSet<_>>();
3142 for point in &manifest.points {
3143 assert_eq!(
3144 declined.contains(&point.id),
3145 point.file == "src/extra.rs",
3146 "{} in {}",
3147 point.id,
3148 point.file
3149 );
3150 }
3151 for decision in &manifest.decisions {
3152 assert_eq!(
3153 declined.contains(&decision.id),
3154 decision.file == "src/extra.rs"
3155 );
3156 }
3157 let report =
3159 crate::frontend_protocol::analyze_frontend_results(&run.declaration, &run.request)
3160 .unwrap();
3161 assert_eq!(
3162 report.view.summary.lines.percentage, 100.0,
3163 "declined obligations must not read as uncovered"
3164 );
3165 fs::remove_dir_all(&root).ok();
3166 }
3167
3168 #[test]
3169 fn assertion_occurrences_are_retained_without_automatic_asserted_credit() {
3170 let nonce = SystemTime::now()
3175 .duration_since(UNIX_EPOCH)
3176 .unwrap()
3177 .as_nanos();
3178 let root = std::env::temp_dir().join(format!(
3179 "supercov-rust-runner-assert-{}-{nonce}",
3180 std::process::id()
3181 ));
3182 fs::create_dir_all(root.join("src")).unwrap();
3183 fs::write(
3184 root.join("Cargo.toml"),
3185 "[package]\nname='fixture'\nversion='0.0.0'\nedition='2024'\n",
3186 )
3187 .unwrap();
3188 fs::write(
3189 root.join("src/lib.rs"),
3190 r#"
3191pub fn checked(value: i32) -> i32 {
3192 value * 2
3193}
3194pub fn unchecked(value: i32) -> i32 {
3195 value + 1
3196}
3197#[cfg(test)]
3198mod tests {
3199 #[test]
3200 fn asserts() {
3201 let doubled = super::checked(2);
3202 assert_eq!(doubled, 4);
3203 }
3204 #[test]
3205 fn asserts_nothing() {
3206 let _ = super::unchecked(1);
3207 }
3208}
3209"#,
3210 )
3211 .unwrap();
3212 let project = prepare_rust_project(&root, None).unwrap();
3213 let run = run_prepared_rust_tests(
3214 &project,
3215 &["cargo".into(), "test".into(), "--lib".into()],
3216 "rust-fixture-assert",
3217 "2026-08-26T00:00:00.000Z",
3218 None,
3219 &[],
3220 &mut Vec::new(),
3221 )
3222 .unwrap();
3223 assert_eq!(run.exit_code, 0);
3224
3225 let phases = |test: &str| {
3228 run.request
3229 .raw_results
3230 .iter()
3231 .find(|result| result.test.ends_with(test))
3232 .unwrap_or_else(|| panic!("no test {test}"))
3233 .phases
3234 .clone()
3235 };
3236 let asserting = phases("asserts");
3237 assert!(
3238 !asserting.is_empty(),
3239 "the asserting test recorded no phase"
3240 );
3241 assert!(
3242 asserting
3243 .iter()
3244 .all(|phase| phase.kind == "assertion" && phase.status.as_deref() == Some("passed"))
3245 );
3246 assert!(
3247 phases("asserts_nothing").is_empty(),
3248 "a test that checks nothing witnesses nothing"
3249 );
3250
3251 let report =
3252 crate::frontend_protocol::analyze_frontend_results(&run.declaration, &run.request)
3253 .unwrap();
3254 let level = |line: usize| {
3255 report
3256 .view
3257 .lines
3258 .iter()
3259 .find(|entry| entry.file == "src/lib.rs" && entry.line == line)
3260 .map(|entry| entry.confidence.level.clone())
3261 .unwrap_or_else(|| panic!("no line {line}"))
3262 };
3263 assert_eq!(level(3), "executed");
3265 assert_eq!(level(6), "executed");
3266 fs::remove_dir_all(&root).ok();
3267 }
3268
3269 #[test]
3270 fn libtest_filters_select_the_tests_plain_cargo_would_run() {
3271 let nonce = SystemTime::now()
3275 .duration_since(UNIX_EPOCH)
3276 .unwrap()
3277 .as_nanos();
3278 let root = std::env::temp_dir().join(format!(
3279 "supercov-rust-runner-filters-{}-{nonce}",
3280 std::process::id()
3281 ));
3282 fs::create_dir_all(root.join("src")).unwrap();
3283 fs::write(
3284 root.join("Cargo.toml"),
3285 "[package]\nname='fixture'\nversion='0.0.0'\nedition='2024'\n",
3286 )
3287 .unwrap();
3288 fs::write(
3289 root.join("src/lib.rs"),
3290 r#"
3291pub fn value(flag: bool) -> i32 { if flag { 1 } else { 0 } }
3292#[cfg(test)]
3293mod tests {
3294 #[test] fn alpha() { assert_eq!(super::value(true), 1); }
3295 #[test] fn beta() { assert_eq!(super::value(false), 0); }
3296 #[test] #[ignore] fn gamma_slow() { assert_eq!(super::value(true), 1); }
3297}
3298"#,
3299 )
3300 .unwrap();
3301 let project = prepare_rust_project(&root, None).unwrap();
3302 let names = |run: &str, command: &[&str]| {
3303 let run = run_prepared_rust_tests(
3304 &project,
3305 &command
3306 .iter()
3307 .map(|word| (*word).into())
3308 .collect::<Vec<String>>(),
3309 run,
3310 "2026-08-26T00:00:00.000Z",
3311 None,
3312 &[],
3313 &mut Vec::new(),
3314 )
3315 .unwrap();
3316 assert_eq!(run.exit_code, 0, "{:?}", run.request.raw_results);
3317 let mut names = run
3318 .request
3319 .raw_results
3320 .iter()
3321 .map(|result| {
3322 (
3323 result.test.rsplit("::").next().unwrap().to_owned(),
3324 result.status.clone().unwrap(),
3325 )
3326 })
3327 .collect::<Vec<_>>();
3328 names.sort();
3329 names
3330 };
3331 assert_eq!(
3333 names("rust-fixture-filters-1", &["cargo", "test", "--lib"]),
3334 [
3335 ("alpha".to_owned(), "passed".to_owned()),
3336 ("beta".to_owned(), "passed".to_owned()),
3337 ("gamma_slow".to_owned(), "skipped".to_owned()),
3338 ]
3339 );
3340 assert_eq!(
3342 names(
3343 "rust-fixture-filters-2",
3344 &["cargo", "test", "--lib", "alpha"]
3345 ),
3346 [("alpha".to_owned(), "passed".to_owned())]
3347 );
3348 assert_eq!(
3349 names(
3350 "rust-fixture-filters-3",
3351 &["cargo", "test", "--lib", "--", "--skip", "beta"]
3352 ),
3353 [
3354 ("alpha".to_owned(), "passed".to_owned()),
3355 ("gamma_slow".to_owned(), "skipped".to_owned()),
3356 ]
3357 );
3358 assert_eq!(
3360 names(
3361 "rust-fixture-filters-4",
3362 &["cargo", "test", "--lib", "--", "--ignored"]
3363 ),
3364 [("gamma_slow".to_owned(), "passed".to_owned())]
3365 );
3366 fs::remove_dir_all(&root).ok();
3367 }
3368
3369 #[test]
3370 fn libtest_processes_run_in_their_package_directory() {
3371 let nonce = SystemTime::now()
3375 .duration_since(UNIX_EPOCH)
3376 .unwrap()
3377 .as_nanos();
3378 let root = std::env::temp_dir().join(format!(
3379 "supercov-rust-runner-cwd-{}-{nonce}",
3380 std::process::id()
3381 ));
3382 fs::create_dir_all(root.join("member/src")).unwrap();
3383 fs::write(
3384 root.join("Cargo.toml"),
3385 "[workspace]\nmembers = ['member']\nresolver = '2'\n",
3386 )
3387 .unwrap();
3388 fs::write(
3389 root.join("member/Cargo.toml"),
3390 "[package]\nname='member'\nversion='0.0.0'\nedition='2024'\n",
3391 )
3392 .unwrap();
3393 fs::write(
3394 root.join("member/src/lib.rs"),
3395 r#"
3396pub fn manifest() -> String {
3397 std::fs::read_to_string("Cargo.toml").unwrap()
3398}
3399#[cfg(test)]
3400mod tests {
3401 #[test] fn reads_own_manifest() { assert!(super::manifest().contains("name='member'")); }
3402}
3403"#,
3404 )
3405 .unwrap();
3406 let project = prepare_rust_project(&root, None).unwrap();
3407 let run = run_prepared_rust_tests(
3408 &project,
3409 &["cargo".into(), "test".into(), "--lib".into()],
3410 "rust-fixture-cwd",
3411 "2026-08-26T00:00:00.000Z",
3412 None,
3413 &[],
3414 &mut Vec::new(),
3415 )
3416 .unwrap();
3417 assert_eq!(run.exit_code, 0, "{:?}", run.request.raw_results);
3418 assert_eq!(run.request.raw_results.len(), 1);
3419 assert_eq!(run.request.raw_results[0].status.as_deref(), Some("passed"));
3420 fs::remove_dir_all(&root).ok();
3421 }
3422
3423 #[test]
3424 fn cargo_libtest_runs_produce_queryable_owned_evidence() {
3425 let nonce = SystemTime::now()
3426 .duration_since(UNIX_EPOCH)
3427 .unwrap()
3428 .as_nanos();
3429 let root = std::env::temp_dir().join(format!(
3430 "supercov-rust-runner-{}-{nonce}",
3431 std::process::id()
3432 ));
3433 fs::create_dir_all(root.join("src")).unwrap();
3434 fs::write(
3435 root.join("Cargo.toml"),
3436 "[package]\nname='fixture'\nversion='0.0.0'\nedition='2024'\n",
3437 )
3438 .unwrap();
3439 fs::write(
3440 root.join("src/lib.rs"),
3441 r#"
3442pub fn choose(left: bool, right: bool) -> i32 {
3443 if left && right { 1 } else { 0 }
3444}
3445pub fn pick(value: i32) -> &'static str {
3446 match value {
3447 0 => "zero",
3448 1 => "one",
3449 _ => "many",
3450 }
3451}
3452pub fn total(values: &[i32]) -> i32 {
3453 let mut sum = 0;
3454 for value in values {
3455 sum += value;
3456 }
3457 sum
3458}
3459pub fn first_even(values: &[i32]) -> Option<i32> {
3460 let mut index = 0;
3461 while index < values.len() {
3462 if values[index] % 2 == 0 {
3463 return Some(values[index]);
3464 }
3465 index += 1;
3466 }
3467 None
3468}
3469pub fn parse_twice(text: &str) -> Option<i32> {
3470 let value: i32 = text.parse().ok()?;
3471 Some(value * 2)
3472}
3473pub fn describe(value: Option<i32>, flag: bool) -> &'static str {
3474 if let Some(inner) = value && inner > 0 && flag {
3475 "positive"
3476 } else {
3477 "other"
3478 }
3479}
3480pub fn depth(n: u32) -> Result<u32, String> {
3481 if n == 0 {
3482 Ok(0)
3483 } else {
3484 let below = depth(n - 1)?;
3485 Ok(below + 1)
3486 }
3487}
3488#[cfg(test)]
3489mod tests {
3490 #[test] fn false_path() { assert_eq!(super::choose(false, true), 0); }
3491 #[test] fn true_path() { assert_eq!(super::choose(true, true), 1); }
3492 #[test] #[ignore] fn ignored_path() { unreachable!(); }
3493 #[test] fn pick_zero() { assert_eq!(super::pick(0), "zero"); }
3494 #[test] fn pick_many() { assert_eq!(super::pick(7), "many"); }
3495 #[test] fn total_empty() { assert_eq!(super::total(&[]), 0); }
3496 #[test] fn total_some() { assert_eq!(super::total(&[1, 2]), 3); }
3497 #[test] fn first_even_empty() { assert_eq!(super::first_even(&[]), None); }
3498 #[test] fn first_even_found() { assert_eq!(super::first_even(&[1, 4]), Some(4)); }
3499 #[test] fn parse_ok() { assert_eq!(super::parse_twice("4"), Some(8)); }
3500 #[test] fn parse_bad() { assert_eq!(super::parse_twice("x"), None); }
3501 #[test] fn chain_taken() { assert_eq!(super::describe(Some(1), true), "positive"); }
3502 #[test] fn chain_pattern_fails() { assert_eq!(super::describe(None, true), "other"); }
3503 #[test] fn chain_negative() { assert_eq!(super::describe(Some(-1), true), "other"); }
3504 #[test] fn chain_flag_fails() { assert_eq!(super::describe(Some(1), false), "other"); }
3505 #[test] fn deep_recursion() { assert_eq!(super::depth(5_000), Ok(5_000)); }
3506}
3507"#,
3508 )
3509 .unwrap();
3510 let project = prepare_rust_project(&root, None).unwrap();
3511 let run = run_prepared_rust_tests(
3515 &project,
3516 &["cargo".into(), "test".into(), "--lib".into()],
3517 "rust-fixture",
3518 "2026-08-26T00:00:00.000Z",
3519 None,
3520 &[],
3521 &mut Vec::new(),
3522 )
3523 .unwrap();
3524 assert_eq!(run.exit_code, 0);
3525 assert_eq!(run.request.raw_results.len(), 16);
3526 let statuses = run
3527 .request
3528 .raw_results
3529 .iter()
3530 .filter_map(|result| result.status.as_deref())
3531 .collect::<Vec<_>>();
3532 assert_eq!(
3533 statuses
3534 .iter()
3535 .filter(|status| **status == "skipped")
3536 .count(),
3537 1
3538 );
3539 assert_eq!(
3540 statuses
3541 .iter()
3542 .filter(|status| **status == "passed")
3543 .count(),
3544 15
3545 );
3546
3547 let chain_vectors = |test: &str| {
3550 let result = run
3551 .request
3552 .raw_results
3553 .iter()
3554 .find(|result| result.test.ends_with(test))
3555 .unwrap_or_else(|| panic!("no test {test}"));
3556 let snapshot = result
3557 .runtime
3558 .iter()
3559 .flat_map(|snapshot| &snapshot.decisions)
3560 .find(|decision| decision.meta.source.starts_with("let Some(inner) = value"))
3561 .unwrap_or_else(|| panic!("{test} recorded no chain decision"));
3562 snapshot
3563 .vectors
3564 .iter()
3565 .map(|vector| (vector.values.clone(), vector.outcome))
3566 .collect::<Vec<_>>()
3567 };
3568 assert_eq!(
3569 chain_vectors("chain_taken"),
3570 [(vec![Some(true), Some(true), Some(true)], true)]
3571 );
3572 assert_eq!(
3573 chain_vectors("chain_pattern_fails"),
3574 [(vec![Some(false), None, None], false)]
3575 );
3576 assert_eq!(
3577 chain_vectors("chain_negative"),
3578 [(vec![Some(true), Some(false), None], false)]
3579 );
3580 assert_eq!(
3581 chain_vectors("chain_flag_fails"),
3582 [(vec![Some(true), Some(true), Some(false)], false)]
3583 );
3584 validate_frontend_report_request(&run.declaration, &run.request).unwrap();
3585 let archive = root.join("evidence.raw.gz");
3586 write_archive(run.archive_entries().unwrap(), &archive).unwrap();
3587 let report = analyze_coverage_archive(&ArchiveReportRequest {
3588 archive_path: archive,
3589 run_id: "rust-fixture".into(),
3590 generated_at: "2026-08-26T00:00:00.000Z".into(),
3591 integrity: None,
3592 test_exit_code: ExitCodeInput::Present(Some(0)),
3593 test_filter: None,
3594 })
3595 .unwrap();
3596 assert_eq!(report.view.tests.len(), 16);
3597 assert!(report.view.summary.lines.covered > 0);
3598 assert!(report.view.summary.decisions > 0);
3599
3600 let single = |kind: &str| {
3603 let mut found = report
3604 .view
3605 .branches
3606 .iter()
3607 .filter(|branch| branch.meta.kind == kind);
3608 let branch = found.next().unwrap_or_else(|| panic!("no {kind} branch"));
3609 assert!(found.next().is_none(), "more than one {kind} branch");
3610 branch
3611 };
3612 let tests_of = |branch: &crate::coverage_report::BranchResult, label: &str| {
3613 branch
3614 .alternatives
3615 .iter()
3616 .find(|alternative| alternative.label == label)
3617 .unwrap_or_else(|| panic!("{} has no alternative {label}", branch.meta.kind))
3618 .tests
3619 .clone()
3620 };
3621 let for_loop = single("for-loop");
3622 assert_eq!(
3623 tests_of(for_loop, "zero iterations"),
3624 ["src/lib.rs::tests::total_empty"]
3625 );
3626 assert_eq!(
3627 tests_of(for_loop, "entered"),
3628 ["src/lib.rs::tests::total_some"]
3629 );
3630 let while_loop = single("while-loop");
3631 assert_eq!(
3632 tests_of(while_loop, "zero iterations"),
3633 ["src/lib.rs::tests::first_even_empty"]
3634 );
3635 assert_eq!(
3636 tests_of(while_loop, "entered"),
3637 ["src/lib.rs::tests::first_even_found"]
3638 );
3639 let try_operator = report
3642 .view
3643 .branches
3644 .iter()
3645 .find(|branch| {
3646 branch.meta.kind == "try-operator" && branch.meta.source.contains("parse().ok()")
3647 })
3648 .expect("parse_twice's try operator");
3649 assert_eq!(
3650 tests_of(try_operator, "continued"),
3651 ["src/lib.rs::tests::parse_ok"]
3652 );
3653 assert_eq!(
3654 tests_of(try_operator, "early return"),
3655 ["src/lib.rs::tests::parse_bad"]
3656 );
3657 let mut logical = report
3658 .view
3659 .branches
3660 .iter()
3661 .filter(|branch| branch.meta.kind == "logical-and")
3662 .collect::<Vec<_>>();
3663 logical.sort_by_key(|branch| (branch.meta.line, branch.meta.column));
3664 assert_eq!(logical.len(), 3);
3666 assert_eq!(
3667 tests_of(logical[0], "short-circuited"),
3668 ["src/lib.rs::tests::false_path"]
3669 );
3670 assert_eq!(
3671 tests_of(logical[0], "right operand evaluated"),
3672 ["src/lib.rs::tests::true_path"]
3673 );
3674 assert_eq!(
3675 tests_of(logical[1], "short-circuited"),
3676 ["src/lib.rs::tests::chain_pattern_fails"]
3677 );
3678 assert_eq!(
3679 tests_of(logical[1], "right operand evaluated"),
3680 [
3681 "src/lib.rs::tests::chain_flag_fails",
3682 "src/lib.rs::tests::chain_negative",
3683 "src/lib.rs::tests::chain_taken",
3684 ]
3685 );
3686 assert_eq!(
3687 tests_of(logical[2], "short-circuited"),
3688 [
3689 "src/lib.rs::tests::chain_negative",
3690 "src/lib.rs::tests::chain_pattern_fails",
3691 ]
3692 );
3693 assert_eq!(
3694 tests_of(logical[2], "right operand evaluated"),
3695 [
3696 "src/lib.rs::tests::chain_flag_fails",
3697 "src/lib.rs::tests::chain_taken",
3698 ]
3699 );
3700 assert!(for_loop.covered && while_loop.covered && try_operator.covered);
3701 assert!(logical.iter().all(|branch| branch.covered));
3702
3703 let mut arms = report
3706 .view
3707 .branches
3708 .iter()
3709 .filter(|branch| branch.meta.kind == "match-arm")
3710 .collect::<Vec<_>>();
3711 arms.sort_by_key(|branch| branch.meta.line);
3712 assert_eq!(arms.len(), 3);
3713 let alternative = |arm: usize, label: &str| {
3714 arms[arm]
3715 .alternatives
3716 .iter()
3717 .find(|alternative| alternative.label == label)
3718 .unwrap_or_else(|| panic!("arm {arm} has no alternative {label}"))
3719 };
3720 assert_eq!(
3721 alternative(0, "selected").tests,
3722 ["src/lib.rs::tests::pick_zero"]
3723 );
3724 assert_eq!(
3725 alternative(0, "not selected").tests,
3726 ["src/lib.rs::tests::pick_many"]
3727 );
3728 assert!(!alternative(1, "selected").covered);
3729 assert_eq!(
3730 alternative(1, "not selected").tests,
3731 ["src/lib.rs::tests::pick_many"]
3732 );
3733 assert_eq!(arms[2].alternatives.len(), 1);
3734 assert_eq!(
3735 alternative(2, "selected").tests,
3736 ["src/lib.rs::tests::pick_many"]
3737 );
3738 assert!(arms[0].covered && !arms[1].covered && arms[2].covered);
3739
3740 fs::remove_dir_all(root).unwrap();
3741 }
3742}