Skip to main content

tidy/
diagnostics.rs

1use std::collections::HashSet;
2use std::fmt::{Display, Formatter};
3use std::io;
4use std::path::{Path, PathBuf};
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{Arc, Mutex};
7
8use build_helper::ci::CiEnv;
9use build_helper::git::{GitConfig, get_closest_upstream_commit};
10use build_helper::stage0_parser::{Stage0Config, parse_stage0_file};
11use termcolor::Color;
12
13/// CLI flags used by tidy.
14#[derive(Clone, Default)]
15pub struct TidyFlags {
16    /// Applies style and formatting changes during a tidy run.
17    bless: bool,
18}
19
20impl TidyFlags {
21    pub fn new(bless: bool) -> Self {
22        Self { bless }
23    }
24}
25
26/// Collects diagnostics from all tidy steps, and contains shared information
27/// that determines how should message and logs be presented.
28///
29/// Since checks are executed in parallel, the context is internally synchronized, to avoid
30/// all checks to lock it explicitly.
31#[derive(Clone)]
32pub struct TidyCtx {
33    tidy_flags: TidyFlags,
34    diag_ctx: Arc<Mutex<DiagCtxInner>>,
35    ci_env: CiEnv,
36    pub base_commit: Option<String>,
37}
38
39impl TidyCtx {
40    pub fn new(
41        root_path: &Path,
42        verbose: bool,
43        ci_flag: Option<bool>,
44        tidy_flags: TidyFlags,
45    ) -> Self {
46        let ci_env = match ci_flag {
47            Some(true) => CiEnv::GitHubActions,
48            Some(false) => CiEnv::None,
49            None => CiEnv::current(),
50        };
51
52        let mut tidy_ctx = Self {
53            diag_ctx: Arc::new(Mutex::new(DiagCtxInner {
54                running_checks: Default::default(),
55                finished_checks: Default::default(),
56                root_path: root_path.to_path_buf(),
57                verbose,
58            })),
59            tidy_flags,
60            ci_env,
61            base_commit: None,
62        };
63        tidy_ctx.base_commit = find_base_commit(&tidy_ctx);
64
65        tidy_ctx
66    }
67
68    pub fn is_bless_enabled(&self) -> bool {
69        self.tidy_flags.bless
70    }
71
72    pub fn is_running_on_ci(&self) -> bool {
73        self.ci_env.is_running_in_ci()
74    }
75
76    pub fn start_check<Id: Into<CheckId>>(&self, id: Id) -> RunningCheck {
77        let mut id = id.into();
78
79        let mut ctx = self.diag_ctx.lock().unwrap();
80
81        // Shorten path for shorter diagnostics
82        id.path = match id.path {
83            Some(path) => Some(path.strip_prefix(&ctx.root_path).unwrap_or(&path).to_path_buf()),
84            None => None,
85        };
86
87        ctx.start_check(id.clone());
88        RunningCheck {
89            id,
90            bad: AtomicBool::new(false),
91            ctx: self.diag_ctx.clone(),
92            #[cfg(test)]
93            errors: Mutex::new(vec![]),
94        }
95    }
96
97    pub fn into_failed_checks(self) -> Vec<FinishedCheck> {
98        let ctx = Arc::into_inner(self.diag_ctx).unwrap().into_inner().unwrap();
99        assert!(ctx.running_checks.is_empty(), "Some checks are still running");
100        ctx.finished_checks.into_iter().filter(|c| c.bad).collect()
101    }
102}
103
104fn find_base_commit(tidy_ctx: &TidyCtx) -> Option<String> {
105    let mut check = tidy_ctx.start_check("CI history");
106
107    let stage0 = parse_stage0_file();
108    let Stage0Config { nightly_branch, git_merge_commit_email, .. } = stage0.config;
109
110    let base_commit = match get_closest_upstream_commit(
111        None,
112        &GitConfig {
113            nightly_branch: &nightly_branch,
114            git_merge_commit_email: &git_merge_commit_email,
115        },
116        tidy_ctx.ci_env,
117    ) {
118        Ok(Some(commit)) => Some(commit),
119        Ok(None) => {
120            error_if_in_ci("no base commit found", tidy_ctx.is_running_on_ci(), &mut check);
121            None
122        }
123        Err(error) => {
124            error_if_in_ci(
125                &format!("failed to retrieve base commit: {error}"),
126                tidy_ctx.is_running_on_ci(),
127                &mut check,
128            );
129            None
130        }
131    };
132
133    base_commit
134}
135
136fn error_if_in_ci(msg: &str, is_ci: bool, check: &mut RunningCheck) {
137    if is_ci {
138        check.error(msg);
139    } else {
140        check.warning(format!("{msg}. Some checks will be skipped."));
141    }
142}
143
144struct DiagCtxInner {
145    running_checks: HashSet<CheckId>,
146    finished_checks: HashSet<FinishedCheck>,
147    verbose: bool,
148    root_path: PathBuf,
149}
150
151impl DiagCtxInner {
152    fn start_check(&mut self, id: CheckId) {
153        if self.has_check_id(&id) {
154            panic!("Starting a check named `{id:?}` for the second time");
155        }
156
157        self.running_checks.insert(id);
158    }
159
160    fn finish_check(&mut self, check: FinishedCheck) {
161        assert!(
162            self.running_checks.remove(&check.id),
163            "Finishing check `{:?}` that was not started",
164            check.id
165        );
166
167        if check.bad {
168            output_message("FAIL", Some(&check.id), Some(COLOR_ERROR));
169        } else if self.verbose {
170            output_message("OK", Some(&check.id), Some(COLOR_SUCCESS));
171        }
172
173        self.finished_checks.insert(check);
174    }
175
176    fn has_check_id(&self, id: &CheckId) -> bool {
177        self.running_checks
178            .iter()
179            .chain(self.finished_checks.iter().map(|c| &c.id))
180            .any(|c| c == id)
181    }
182}
183
184/// Identifies a single step
185#[derive(PartialEq, Eq, Hash, Clone, Debug)]
186pub struct CheckId {
187    pub name: String,
188    pub path: Option<PathBuf>,
189}
190
191impl CheckId {
192    pub fn new(name: &'static str) -> Self {
193        Self { name: name.to_string(), path: None }
194    }
195
196    pub fn path(self, path: &Path) -> Self {
197        Self { path: Some(path.to_path_buf()), ..self }
198    }
199}
200
201impl From<&'static str> for CheckId {
202    fn from(name: &'static str) -> Self {
203        Self::new(name)
204    }
205}
206
207impl Display for CheckId {
208    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
209        write!(f, "{}", self.name)?;
210        if let Some(path) = &self.path {
211            write!(f, " ({})", path.display())?;
212        }
213        Ok(())
214    }
215}
216
217#[derive(PartialEq, Eq, Hash, Debug)]
218pub struct FinishedCheck {
219    id: CheckId,
220    bad: bool,
221}
222
223impl FinishedCheck {
224    pub fn id(&self) -> &CheckId {
225        &self.id
226    }
227}
228
229/// Represents a single tidy check, identified by its `name`, running.
230pub struct RunningCheck {
231    id: CheckId,
232    bad: AtomicBool,
233    ctx: Arc<Mutex<DiagCtxInner>>,
234    #[cfg(test)]
235    errors: Mutex<Vec<String>>,
236}
237
238impl RunningCheck {
239    /// Creates a new instance of a running check without going through the diag
240    /// context.
241    /// Useful if you want to run some functions from tidy without configuring
242    /// diagnostics.
243    pub fn new_noop() -> Self {
244        let ctx = TidyCtx::new(Path::new(""), false, None, TidyFlags::default());
245        ctx.start_check("noop")
246    }
247
248    /// Immediately output an error and mark the check as failed.
249    pub fn error<T: Display>(&self, msg: T) {
250        self.mark_as_bad();
251        let msg = msg.to_string();
252        output_message(&msg, Some(&self.id), Some(COLOR_ERROR));
253        #[cfg(test)]
254        self.errors.lock().unwrap().push(msg);
255    }
256
257    /// Immediately output a warning.
258    pub fn warning<T: Display>(&self, msg: T) {
259        output_message(&msg.to_string(), Some(&self.id), Some(COLOR_WARNING));
260    }
261
262    /// Output an informational message
263    pub fn message<T: Display>(&self, msg: T) {
264        output_message(&msg.to_string(), Some(&self.id), None);
265    }
266
267    /// Output a message only if verbose output is enabled.
268    pub fn verbose_msg<T: Display>(&self, msg: T) {
269        if self.is_verbose_enabled() {
270            self.message(msg);
271        }
272    }
273
274    /// Has an error already occurred for this check?
275    pub fn is_bad(&self) -> bool {
276        self.bad.load(Ordering::Relaxed)
277    }
278
279    /// Is verbose output enabled?
280    pub fn is_verbose_enabled(&self) -> bool {
281        self.ctx.lock().unwrap().verbose
282    }
283
284    #[cfg(test)]
285    pub fn get_errors(&self) -> Vec<String> {
286        self.errors.lock().unwrap().clone()
287    }
288
289    fn mark_as_bad(&self) {
290        self.bad.store(true, Ordering::Relaxed);
291    }
292}
293
294impl Drop for RunningCheck {
295    fn drop(&mut self) {
296        self.ctx.lock().unwrap().finish_check(FinishedCheck {
297            id: self.id.clone(),
298            bad: self.bad.load(Ordering::Relaxed),
299        })
300    }
301}
302
303pub const COLOR_SUCCESS: Color = Color::Green;
304pub const COLOR_ERROR: Color = Color::Red;
305pub const COLOR_WARNING: Color = Color::Yellow;
306
307/// Output a message to stderr.
308/// The message can be optionally scoped to a certain check, and it can also have a certain color.
309pub fn output_message(msg: &str, id: Option<&CheckId>, color: Option<Color>) {
310    use termcolor::{ColorChoice, ColorSpec};
311
312    let stderr: &mut dyn termcolor::WriteColor = if cfg!(test) {
313        &mut StderrForUnitTests
314    } else {
315        &mut termcolor::StandardStream::stderr(ColorChoice::Auto)
316    };
317
318    if let Some(color) = &color {
319        stderr.set_color(ColorSpec::new().set_fg(Some(*color))).unwrap();
320    }
321
322    match id {
323        Some(id) => {
324            write!(stderr, "tidy [{}", id.name).unwrap();
325            if let Some(path) = &id.path {
326                write!(stderr, " ({})", path.display()).unwrap();
327            }
328            write!(stderr, "]").unwrap();
329        }
330        None => {
331            write!(stderr, "tidy").unwrap();
332        }
333    }
334    if color.is_some() {
335        stderr.set_color(&ColorSpec::new()).unwrap();
336    }
337
338    writeln!(stderr, ": {msg}").unwrap();
339}
340
341/// An implementation of `io::Write` and `termcolor::WriteColor` that writes
342/// to stderr via `eprint!`, so that the output can be properly captured when
343/// running tidy's unit tests.
344struct StderrForUnitTests;
345
346impl io::Write for StderrForUnitTests {
347    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
348        eprint!("{}", String::from_utf8_lossy(buf));
349        Ok(buf.len())
350    }
351
352    fn flush(&mut self) -> io::Result<()> {
353        Ok(())
354    }
355}
356
357impl termcolor::WriteColor for StderrForUnitTests {
358    fn supports_color(&self) -> bool {
359        false
360    }
361
362    fn set_color(&mut self, _spec: &termcolor::ColorSpec) -> io::Result<()> {
363        Ok(())
364    }
365
366    fn reset(&mut self) -> io::Result<()> {
367        Ok(())
368    }
369}