tidy/
diagnostics.rs

1use std::collections::HashSet;
2use std::fmt::{Display, Formatter};
3use std::path::{Path, PathBuf};
4use std::sync::{Arc, Mutex};
5
6use termcolor::{Color, WriteColor};
7
8/// Collects diagnostics from all tidy steps, and contains shared information
9/// that determines how should message and logs be presented.
10///
11/// Since checks are executed in parallel, the context is internally synchronized, to avoid
12/// all checks to lock it explicitly.
13#[derive(Clone)]
14pub struct DiagCtx(Arc<Mutex<DiagCtxInner>>);
15
16impl DiagCtx {
17    pub fn new(root_path: &Path, verbose: bool) -> Self {
18        Self(Arc::new(Mutex::new(DiagCtxInner {
19            running_checks: Default::default(),
20            finished_checks: Default::default(),
21            root_path: root_path.to_path_buf(),
22            verbose,
23        })))
24    }
25
26    pub fn start_check<Id: Into<CheckId>>(&self, id: Id) -> RunningCheck {
27        let mut id = id.into();
28
29        let mut ctx = self.0.lock().unwrap();
30
31        // Shorten path for shorter diagnostics
32        id.path = match id.path {
33            Some(path) => Some(path.strip_prefix(&ctx.root_path).unwrap_or(&path).to_path_buf()),
34            None => None,
35        };
36
37        ctx.start_check(id.clone());
38        RunningCheck {
39            id,
40            bad: false,
41            ctx: self.0.clone(),
42            #[cfg(test)]
43            errors: vec![],
44        }
45    }
46
47    pub fn into_failed_checks(self) -> Vec<FinishedCheck> {
48        let ctx = Arc::into_inner(self.0).unwrap().into_inner().unwrap();
49        assert!(ctx.running_checks.is_empty(), "Some checks are still running");
50        ctx.finished_checks.into_iter().filter(|c| c.bad).collect()
51    }
52}
53
54struct DiagCtxInner {
55    running_checks: HashSet<CheckId>,
56    finished_checks: HashSet<FinishedCheck>,
57    verbose: bool,
58    root_path: PathBuf,
59}
60
61impl DiagCtxInner {
62    fn start_check(&mut self, id: CheckId) {
63        if self.has_check_id(&id) {
64            panic!("Starting a check named `{id:?}` for the second time");
65        }
66
67        self.running_checks.insert(id);
68    }
69
70    fn finish_check(&mut self, check: FinishedCheck) {
71        assert!(
72            self.running_checks.remove(&check.id),
73            "Finishing check `{:?}` that was not started",
74            check.id
75        );
76
77        if check.bad {
78            output_message("FAIL", Some(&check.id), Some(COLOR_ERROR));
79        } else if self.verbose {
80            output_message("OK", Some(&check.id), Some(COLOR_SUCCESS));
81        }
82
83        self.finished_checks.insert(check);
84    }
85
86    fn has_check_id(&self, id: &CheckId) -> bool {
87        self.running_checks
88            .iter()
89            .chain(self.finished_checks.iter().map(|c| &c.id))
90            .any(|c| c == id)
91    }
92}
93
94/// Identifies a single step
95#[derive(PartialEq, Eq, Hash, Clone, Debug)]
96pub struct CheckId {
97    pub name: String,
98    pub path: Option<PathBuf>,
99}
100
101impl CheckId {
102    pub fn new(name: &'static str) -> Self {
103        Self { name: name.to_string(), path: None }
104    }
105
106    pub fn path(self, path: &Path) -> Self {
107        Self { path: Some(path.to_path_buf()), ..self }
108    }
109}
110
111impl From<&'static str> for CheckId {
112    fn from(name: &'static str) -> Self {
113        Self::new(name)
114    }
115}
116
117impl Display for CheckId {
118    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
119        write!(f, "{}", self.name)?;
120        if let Some(path) = &self.path {
121            write!(f, " ({})", path.display())?;
122        }
123        Ok(())
124    }
125}
126
127#[derive(PartialEq, Eq, Hash, Debug)]
128pub struct FinishedCheck {
129    id: CheckId,
130    bad: bool,
131}
132
133impl FinishedCheck {
134    pub fn id(&self) -> &CheckId {
135        &self.id
136    }
137}
138
139/// Represents a single tidy check, identified by its `name`, running.
140pub struct RunningCheck {
141    id: CheckId,
142    bad: bool,
143    ctx: Arc<Mutex<DiagCtxInner>>,
144    #[cfg(test)]
145    errors: Vec<String>,
146}
147
148impl RunningCheck {
149    /// Creates a new instance of a running check without going through the diag
150    /// context.
151    /// Useful if you want to run some functions from tidy without configuring
152    /// diagnostics.
153    pub fn new_noop() -> Self {
154        let ctx = DiagCtx::new(Path::new(""), false);
155        ctx.start_check("noop")
156    }
157
158    /// Immediately output an error and mark the check as failed.
159    pub fn error<T: Display>(&mut self, msg: T) {
160        self.mark_as_bad();
161        let msg = msg.to_string();
162        output_message(&msg, Some(&self.id), Some(COLOR_ERROR));
163        #[cfg(test)]
164        self.errors.push(msg);
165    }
166
167    /// Immediately output a warning.
168    pub fn warning<T: Display>(&mut self, msg: T) {
169        output_message(&msg.to_string(), Some(&self.id), Some(COLOR_WARNING));
170    }
171
172    /// Output an informational message
173    pub fn message<T: Display>(&mut self, msg: T) {
174        output_message(&msg.to_string(), Some(&self.id), None);
175    }
176
177    /// Output a message only if verbose output is enabled.
178    pub fn verbose_msg<T: Display>(&mut self, msg: T) {
179        if self.is_verbose_enabled() {
180            self.message(msg);
181        }
182    }
183
184    /// Has an error already occured for this check?
185    pub fn is_bad(&self) -> bool {
186        self.bad
187    }
188
189    /// Is verbose output enabled?
190    pub fn is_verbose_enabled(&self) -> bool {
191        self.ctx.lock().unwrap().verbose
192    }
193
194    #[cfg(test)]
195    pub fn get_errors(&self) -> Vec<String> {
196        self.errors.clone()
197    }
198
199    fn mark_as_bad(&mut self) {
200        self.bad = true;
201    }
202}
203
204impl Drop for RunningCheck {
205    fn drop(&mut self) {
206        self.ctx.lock().unwrap().finish_check(FinishedCheck { id: self.id.clone(), bad: self.bad })
207    }
208}
209
210pub const COLOR_SUCCESS: Color = Color::Green;
211pub const COLOR_ERROR: Color = Color::Red;
212pub const COLOR_WARNING: Color = Color::Yellow;
213
214/// Output a message to stderr.
215/// The message can be optionally scoped to a certain check, and it can also have a certain color.
216pub fn output_message(msg: &str, id: Option<&CheckId>, color: Option<Color>) {
217    use std::io::Write;
218
219    use termcolor::{ColorChoice, ColorSpec, StandardStream};
220
221    let mut stderr = StandardStream::stderr(ColorChoice::Auto);
222    if let Some(color) = &color {
223        stderr.set_color(ColorSpec::new().set_fg(Some(*color))).unwrap();
224    }
225
226    match id {
227        Some(id) => {
228            write!(&mut stderr, "tidy [{}", id.name).unwrap();
229            if let Some(path) = &id.path {
230                write!(&mut stderr, " ({})", path.display()).unwrap();
231            }
232            write!(&mut stderr, "]").unwrap();
233        }
234        None => {
235            write!(&mut stderr, "tidy").unwrap();
236        }
237    }
238    if color.is_some() {
239        stderr.set_color(&ColorSpec::new()).unwrap();
240    }
241
242    writeln!(&mut stderr, ": {msg}").unwrap();
243}