Skip to main content

supercov_engine/
rust_compiler_run.rs

1//! Private transactional lifecycle for the compiler-owned Rust frontend.
2
3use 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}