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 workspace_started = Instant::now();
196 let workspace =
197 prepare_cargo_cached_workspace(&root, &lock).map_err(|error| error.to_string())?;
198 let cargo_runner_plan = resolve_cargo_runner_plan(&root, &workspace, &cargo_invocation)
199 .map_err(|error| error.to_string())?;
200 let workspace_preparation_ms = elapsed_ms(workspace_started);
201 let adapter_setup_ms = (elapsed_ms(adapter_started) - workspace_preparation_ms).max(0.0);
202
203 write_run_state(
204 &root,
205 &RunState {
206 id: request.run_id.clone(),
207 pid: std::process::id(),
208 root: root.display().to_string(),
209 workspace: workspace.display().to_string(),
210 started_at: request.started_at.clone(),
211 updated_at: request.started_at.clone(),
212 status: RunStateStatus::Preparing,
213 signal: None,
214 error: None,
215 },
216 )
217 .map_err(|error| error.to_string())?;
218 update_run_state(
219 &root,
220 &request.run_id,
221 RunStateStatus::Building,
222 &request.started_at,
223 None,
224 )
225 .map_err(|error| error.to_string())?;
226
227 let run = run_rust_compiler_frontend(
228 &RustCompilerRunRequest {
229 project_root: workspace.clone(),
230 command: request.command.clone(),
231 run_id: request.run_id.clone(),
232 generated_at: request.started_at.clone(),
233 wrapper_path: request.wrapper_path.clone(),
234 companion_candidates: request.companion_candidates.clone(),
235 require_public_capabilities: request.require_public_capabilities,
236 cargo_runner_plan,
237 watchdog_program: request.watchdog_program.clone(),
238 },
239 diagnostics,
240 )
241 .map_err(direct_frontend_error)?;
242
243 let publication_started = Instant::now();
244 let archive_path = root
245 .join(".supercov/work")
246 .join(&request.run_id)
247 .join("evidence.raw.gz");
248 let archive_entries = run.archive_entries().map_err(|error| error.to_string())?;
249 let raw = match request.publication_fault {
250 Some(RustCompilerPublicationFault::ArchiveEnospc) => write_archive_with_fault(
251 archive_entries,
252 &archive_path,
253 EvidenceArchiveWriteFault::NoSpaceAfterBytes(128),
254 ),
255 _ => write_archive(archive_entries, &archive_path),
256 }
257 .map_err(|error| error.to_string())?;
258 let report = analyze_coverage_archive(&ArchiveReportRequest {
259 archive_path: archive_path.clone(),
260 run_id: request.run_id.clone(),
261 generated_at: request.started_at.clone(),
262 integrity: None,
263 test_exit_code: ExitCodeInput::Present(Some(run.exit_code)),
264 })
265 .map_err(|error| format!("archived Rust compiler evidence is invalid: {error:?}"))?;
266 let evidence_publication_ms = elapsed_ms(publication_started);
267 let timings = RunTimings {
268 initialization_ms,
269 workspace_preparation_ms,
270 adapter_setup_ms,
271 instrumented_build_ms: (run.build_ms * 10.0).round() / 10.0,
272 test_command_ms: (run.execution_ms * 10.0).round() / 10.0,
273 evidence_publication_ms,
274 };
275 let metadata = RunMetadata {
276 id: request.run_id.clone(),
277 started_at: request.started_at.clone(),
278 duration_ms: elapsed_ms(total_started),
279 command: request.command.clone(),
280 test_exit_code: Some(run.exit_code),
281 integrity,
282 raw_evidence: RawEvidenceMetadata {
283 schema_version: raw.schema_version,
284 format: raw.format.into(),
285 file: raw.file.into(),
286 files: raw.files,
287 uncompressed_bytes: raw.uncompressed_bytes,
288 compressed_bytes: raw.compressed_bytes,
289 },
290 isolated_build: Some(true),
291 instrumented_build_cache: None,
292 timings: Some(timings),
293 merged: None,
294 parents: None,
295 };
296 let tests = run
297 .request
298 .raw_results
299 .iter()
300 .filter(|result| result.role == "test")
301 .map(|result| result.test_id.as_deref().unwrap_or(&result.test))
302 .collect::<std::collections::BTreeSet<_>>()
303 .len();
304 let libtests = run
305 .request
306 .raw_results
307 .iter()
308 .filter(|result| {
309 result.role == "test"
310 && matches!(
311 result.provenance.runner.as_str(),
312 "rust-libtest" | "rust-custom-harness" | "rust-nextest"
313 )
314 })
315 .map(|result| result.test_id.as_deref().unwrap_or(&result.test))
316 .collect::<std::collections::BTreeSet<_>>()
317 .len();
318 let doctests = run
319 .request
320 .raw_results
321 .iter()
322 .filter(|result| result.role == "test" && result.provenance.runner == "rustdoc")
323 .map(|result| result.test_id.as_deref().unwrap_or(&result.test))
324 .collect::<std::collections::BTreeSet<_>>()
325 .len();
326 if tests != libtests + doctests {
327 return Err("Rust compiler run contains an unclassified test runner".into());
328 }
329 let background_results = run
330 .request
331 .raw_results
332 .iter()
333 .filter(|result| result.role == "background")
334 .count();
335 let setup_results = run
336 .request
337 .raw_results
338 .iter()
339 .filter(|result| result.role == "setup")
340 .count();
341 let denominator = RustCompilerDenominatorCounts {
342 points: run.request.manifest.points.len(),
343 branches: run.request.manifest.branches.len(),
344 decisions: run.request.manifest.decisions.len(),
345 limitations: run.request.manifest.limitations.len(),
346 };
347 update_run_state(
348 &root,
349 &request.run_id,
350 RunStateStatus::Publishing,
351 &request.started_at,
352 None,
353 )
354 .map_err(|error| error.to_string())?;
355 if request.publication_fault == Some(RustCompilerPublicationFault::WaitBeforePublication) {
356 wait_before_publication(&root, &request.run_id)?;
357 }
358 let run_directory = match request.publication_fault {
359 Some(RustCompilerPublicationFault::FinalRename) => publish_run_with_fault(
360 &root,
361 &metadata,
362 &archive_path,
363 Some(RunPublicationFault::FinalRename),
364 ),
365 _ => publish_run(&root, &metadata, &archive_path),
366 }
367 .map_err(|error| error.to_string())?;
368 update_run_state(
369 &root,
370 &request.run_id,
371 if run.exit_code == 0 {
372 RunStateStatus::Complete
373 } else {
374 RunStateStatus::Failed
375 },
376 &request.started_at,
377 None,
378 )
379 .map_err(|error| error.to_string())?;
380 finalize_published_run(&root, &request.run_id).map_err(|error| error.to_string())?;
381 remove_cargo_workspace_run(&root, &request.run_id).map_err(|error| error.to_string())?;
382 Ok(DirectRustCompilerRunResult {
383 run_id: request.run_id.clone(),
384 run_directory,
385 exit_code: run.exit_code,
386 tests,
387 libtests,
388 doctests,
389 setup_results,
390 background_results,
391 artifacts: run.artifacts,
392 selection: run.selection,
393 denominator,
394 transport_health: run.transport_health,
395 summary: report.view.summary,
396 recovered_runs,
397 metadata,
398 })
399 })();
400 if let Err(error) = &result {
401 if let Some(signal) = &error.signal {
402 let _ = crate::lifecycle::interrupt_run_state(
403 &root,
404 &request.run_id,
405 &request.started_at,
406 signal,
407 );
408 } else {
409 let _ = update_run_state(
410 &root,
411 &request.run_id,
412 RunStateStatus::Failed,
413 &request.started_at,
414 Some(error.message.clone()),
415 );
416 }
417 let _ =
418 remove_stored_tree_deferred(&root, &root.join(".supercov/work").join(&request.run_id));
419 let _ = remove_cargo_workspace_run(&root, &request.run_id);
420 }
421 let release = lock.release().map_err(|error| error.to_string());
422 match (result, release) {
423 (Ok(result), Ok(())) => Ok(result),
424 (Err(error), _) => Err(error),
425 (Ok(_), Err(error)) => Err(error.into()),
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn unverified_test_runner_status_preserves_the_user_exit_code() {
435 let error = direct_frontend_error(
436 crate::rust_compiler_test_runner::RustCompilerTestError::UnverifiedExecution {
437 code: 104,
438 reason: "runner setup failed".into(),
439 },
440 );
441 assert_eq!(error.exit_code, 104);
442 assert!(error.signal.is_none());
443 assert!(error.message.contains("could not authenticate"));
444 }
445}