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) -> Result<crate::run_store::RunIntegrity, String> {
174 let root = fs::canonicalize(root).map_err(|error| error.to_string())?;
175 create_explicit_run_integrity(
176 &root,
177 &collect_integrity_inputs(&root, command)?,
178 &FrontendIntegrityInputs::embedded_rust(),
179 )
180 .map_err(|error| error.to_string())
181}
182
183pub fn run_direct_rust(
184 request: &DirectRustRunRequest,
185 diagnostics: &mut dyn Write,
186) -> Result<DirectRustRunResult, String> {
187 if request.command.is_empty() {
188 return Err("test command must not be empty".into());
189 }
190 if !cfg!(any(
195 target_os = "macos",
196 target_os = "linux",
197 target_os = "windows"
198 )) {
199 return Err(
200 "Rust suites are not supported on this platform: the probe transport has no implementation here, so a run would measure nothing"
201 .into(),
202 );
203 }
204 let total_started = Instant::now();
205 let initialization_started = Instant::now();
206 let root = fs::canonicalize(&request.root)
207 .map_err(|error| format!("{}: {error}", request.root.display()))?;
208 let mut lock = ProjectLock::acquire(&root, &request.run_id, &request.started_at)
209 .map_err(|error| error.to_string())?;
210 let initialization_ms = elapsed_ms(initialization_started);
211 let result = (|| {
212 let recovered_runs = recover_abandoned_runs(&root, &request.started_at)
213 .map_err(|error| error.to_string())?;
214 if !recovered_runs.is_empty() {
215 writeln!(
216 diagnostics,
217 "[supercov] recovered abandoned run(s): {}",
218 recovered_runs.join(", ")
219 )
220 .map_err(|error| error.to_string())?;
221 }
222
223 let adapter_started = Instant::now();
224 let integrity_inputs = collect_integrity_inputs(&root, &request.command)?;
225 let assertion_inputs =
226 crate::assertion_inputs::capture(&root, "rust", integrity_inputs.assertion_paths())?;
227 let integrity = create_explicit_run_integrity(
228 &root,
229 &integrity_inputs,
230 &FrontendIntegrityInputs::embedded_rust(),
231 )
232 .map_err(|error| error.to_string())?;
233 let build_cache_key = rust_build_cache_key(&integrity, &request.command)
234 .map_err(|error| error.to_string())?;
235
236 let workspace_started = Instant::now();
237 recover_cached_workspace(&root, &lock).map_err(|error| error.to_string())?;
238 let workspace = cached_workspace_path(&root).map_err(|error| error.to_string())?;
239 let target_directory = rust_target_directory(&root);
240 let cache_started = Instant::now();
241 let cached = read_rust_build_cache(&workspace, &target_directory, &build_cache_key);
242 let cache_read_ms = elapsed_ms(cache_started);
243 let mut copy_ms = 0.0;
244 let reused_build = cached.is_some();
245 let mut project = if let Some(cached) = cached {
246 writeln!(
247 diagnostics,
248 "[supercov] detected Rust; reusing authenticated instrumented workspace {}",
249 workspace.display()
250 )
251 .map_err(|error| error.to_string())?;
252 PreparedRustProject {
253 workspace_root: workspace.clone(),
254 target_directory: target_directory.clone(),
255 source_files: cached.source_files,
256 crate_roots: Vec::new(),
257 runtime_module: String::new(),
258 manifest: cached.manifest,
259 preparation: Default::default(),
260 }
261 } else {
262 let copy_started = Instant::now();
263 let workspace =
264 prepare_cached_workspace(&root, &lock, &[]).map_err(|error| error.to_string())?;
265 copy_ms = elapsed_ms(copy_started);
266 writeln!(
267 diagnostics,
268 "[supercov] detected Rust; instrumenting isolated Cargo workspace {}",
269 workspace.display()
270 )
271 .map_err(|error| error.to_string())?;
272 prepare_rust_project(&workspace).map_err(|error| error.to_string())?
273 };
274 project.target_directory = target_directory;
275 fs::create_dir_all(&project.target_directory).map_err(|error| error.to_string())?;
276 let workspace_preparation_ms = elapsed_ms(workspace_started);
277 let adapter_setup_ms = (elapsed_ms(adapter_started) - workspace_preparation_ms).max(0.0);
278 if std::env::var("SUPERCOV_PHASE_TIMING").as_deref() == Ok("1") {
279 let preparation = &project.preparation;
280 writeln!(
281 diagnostics,
282 "[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",
283 preparation.metadata_ms,
284 preparation.discovery_ms,
285 preparation.instrument_ms,
286 preparation.runtime_ms,
287 )
288 .map_err(|error| error.to_string())?;
289 }
290
291 let nextest = request
292 .command
293 .windows(2)
294 .any(|pair| pair == ["nextest", "run"]);
295 writeln!(
296 diagnostics,
297 "{}",
298 if nextest {
299 "[supercov] running cargo nextest with Supercov as its target runner, each attempt in its own process"
300 } else {
301 "[supercov] building once and running each libtest case and doctest in its own process"
302 }
303 )
304 .map_err(|error| error.to_string())?;
305 let run = run_prepared_rust_tests(
306 &project,
307 &request.command,
308 &request.run_id,
309 &request.started_at,
310 diagnostics,
311 )
312 .map_err(|error| error.to_string())?;
313 write_rust_build_cache(
314 &root,
315 &workspace,
316 &build_cache_key,
317 &request.started_at,
318 &project.source_files,
319 &run.request.manifest,
322 &run.artifact_files,
323 )?;
324
325 let publication_started = Instant::now();
326 let archive_path = root
327 .join(".supercov/work")
328 .join(&request.run_id)
329 .join("evidence.raw.gz");
330 let raw = write_archive(
331 crate::assertion_inputs::append(
332 run.archive_entries().map_err(|error| error.to_string())?,
333 &assertion_inputs,
334 )?,
335 &archive_path,
336 )
337 .map_err(|error| error.to_string())?;
338 remove_stored_tree_deferred(
339 &root,
340 &workspace
341 .join(".supercov/rust-evidence")
342 .join(&request.run_id),
343 )
344 .map_err(|error| error.to_string())?;
345 let evidence_publication_ms = elapsed_ms(publication_started);
346 let timings = RunTimings {
347 initialization_ms,
348 workspace_preparation_ms,
349 adapter_setup_ms,
350 instrumented_build_ms: (run.build_ms * 10.0).round() / 10.0,
351 test_command_ms: (run.execution_ms * 10.0).round() / 10.0,
352 evidence_publication_ms,
353 };
354 let metadata = RunMetadata {
355 id: request.run_id.clone(),
356 started_at: request.started_at.clone(),
357 duration_ms: elapsed_ms(total_started),
358 command: request.command.clone(),
359 test_exit_code: Some(run.exit_code),
360 integrity,
361 raw_evidence: RawEvidenceMetadata {
362 schema_version: raw.schema_version,
363 format: raw.format.into(),
364 file: raw.file.into(),
365 files: raw.files,
366 uncompressed_bytes: raw.uncompressed_bytes,
367 compressed_bytes: raw.compressed_bytes,
368 },
369 isolated_build: Some(true),
370 instrumented_build_cache: Some(InstrumentedBuildCache {
371 key: build_cache_key,
372 reused: reused_build,
373 }),
374 timings: Some(timings),
375 merged: None,
376 parents: None,
377 };
378 let run_directory =
379 publish_run(&root, &metadata, &archive_path).map_err(|error| error.to_string())?;
380 finalize_published_run(&root, &request.run_id).map_err(|error| error.to_string())?;
381 Ok(DirectRustRunResult {
382 run_id: request.run_id.clone(),
383 run_directory,
384 exit_code: run.exit_code,
385 tests: run
387 .request
388 .raw_results
389 .iter()
390 .map(|result| result.test.as_str())
391 .collect::<std::collections::BTreeSet<_>>()
392 .len(),
393 artifacts: run.artifacts,
394 recovered_runs,
395 metadata,
396 })
397 })();
398 if result.is_err() {
399 let _ =
400 remove_stored_tree_deferred(&root, &root.join(".supercov/work").join(&request.run_id));
401 if let Ok(workspace) = cached_workspace_path(&root) {
402 let _ = remove_stored_tree_deferred(
403 &root,
404 &workspace
405 .join(".supercov/rust-evidence")
406 .join(&request.run_id),
407 );
408 }
409 }
410 let release = lock.release().map_err(|error| error.to_string());
411 match (result, release) {
412 (Ok(result), Ok(())) => Ok(result),
413 (Err(error), _) => Err(error),
414 (Ok(_), Err(error)) => Err(error),
415 }
416}