Skip to main content

yog_cli/
diagnostics.rs

1//! What a build or a check finds, each as a record: what it is (a code),
2//! how bad, what's wrong and where. Written for a tool — a Yog Data frame
3//! each to the file `YOG_DIAGNOSTICS` names (`yog_ids::env::DIAGNOSTICS`) —
4//! while the text goes on as ever (each one's `Display`).
5
6use std::fmt;
7use std::path::Path;
8
9pub use yog_data::{Diagnostic, Severity};
10use yog_sync::Mutex;
11use yog_sync::atomic::{AtomicUsize, Ordering};
12
13/// How many errors this run reported.
14static ERRORS: AtomicUsize = AtomicUsize::new(0);
15
16/// What this run reported: each one once, though the build meets it again
17/// for every platform.
18static REPORTED: Mutex<Vec<Diagnostic>> = Mutex::new(Vec::new());
19
20/// Reports `d` to the tool listening (if one is), once; its text is the
21/// caller's to print.
22pub fn report(d: &Diagnostic) {
23    {
24        let mut seen = REPORTED.lock();
25        if seen.contains(d) {
26            return;
27        }
28        seen.push(d.clone());
29    }
30    if d.is_error() {
31        ERRORS.fetch_add(1, Ordering::Relaxed);
32    }
33    let Some(path) = std::env::var_os(yog_ids::env::DIAGNOSTICS) else {
34        return;
35    };
36    if let Err(e) = append(Path::new(&path), d) {
37        // The text still says it; only the tool misses it.
38        eprintln!(
39            "warning: couldn't report a diagnostic to {}: {e}",
40            Path::new(&path).display()
41        );
42    }
43}
44
45/// `message` reported as an error of kind `code`, and returned to be said.
46pub fn failed(code: &str, message: impl fmt::Display) -> String {
47    let d = Diagnostic::error(code, message);
48    report(&d);
49    d.message
50}
51
52/// `d` reported, and returned to be said.
53pub fn reported(d: Diagnostic) -> String {
54    report(&d);
55    d.to_string()
56}
57
58/// Reports every one of `ds`.
59pub fn report_all<'d>(ds: impl IntoIterator<Item = &'d Diagnostic>) {
60    for d in ds {
61        report(d);
62    }
63}
64
65/// How many errors this run has reported so far.
66pub fn errors_reported() -> usize {
67    ERRORS.load(Ordering::Relaxed)
68}
69
70fn append(path: &Path, d: &Diagnostic) -> std::io::Result<()> {
71    let mut file = std::fs::OpenOptions::new()
72        .create(true)
73        .append(true)
74        .open(path)?;
75    yog_data::io::write_frame(&mut file, d)
76}
77
78/// Runs `command` (a `yog` one) with its diagnostics written to a file of
79/// its own: how it went, and what it reported.
80pub fn collected(
81    command: &mut std::process::Command,
82) -> std::io::Result<(std::process::Output, Vec<Diagnostic>)> {
83    static RUNS: AtomicUsize = AtomicUsize::new(0);
84    let run = RUNS.fetch_add(1, Ordering::Relaxed);
85    let file = std::env::temp_dir().join(format!("{FEED_PREFIX}{}-{run}", std::process::id()));
86    let output = command.env(yog_ids::env::DIAGNOSTICS, &file).output();
87    let found = read_all(&file);
88    // Gone with the run; never written when nothing was found.
89    let _ = std::fs::remove_file(&file);
90    Ok((output?, found))
91}
92
93/// What a run's diagnostics files are named from.
94const FEED_PREFIX: &str = "yog-diagnostics-";
95
96/// Every record in the file at `path` (none when there's no file).
97pub fn read_all(path: &Path) -> Vec<Diagnostic> {
98    let Ok(bytes) = std::fs::read(path) else {
99        return Vec::new();
100    };
101    let mut rest = bytes.as_slice();
102    let mut found = Vec::new();
103    while let Ok(Some(d)) = yog_data::io::read_frame::<Diagnostic>(&mut rest) {
104        found.push(d);
105    }
106    found
107}
108
109/// `ds` as the text says them, one a line, indented under a heading.
110pub fn listed(ds: &[Diagnostic]) -> String {
111    let lines: Vec<String> = ds.iter().map(ToString::to_string).collect();
112    lines.join(LIST_SEP)
113}
114
115/// What separates the lines of a list of them (under its heading).
116pub const LIST_SEP: &str = "\n  ";
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn a_failure_is_reported_once_and_said() {
124        let said = failed(yog_ids::build_problem::CONTENT, "no model");
125        assert_eq!(said, "no model");
126        let before = errors_reported();
127        failed(yog_ids::build_problem::CONTENT, "no model");
128        assert_eq!(errors_reported(), before);
129    }
130}