Skip to main content

fallow_api/mutation/
last_report.rs

1//! The saved report of the last mutation run, which `fallow mutation
2//! inspect` reads when no `--report` is given.
3//!
4//! Each run saves its full JSON report to
5//! `<cache dir>/mutation/last-report.json`. The cache directory has a
6//! `.gitignore` with `*`, so the saved report never makes the worktree dirty.
7//! `fallow mutation inspect` reads this file when the caller names no report.
8
9use std::path::{Path, PathBuf};
10
11use crate::analysis_context::resolve_programmatic_analysis_context;
12use crate::{AnalysisOptions, ProgrammaticError};
13
14/// The file name of the saved report below `<cache dir>/mutation`.
15const LAST_REPORT_FILE: &str = "last-report.json";
16
17/// Writes `*` to `<cache>/.gitignore` when the cache has no ignore file, so
18/// that the run directory and the saved report never show in `git status`.
19pub fn ignore_cache_dir(cache_dir: &Path) -> std::io::Result<()> {
20    std::fs::create_dir_all(cache_dir)?;
21    let ignore = cache_dir.join(".gitignore");
22    if !ignore.exists() {
23        std::fs::write(ignore, "*\n")?;
24    }
25    Ok(())
26}
27
28fn cache_error(message: String) -> ProgrammaticError {
29    ProgrammaticError::new(message, 2).with_code("FALLOW_MUTATION_CACHE_WRITE_FAILED")
30}
31
32/// The cache directory of the project config: the directory that a run uses.
33fn cache_dir(analysis: &AnalysisOptions) -> Result<PathBuf, ProgrammaticError> {
34    let resolved = resolve_programmatic_analysis_context(analysis)?;
35    let project = fallow_engine::project_config::config_for_project_with_load_options(
36        resolved.root(),
37        resolved.config_path().as_deref(),
38        fallow_config::ConfigLoadOptions {
39            allow_remote_extends: resolved.allow_remote_extends(),
40        },
41    )
42    .map_err(|error| {
43        ProgrammaticError::new(format!("failed to load config: {error}"), 2)
44            .with_code("FALLOW_CONFIG_LOAD_FAILED")
45            .with_context("analysis.configPath")
46    })?;
47    Ok(project.config.cache_dir)
48}
49
50/// The path of the saved report of the last run of the project.
51///
52/// # Errors
53///
54/// Returns an error when the root or the config does not resolve.
55pub fn last_mutation_report_path(analysis: &AnalysisOptions) -> Result<PathBuf, ProgrammaticError> {
56    Ok(cache_dir(analysis)?.join("mutation").join(LAST_REPORT_FILE))
57}
58
59/// Removes the outcome journal of the project when `report` is complete,
60/// and returns true when it removed a journal. Call it after the report is
61/// saved: the saved report then holds each result. A partial or failed run
62/// keeps the journal, so `--resume` can continue it. A run that started no
63/// test runner (`runner` is absent) did not write or check the journal, so
64/// the journal can belong to an interrupted run of another scope, and it
65/// stays.
66///
67/// # Errors
68///
69/// Returns an error when the config does not resolve or the journal cannot
70/// be removed.
71pub fn remove_finished_mutation_journal(
72    analysis: &AnalysisOptions,
73    report: &fallow_output::MutationReport,
74) -> Result<bool, ProgrammaticError> {
75    if report.completion.status != fallow_output::MutationCompletionStatus::Complete
76        || report.runner.is_none()
77    {
78        return Ok(false);
79    }
80    let path = super::journal::Journal::path(&cache_dir(analysis)?.join("mutation"));
81    match std::fs::remove_file(&path) {
82        Ok(()) => Ok(true),
83        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
84        Err(error) => Err(cache_error(format!(
85            "failed to remove {}: {error}",
86            path.display()
87        ))),
88    }
89}
90
91/// Saves the JSON `report` of a run as the last report of the project, and
92/// returns its path. The write goes to a temporary file first, so a reader
93/// never sees half a report.
94///
95/// # Errors
96///
97/// Returns an error when the config does not resolve or the cache directory
98/// cannot be written.
99pub fn save_last_mutation_report(
100    analysis: &AnalysisOptions,
101    report: &serde_json::Value,
102) -> Result<PathBuf, ProgrammaticError> {
103    let cache_dir = cache_dir(analysis)?;
104    ignore_cache_dir(&cache_dir).map_err(|error| cache_error(error.to_string()))?;
105    let dir = cache_dir.join("mutation");
106    std::fs::create_dir_all(&dir).map_err(|error| cache_error(error.to_string()))?;
107    let bytes = serde_json::to_vec(report)
108        .map_err(|error| cache_error(format!("failed to serialize the report: {error}")))?;
109    let path = dir.join(LAST_REPORT_FILE);
110    let temporary = dir.join(format!("{LAST_REPORT_FILE}.{}.tmp", std::process::id()));
111    std::fs::write(&temporary, bytes)
112        .and_then(|()| std::fs::rename(&temporary, &path))
113        .map_err(|error| {
114            let _ = std::fs::remove_file(&temporary);
115            cache_error(format!("failed to save {}: {error}", path.display()))
116        })?;
117    Ok(path)
118}