1use std::{
4 collections::BTreeSet,
5 fs,
6 io::Write,
7 path::{Path, PathBuf},
8 time::Instant,
9};
10
11use serde::{Deserialize, Serialize};
12
13use crate::{
14 evidence_archive::write_archive,
15 integrity::{ExplicitIntegrityInputs, FrontendIntegrityInputs, create_explicit_run_integrity},
16 lifecycle::{
17 ProjectLock, finalize_published_run, publish_run, recover_abandoned_runs,
18 remove_stored_tree_deferred,
19 },
20 run_store::{InstrumentedBuildCache, RawEvidenceMetadata, RunMetadata, RunTimings},
21 rust_build_cache::{
22 read_rust_build_cache, rust_build_cache_key, rust_target_directory, write_rust_build_cache,
23 },
24 rust_project::{PreparedRustProject, prepare_rust_project},
25 rust_test_runner::run_prepared_rust_tests,
26 workspace::{cached_workspace_path, prepare_cached_workspace, recover_cached_workspace},
27};
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase", deny_unknown_fields)]
31pub struct DirectRustRunRequest {
32 pub root: PathBuf,
33 pub command: Vec<String>,
34 pub run_id: String,
35 pub started_at: String,
36}
37
38#[derive(Debug, Clone, PartialEq)]
39pub struct DirectRustRunResult {
40 pub run_id: String,
41 pub run_directory: PathBuf,
42 pub exit_code: i32,
43 pub tests: usize,
44 pub artifacts: usize,
45 pub recovered_runs: Vec<String>,
46 pub metadata: RunMetadata,
47}
48
49fn elapsed_ms(started: Instant) -> f64 {
50 (started.elapsed().as_secs_f64() * 10_000.0).round() / 10.0
51}
52
53const ROOT_INPUT_EXCLUSIONS: &[&str] = &[
54 ".cache",
55 ".git",
56 ".supercov",
57 ".mcdc-pool",
58 "node_modules",
59 "target",
60 "build",
61 "dist",
62 ".next",
63 ".nuxt",
64 ".output",
65 "coverage",
66 "playwright-report",
67 "test-results",
68];
69
70fn collect_project_inputs(
71 root: &Path,
72 directory: &Path,
73 root_level: bool,
74 regular: &mut Vec<PathBuf>,
75 links: &mut Vec<String>,
76) -> Result<(), String> {
77 let mut entries = fs::read_dir(directory)
78 .map_err(|error| format!("{}: {error}", directory.display()))?
79 .collect::<Result<Vec<_>, _>>()
80 .map_err(|error| error.to_string())?;
81 entries.sort_by_key(fs::DirEntry::file_name);
82 for entry in entries {
83 let path = entry.path();
84 let name = entry
85 .file_name()
86 .into_string()
87 .map_err(|_| format!("Rust project contains a non-UTF-8 path: {}", path.display()))?;
88 if (root_level && ROOT_INPUT_EXCLUSIONS.contains(&name.as_str()))
89 || matches!(name.as_str(), ".supercov" | ".mcdc-pool")
90 {
91 continue;
92 }
93 let file_type = entry.file_type().map_err(|error| error.to_string())?;
94 if file_type.is_dir() {
95 collect_project_inputs(root, &path, false, regular, links)?;
96 } else if file_type.is_file() {
97 let relative = path
98 .strip_prefix(root)
99 .map_err(|_| format!("project input escaped root: {}", path.display()))?;
100 regular.push(relative.to_owned());
101 } else if file_type.is_symlink() {
102 let relative = path
103 .strip_prefix(root)
104 .map_err(|_| format!("project link escaped root: {}", path.display()))?;
105 let target = fs::read_link(&path).map_err(|error| error.to_string())?;
106 links.push(format!(
107 "{}=>{}",
108 relative.to_string_lossy().replace('\\', "/"),
109 target.to_string_lossy().replace('\\', "/")
110 ));
111 } else {
112 return Err(format!(
113 "unsupported Rust project input: {}",
114 path.display()
115 ));
116 }
117 }
118 Ok(())
119}
120
121pub(crate) fn collect_integrity_inputs(
122 root: &Path,
123 command: &[String],
124) -> Result<ExplicitIntegrityInputs, String> {
125 let mut files = Vec::new();
126 let mut links = Vec::new();
127 collect_project_inputs(root, root, true, &mut files, &mut links)?;
128 files.sort();
129 files.dedup();
130 links.sort();
131 links.dedup();
132 let source_files = files
133 .iter()
134 .filter(|path| path.extension().and_then(|value| value.to_str()) == Some("rs"))
135 .cloned()
136 .collect::<Vec<_>>();
137 let test_files = source_files.clone();
141 let dependency_files = files
142 .iter()
143 .filter(|path| {
144 path.file_name()
145 .and_then(|value| value.to_str())
146 .is_some_and(|name| matches!(name, "Cargo.toml" | "Cargo.lock"))
147 })
148 .cloned()
149 .collect::<Vec<_>>();
150 let source_set = source_files.iter().cloned().collect::<BTreeSet<_>>();
151 let dependency_set = dependency_files.iter().cloned().collect::<BTreeSet<_>>();
152 let configuration_files = files
153 .into_iter()
154 .filter(|path| !source_set.contains(path) && !dependency_set.contains(path))
155 .collect();
156 let mut execution_configuration = command.join("\0").into_bytes();
157 for link in links {
158 execution_configuration.push(0);
159 execution_configuration.extend_from_slice(link.as_bytes());
160 }
161 Ok(ExplicitIntegrityInputs {
162 source_files,
163 test_files,
164 dependency_files,
165 configuration_files,
166 execution_configuration,
167 })
168}
169
170pub fn current_rust_integrity(
171 root: &Path,
172 command: &[String],
173 source_roots: Option<&[String]>,
174) -> Result<crate::run_store::RunIntegrity, String> {
175 let root = fs::canonicalize(root).map_err(|error| error.to_string())?;
176 let mut inputs = collect_integrity_inputs(&root, command)?;
177 crate::source_discovery::fold_roots_into_configuration(
178 &mut inputs.execution_configuration,
179 source_roots,
180 );
181 create_explicit_run_integrity(&root, &inputs, &FrontendIntegrityInputs::embedded_rust())
182 .map_err(|error| error.to_string())
183}
184
185pub fn run_direct_rust(
186 request: &DirectRustRunRequest,
187 diagnostics: &mut dyn Write,
188) -> Result<DirectRustRunResult, String> {
189 if request.command.is_empty() {
190 return Err("test command must not be empty".into());
191 }
192 if !cfg!(any(
197 target_os = "macos",
198 target_os = "linux",
199 target_os = "windows"
200 )) {
201 return Err(
202 "Rust suites are not supported on this platform: the probe transport has no implementation here, so a run would measure nothing"
203 .into(),
204 );
205 }
206 let total_started = Instant::now();
207 let initialization_started = Instant::now();
208 let root = fs::canonicalize(&request.root)
209 .map_err(|error| format!("{}: {error}", request.root.display()))?;
210 let mut lock = ProjectLock::acquire(&root, &request.run_id, &request.started_at)
211 .map_err(|error| error.to_string())?;
212 let initialization_ms = elapsed_ms(initialization_started);
213 let result = (|| {
214 let recovered_runs = recover_abandoned_runs(&root, &request.started_at)
215 .map_err(|error| error.to_string())?;
216 if !recovered_runs.is_empty() {
217 writeln!(
218 diagnostics,
219 "[supercov] recovered abandoned run(s): {}",
220 recovered_runs.join(", ")
221 )
222 .map_err(|error| error.to_string())?;
223 }
224
225 let adapter_started = Instant::now();
226 let source_roots = crate::source_discovery::configured_source_roots(
227 &std::env::vars().collect::<std::collections::BTreeMap<_, _>>(),
228 );
229 let mut integrity_inputs = collect_integrity_inputs(&root, &request.command)?;
230 crate::source_discovery::fold_roots_into_configuration(
231 &mut integrity_inputs.execution_configuration,
232 source_roots.as_deref(),
233 );
234 let assertion_inputs =
235 crate::assertion_inputs::capture(&root, "rust", integrity_inputs.assertion_paths())?;
236 let integrity = create_explicit_run_integrity(
237 &root,
238 &integrity_inputs,
239 &FrontendIntegrityInputs::embedded_rust(),
240 )
241 .map_err(|error| error.to_string())?;
242 let build_cache_key = rust_build_cache_key(&integrity, &request.command)
243 .map_err(|error| error.to_string())?;
244
245 let workspace_started = Instant::now();
246 recover_cached_workspace(&root, &lock).map_err(|error| error.to_string())?;
247 let workspace = cached_workspace_path(&root).map_err(|error| error.to_string())?;
248 let target_directory = rust_target_directory(&root);
249 let cache_started = Instant::now();
250 let cached = read_rust_build_cache(&workspace, &target_directory, &build_cache_key);
251 let cache_read_ms = elapsed_ms(cache_started);
252 let mut copy_ms = 0.0;
253 let reused_build = cached.is_some();
254 let mut project = if let Some(cached) = cached {
255 writeln!(
256 diagnostics,
257 "[supercov] detected Rust; reusing authenticated instrumented workspace {}",
258 workspace.display()
259 )
260 .map_err(|error| error.to_string())?;
261 PreparedRustProject {
262 workspace_root: workspace.clone(),
263 target_directory: target_directory.clone(),
264 source_files: cached.source_files,
265 crate_roots: Vec::new(),
266 runtime_module: String::new(),
267 manifest: cached.manifest,
268 preparation: Default::default(),
269 }
270 } else {
271 let copy_started = Instant::now();
272 let workspace =
273 prepare_cached_workspace(&root, &lock, &[]).map_err(|error| error.to_string())?;
274 copy_ms = elapsed_ms(copy_started);
275 writeln!(
276 diagnostics,
277 "[supercov] detected Rust; instrumenting isolated Cargo workspace {}",
278 workspace.display()
279 )
280 .map_err(|error| error.to_string())?;
281 let ambient = std::env::vars().collect::<std::collections::BTreeMap<_, _>>();
282 let roots = crate::source_discovery::ExplicitSourceRoots::from_environment_in(
283 &root, &workspace, &ambient,
284 )
285 .map_err(|error| error.to_string())?;
286 prepare_rust_project(&workspace, roots.as_ref()).map_err(|error| error.to_string())?
287 };
288 project.target_directory = target_directory;
289 fs::create_dir_all(&project.target_directory).map_err(|error| error.to_string())?;
290 let workspace_preparation_ms = elapsed_ms(workspace_started);
291 let adapter_setup_ms = (elapsed_ms(adapter_started) - workspace_preparation_ms).max(0.0);
292 if std::env::var("SUPERCOV_PHASE_TIMING").as_deref() == Ok("1") {
293 let preparation = &project.preparation;
294 writeln!(
295 diagnostics,
296 "[supercov] workspace timings cache-check={cache_read_ms:.1}ms copy={copy_ms:.1}ms metadata={:.1}ms discovery={:.1}ms instrument={:.1}ms runtime={:.1}ms",
297 preparation.metadata_ms,
298 preparation.discovery_ms,
299 preparation.instrument_ms,
300 preparation.runtime_ms,
301 )
302 .map_err(|error| error.to_string())?;
303 }
304
305 let nextest = request
306 .command
307 .windows(2)
308 .any(|pair| pair == ["nextest", "run"]);
309 writeln!(
310 diagnostics,
311 "{}",
312 if nextest {
313 "[supercov] running cargo nextest with Supercov as its target runner, each attempt in its own process"
314 } else {
315 "[supercov] building once and running each libtest case and doctest in its own process"
316 }
317 )
318 .map_err(|error| error.to_string())?;
319 let run = run_prepared_rust_tests(
320 &project,
321 &request.command,
322 &request.run_id,
323 &request.started_at,
324 diagnostics,
325 )
326 .map_err(|error| error.to_string())?;
327 write_rust_build_cache(
328 &root,
329 &workspace,
330 &build_cache_key,
331 &request.started_at,
332 &project.source_files,
333 &run.request.manifest,
336 &run.artifact_files,
337 )?;
338
339 let publication_started = Instant::now();
340 let archive_path = root
341 .join(".supercov/work")
342 .join(&request.run_id)
343 .join("evidence.raw.gz");
344 let raw = write_archive(
345 crate::assertion_inputs::append(
346 run.archive_entries().map_err(|error| error.to_string())?,
347 &assertion_inputs,
348 )?,
349 &archive_path,
350 )
351 .map_err(|error| error.to_string())?;
352 remove_stored_tree_deferred(
353 &root,
354 &workspace
355 .join(".supercov/rust-evidence")
356 .join(&request.run_id),
357 )
358 .map_err(|error| error.to_string())?;
359 let evidence_publication_ms = elapsed_ms(publication_started);
360 let timings = RunTimings {
361 initialization_ms,
362 workspace_preparation_ms,
363 adapter_setup_ms,
364 instrumented_build_ms: (run.build_ms * 10.0).round() / 10.0,
365 test_command_ms: (run.execution_ms * 10.0).round() / 10.0,
366 evidence_publication_ms,
367 };
368 let metadata = RunMetadata {
369 id: request.run_id.clone(),
370 started_at: request.started_at.clone(),
371 duration_ms: elapsed_ms(total_started),
372 command: request.command.clone(),
373 test_exit_code: Some(run.exit_code),
374 integrity,
375 raw_evidence: RawEvidenceMetadata {
376 schema_version: raw.schema_version,
377 format: raw.format.into(),
378 file: raw.file.into(),
379 files: raw.files,
380 uncompressed_bytes: raw.uncompressed_bytes,
381 compressed_bytes: raw.compressed_bytes,
382 },
383 isolated_build: Some(true),
384 instrumented_build_cache: Some(InstrumentedBuildCache {
385 key: build_cache_key,
386 reused: reused_build,
387 }),
388 timings: Some(timings),
389 merged: None,
390 parents: None,
391 source_roots: source_roots.clone(),
392 };
393 let run_directory =
394 publish_run(&root, &metadata, &archive_path).map_err(|error| error.to_string())?;
395 finalize_published_run(&root, &request.run_id).map_err(|error| error.to_string())?;
396 Ok(DirectRustRunResult {
397 run_id: request.run_id.clone(),
398 run_directory,
399 exit_code: run.exit_code,
400 tests: run
402 .request
403 .raw_results
404 .iter()
405 .map(|result| result.test.as_str())
406 .collect::<std::collections::BTreeSet<_>>()
407 .len(),
408 artifacts: run.artifacts,
409 recovered_runs,
410 metadata,
411 })
412 })();
413 if result.is_err() {
414 let _ =
415 remove_stored_tree_deferred(&root, &root.join(".supercov/work").join(&request.run_id));
416 if let Ok(workspace) = cached_workspace_path(&root) {
417 let _ = remove_stored_tree_deferred(
418 &root,
419 &workspace
420 .join(".supercov/rust-evidence")
421 .join(&request.run_id),
422 );
423 }
424 }
425 let release = lock.release().map_err(|error| error.to_string());
426 match (result, release) {
427 (Ok(result), Ok(())) => Ok(result),
428 (Err(error), _) => Err(error),
429 (Ok(_), Err(error)) => Err(error),
430 }
431}