1use std::{
4 fs,
5 io::Write,
6 path::PathBuf,
7 thread,
8 time::{Duration, Instant},
9};
10
11use serde::{Deserialize, Serialize};
12
13use crate::{
14 coverage_report::{ArchiveReportRequest, ExitCodeInput, analyze_coverage_archive},
15 evidence_archive::{EvidenceArchiveWriteFault, write_archive, write_archive_with_fault},
16 lifecycle::{
17 ProjectLock, RunPublicationFault, RunState, RunStateStatus, atomic_write,
18 finalize_published_run, publish_run, publish_run_with_fault, recover_abandoned_runs,
19 remove_stored_tree_deferred, update_run_state, write_run_state,
20 },
21 run_store::{RawEvidenceMetadata, RunMetadata, RunTimings},
22 rust_cargo_configuration::resolve_cargo_runner_plan,
23 rust_compiler_test_runner::{RustCompilerRunRequest, run_rust_compiler_frontend},
24 rust_run::current_rust_integrity,
25 rust_test_runner::cargo_invocation,
26 workspace::{prepare_cargo_cached_workspace, remove_cargo_workspace_run},
27};
28
29#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
30#[serde(rename_all = "camelCase", deny_unknown_fields)]
31pub struct DirectRustCompilerRunRequest {
32 pub root: PathBuf,
33 pub command: Vec<String>,
34 pub run_id: String,
35 pub started_at: String,
36 pub wrapper_path: PathBuf,
37 pub companion_candidates: Vec<PathBuf>,
38 pub require_public_capabilities: bool,
39 #[serde(skip)]
40 pub watchdog_program: Option<PathBuf>,
41 #[serde(skip)]
42 pub publication_fault: Option<RustCompilerPublicationFault>,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum RustCompilerPublicationFault {
47 ArchiveEnospc,
48 FinalRename,
49 WaitBeforePublication,
50}
51
52#[derive(Debug, Clone, PartialEq, Serialize)]
53#[serde(rename_all = "camelCase")]
54pub struct DirectRustCompilerRunResult {
55 pub run_id: String,
56 pub run_directory: PathBuf,
57 pub exit_code: i32,
58 pub tests: usize,
59 pub libtests: usize,
60 pub doctests: usize,
61 pub setup_results: usize,
62 pub background_results: usize,
63 pub artifacts: usize,
64 pub selection: crate::rust_compiler_selection::SelectedRustCompilerCompanion,
65 pub denominator: RustCompilerDenominatorCounts,
66 pub transport_health: Vec<crate::rust_compiler_test_runner::RustCompilerTransportHealthRecord>,
67 pub summary: crate::coverage_analysis::CoverageSummary,
68 pub recovered_runs: Vec<String>,
69 pub metadata: RunMetadata,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
73#[serde(rename_all = "camelCase")]
74pub struct RustCompilerDenominatorCounts {
75 pub points: usize,
76 pub branches: usize,
77 pub decisions: usize,
78 pub limitations: usize,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct DirectRustCompilerRunError {
83 pub message: String,
84 pub exit_code: i32,
85 pub signal: Option<String>,
86}
87
88impl std::fmt::Display for DirectRustCompilerRunError {
89 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 formatter.write_str(&self.message)
91 }
92}
93
94impl std::error::Error for DirectRustCompilerRunError {}
95
96impl From<String> for DirectRustCompilerRunError {
97 fn from(message: String) -> Self {
98 Self {
99 message,
100 exit_code: 2,
101 signal: None,
102 }
103 }
104}
105
106impl From<&str> for DirectRustCompilerRunError {
107 fn from(message: &str) -> Self {
108 message.to_owned().into()
109 }
110}
111
112fn elapsed_ms(started: Instant) -> f64 {
113 (started.elapsed().as_secs_f64() * 10_000.0).round() / 10.0
114}
115
116fn direct_frontend_error(
117 error: crate::rust_compiler_test_runner::RustCompilerTestError,
118) -> DirectRustCompilerRunError {
119 match error {
120 crate::rust_compiler_test_runner::RustCompilerTestError::Interrupted { code, signal } => {
121 DirectRustCompilerRunError {
122 message: format!("Rust compiler run was interrupted by {signal}"),
123 exit_code: code,
124 signal: Some(signal),
125 }
126 }
127 crate::rust_compiler_test_runner::RustCompilerTestError::UnverifiedExecution {
128 code,
129 reason,
130 } => DirectRustCompilerRunError {
131 message: format!(
132 "Rust test command exited {code}, but Supercov could not authenticate complete coverage evidence: {reason}"
133 ),
134 exit_code: code,
135 signal: None,
136 },
137 error => error.to_string().into(),
138 }
139}
140
141fn wait_before_publication(root: &std::path::Path, run_id: &str) -> Result<(), String> {
142 let work = root.join(".supercov/work").join(run_id);
143 let ready = work.join("spike-publication-ready");
144 let release = work.join("spike-publication-release");
145 atomic_write(root, &ready, b"ready\n").map_err(|error| error.to_string())?;
146 let started = Instant::now();
147 loop {
148 match fs::symlink_metadata(&release) {
149 Ok(metadata) if metadata.file_type().is_file() => return Ok(()),
150 Ok(_) => {
151 return Err(format!(
152 "invalid private publication release marker: {}",
153 release.display()
154 ));
155 }
156 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
157 Err(error) => return Err(format!("{}: {error}", release.display())),
158 }
159 if started.elapsed() >= Duration::from_secs(120) {
160 return Err("timed out at private compiler publication gate".into());
161 }
162 thread::sleep(Duration::from_millis(25));
163 }
164}
165
166pub fn run_direct_rust_compiler(
167 request: &DirectRustCompilerRunRequest,
168 diagnostics: &mut dyn Write,
169) -> Result<DirectRustCompilerRunResult, DirectRustCompilerRunError> {
170 if request.command.is_empty() || request.companion_candidates.is_empty() {
171 return Err("test command and exact compiler companion candidates are required".into());
172 }
173 let total_started = Instant::now();
174 let initialization_started = Instant::now();
175 let root = fs::canonicalize(&request.root)
176 .map_err(|error| format!("{}: {error}", request.root.display()))?;
177 let cargo_invocation =
178 cargo_invocation(&root, &request.command).map_err(|error| error.to_string())?;
179 let mut lock = ProjectLock::acquire(&root, &request.run_id, &request.started_at)
180 .map_err(|error| error.to_string())?;
181 let initialization_ms = elapsed_ms(initialization_started);
182 let result = (|| -> Result<DirectRustCompilerRunResult, DirectRustCompilerRunError> {
183 let recovered_runs = recover_abandoned_runs(&root, &request.started_at)
184 .map_err(|error| error.to_string())?;
185 if !recovered_runs.is_empty() {
186 writeln!(
187 diagnostics,
188 "[supercov] recovered abandoned run(s): {}",
189 recovered_runs.join(", ")
190 )
191 .map_err(|error| error.to_string())?;
192 }
193 let adapter_started = Instant::now();
194 let integrity = current_rust_integrity(&root, &request.command)?;
195 let assertion_inputs = crate::assertion_inputs::capture(
196 &root,
197 "rust",
198 crate::rust_run::collect_integrity_inputs(&root, &request.command)?.assertion_paths(),
199 )?;
200 let workspace_started = Instant::now();
201 let workspace =
202 prepare_cargo_cached_workspace(&root, &lock).map_err(|error| error.to_string())?;
203 let cargo_runner_plan = resolve_cargo_runner_plan(&root, &workspace, &cargo_invocation)
204 .map_err(|error| error.to_string())?;
205 let workspace_preparation_ms = elapsed_ms(workspace_started);
206 let adapter_setup_ms = (elapsed_ms(adapter_started) - workspace_preparation_ms).max(0.0);
207
208 write_run_state(
209 &root,
210 &RunState {
211 id: request.run_id.clone(),
212 pid: std::process::id(),
213 root: root.display().to_string(),
214 workspace: workspace.display().to_string(),
215 started_at: request.started_at.clone(),
216 updated_at: request.started_at.clone(),
217 status: RunStateStatus::Preparing,
218 signal: None,
219 error: None,
220 },
221 )
222 .map_err(|error| error.to_string())?;
223 update_run_state(
224 &root,
225 &request.run_id,
226 RunStateStatus::Building,
227 &request.started_at,
228 None,
229 )
230 .map_err(|error| error.to_string())?;
231
232 let run = run_rust_compiler_frontend(
233 &RustCompilerRunRequest {
234 project_root: workspace.clone(),
235 command: request.command.clone(),
236 run_id: request.run_id.clone(),
237 generated_at: request.started_at.clone(),
238 wrapper_path: request.wrapper_path.clone(),
239 companion_candidates: request.companion_candidates.clone(),
240 require_public_capabilities: request.require_public_capabilities,
241 cargo_runner_plan,
242 watchdog_program: request.watchdog_program.clone(),
243 },
244 diagnostics,
245 )
246 .map_err(direct_frontend_error)?;
247
248 let publication_started = Instant::now();
249 let archive_path = root
250 .join(".supercov/work")
251 .join(&request.run_id)
252 .join("evidence.raw.gz");
253 let archive_entries = crate::assertion_inputs::append(
254 run.archive_entries().map_err(|error| error.to_string())?,
255 &assertion_inputs,
256 )?;
257 let raw = match request.publication_fault {
258 Some(RustCompilerPublicationFault::ArchiveEnospc) => write_archive_with_fault(
259 archive_entries,
260 &archive_path,
261 EvidenceArchiveWriteFault::NoSpaceAfterBytes(128),
262 ),
263 _ => write_archive(archive_entries, &archive_path),
264 }
265 .map_err(|error| error.to_string())?;
266 let report = analyze_coverage_archive(&ArchiveReportRequest {
267 archive_path: archive_path.clone(),
268 run_id: request.run_id.clone(),
269 generated_at: request.started_at.clone(),
270 integrity: None,
271 test_exit_code: ExitCodeInput::Present(Some(run.exit_code)),
272 })
273 .map_err(|error| format!("archived Rust compiler evidence is invalid: {error:?}"))?;
274 let evidence_publication_ms = elapsed_ms(publication_started);
275 let timings = RunTimings {
276 initialization_ms,
277 workspace_preparation_ms,
278 adapter_setup_ms,
279 instrumented_build_ms: (run.build_ms * 10.0).round() / 10.0,
280 test_command_ms: (run.execution_ms * 10.0).round() / 10.0,
281 evidence_publication_ms,
282 };
283 let metadata = RunMetadata {
284 id: request.run_id.clone(),
285 started_at: request.started_at.clone(),
286 duration_ms: elapsed_ms(total_started),
287 command: request.command.clone(),
288 test_exit_code: Some(run.exit_code),
289 integrity,
290 raw_evidence: RawEvidenceMetadata {
291 schema_version: raw.schema_version,
292 format: raw.format.into(),
293 file: raw.file.into(),
294 files: raw.files,
295 uncompressed_bytes: raw.uncompressed_bytes,
296 compressed_bytes: raw.compressed_bytes,
297 },
298 isolated_build: Some(true),
299 instrumented_build_cache: None,
300 timings: Some(timings),
301 merged: None,
302 parents: None,
303 };
304 let tests = run
305 .request
306 .raw_results
307 .iter()
308 .filter(|result| result.role == "test")
309 .map(|result| result.test_id.as_deref().unwrap_or(&result.test))
310 .collect::<std::collections::BTreeSet<_>>()
311 .len();
312 let libtests = run
313 .request
314 .raw_results
315 .iter()
316 .filter(|result| {
317 result.role == "test"
318 && matches!(
319 result.provenance.runner.as_str(),
320 "rust-libtest" | "rust-custom-harness" | "rust-nextest"
321 )
322 })
323 .map(|result| result.test_id.as_deref().unwrap_or(&result.test))
324 .collect::<std::collections::BTreeSet<_>>()
325 .len();
326 let doctests = run
327 .request
328 .raw_results
329 .iter()
330 .filter(|result| result.role == "test" && result.provenance.runner == "rustdoc")
331 .map(|result| result.test_id.as_deref().unwrap_or(&result.test))
332 .collect::<std::collections::BTreeSet<_>>()
333 .len();
334 if tests != libtests + doctests {
335 return Err("Rust compiler run contains an unclassified test runner".into());
336 }
337 let background_results = run
338 .request
339 .raw_results
340 .iter()
341 .filter(|result| result.role == "background")
342 .count();
343 let setup_results = run
344 .request
345 .raw_results
346 .iter()
347 .filter(|result| result.role == "setup")
348 .count();
349 let denominator = RustCompilerDenominatorCounts {
350 points: run.request.manifest.points.len(),
351 branches: run.request.manifest.branches.len(),
352 decisions: run.request.manifest.decisions.len(),
353 limitations: run.request.manifest.limitations.len(),
354 };
355 update_run_state(
356 &root,
357 &request.run_id,
358 RunStateStatus::Publishing,
359 &request.started_at,
360 None,
361 )
362 .map_err(|error| error.to_string())?;
363 if request.publication_fault == Some(RustCompilerPublicationFault::WaitBeforePublication) {
364 wait_before_publication(&root, &request.run_id)?;
365 }
366 let run_directory = match request.publication_fault {
367 Some(RustCompilerPublicationFault::FinalRename) => publish_run_with_fault(
368 &root,
369 &metadata,
370 &archive_path,
371 Some(RunPublicationFault::FinalRename),
372 ),
373 _ => publish_run(&root, &metadata, &archive_path),
374 }
375 .map_err(|error| error.to_string())?;
376 update_run_state(
377 &root,
378 &request.run_id,
379 if run.exit_code == 0 {
380 RunStateStatus::Complete
381 } else {
382 RunStateStatus::Failed
383 },
384 &request.started_at,
385 None,
386 )
387 .map_err(|error| error.to_string())?;
388 finalize_published_run(&root, &request.run_id).map_err(|error| error.to_string())?;
389 remove_cargo_workspace_run(&root, &request.run_id).map_err(|error| error.to_string())?;
390 Ok(DirectRustCompilerRunResult {
391 run_id: request.run_id.clone(),
392 run_directory,
393 exit_code: run.exit_code,
394 tests,
395 libtests,
396 doctests,
397 setup_results,
398 background_results,
399 artifacts: run.artifacts,
400 selection: run.selection,
401 denominator,
402 transport_health: run.transport_health,
403 summary: report.view.summary,
404 recovered_runs,
405 metadata,
406 })
407 })();
408 if let Err(error) = &result {
409 if let Some(signal) = &error.signal {
410 let _ = crate::lifecycle::interrupt_run_state(
411 &root,
412 &request.run_id,
413 &request.started_at,
414 signal,
415 );
416 } else {
417 let _ = update_run_state(
418 &root,
419 &request.run_id,
420 RunStateStatus::Failed,
421 &request.started_at,
422 Some(error.message.clone()),
423 );
424 }
425 let _ =
426 remove_stored_tree_deferred(&root, &root.join(".supercov/work").join(&request.run_id));
427 let _ = remove_cargo_workspace_run(&root, &request.run_id);
428 }
429 let release = lock.release().map_err(|error| error.to_string());
430 match (result, release) {
431 (Ok(result), Ok(())) => Ok(result),
432 (Err(error), _) => Err(error),
433 (Ok(_), Err(error)) => Err(error.into()),
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
442 fn unverified_test_runner_status_preserves_the_user_exit_code() {
443 let error = direct_frontend_error(
444 crate::rust_compiler_test_runner::RustCompilerTestError::UnverifiedExecution {
445 code: 104,
446 reason: "runner setup failed".into(),
447 },
448 );
449 assert_eq!(error.exit_code, 104);
450 assert!(error.signal.is_none());
451 assert!(error.message.contains("could not authenticate"));
452 }
453}