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