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 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}