| use std::collections::HashSet; |
| use std::fmt::{Display, Formatter}; |
| use std::io; |
| use std::path::{Path, PathBuf}; |
| use std::sync::{Arc, Mutex}; |
| |
| use build_helper::ci::CiEnv; |
| use build_helper::git::{GitConfig, get_closest_upstream_commit}; |
| use build_helper::stage0_parser::{Stage0Config, parse_stage0_file}; |
| use termcolor::Color; |
| |
| /// CLI flags used by tidy. |
| #[derive(Clone, Default)] |
| pub struct TidyFlags { |
| /// Applies style and formatting changes during a tidy run. |
| bless: bool, |
| } |
| |
| impl TidyFlags { |
| pub fn new(bless: bool) -> Self { |
| Self { bless } |
| } |
| } |
| |
| /// Collects diagnostics from all tidy steps, and contains shared information |
| /// that determines how should message and logs be presented. |
| /// |
| /// Since checks are executed in parallel, the context is internally synchronized, to avoid |
| /// all checks to lock it explicitly. |
| #[derive(Clone)] |
| pub struct TidyCtx { |
| tidy_flags: TidyFlags, |
| diag_ctx: Arc<Mutex<DiagCtxInner>>, |
| ci_env: CiEnv, |
| pub base_commit: Option<String>, |
| } |
| |
| impl TidyCtx { |
| pub fn new( |
| root_path: &Path, |
| verbose: bool, |
| ci_flag: Option<bool>, |
| tidy_flags: TidyFlags, |
| ) -> Self { |
| let ci_env = match ci_flag { |
| Some(true) => CiEnv::GitHubActions, |
| Some(false) => CiEnv::None, |
| None => CiEnv::current(), |
| }; |
| |
| let mut tidy_ctx = Self { |
| diag_ctx: Arc::new(Mutex::new(DiagCtxInner { |
| running_checks: Default::default(), |
| finished_checks: Default::default(), |
| root_path: root_path.to_path_buf(), |
| verbose, |
| })), |
| tidy_flags, |
| ci_env, |
| base_commit: None, |
| }; |
| tidy_ctx.base_commit = find_base_commit(&tidy_ctx); |
| |
| tidy_ctx |
| } |
| |
| pub fn is_bless_enabled(&self) -> bool { |
| self.tidy_flags.bless |
| } |
| |
| pub fn is_running_on_ci(&self) -> bool { |
| self.ci_env.is_running_in_ci() |
| } |
| |
| pub fn start_check<Id: Into<CheckId>>(&self, id: Id) -> RunningCheck { |
| let mut id = id.into(); |
| |
| let mut ctx = self.diag_ctx.lock().unwrap(); |
| |
| // Shorten path for shorter diagnostics |
| id.path = match id.path { |
| Some(path) => Some(path.strip_prefix(&ctx.root_path).unwrap_or(&path).to_path_buf()), |
| None => None, |
| }; |
| |
| ctx.start_check(id.clone()); |
| RunningCheck { |
| id, |
| bad: false, |
| ctx: self.diag_ctx.clone(), |
| #[cfg(test)] |
| errors: vec![], |
| } |
| } |
| |
| pub fn into_failed_checks(self) -> Vec<FinishedCheck> { |
| let ctx = Arc::into_inner(self.diag_ctx).unwrap().into_inner().unwrap(); |
| assert!(ctx.running_checks.is_empty(), "Some checks are still running"); |
| ctx.finished_checks.into_iter().filter(|c| c.bad).collect() |
| } |
| } |
| |
| fn find_base_commit(tidy_ctx: &TidyCtx) -> Option<String> { |
| let mut check = tidy_ctx.start_check("CI history"); |
| |
| let stage0 = parse_stage0_file(); |
| let Stage0Config { nightly_branch, git_merge_commit_email, .. } = stage0.config; |
| |
| let base_commit = match get_closest_upstream_commit( |
| None, |
| &GitConfig { |
| nightly_branch: &nightly_branch, |
| git_merge_commit_email: &git_merge_commit_email, |
| }, |
| tidy_ctx.ci_env, |
| ) { |
| Ok(Some(commit)) => Some(commit), |
| Ok(None) => { |
| error_if_in_ci("no base commit found", tidy_ctx.is_running_on_ci(), &mut check); |
| None |
| } |
| Err(error) => { |
| error_if_in_ci( |
| &format!("failed to retrieve base commit: {error}"), |
| tidy_ctx.is_running_on_ci(), |
| &mut check, |
| ); |
| None |
| } |
| }; |
| |
| base_commit |
| } |
| |
| fn error_if_in_ci(msg: &str, is_ci: bool, check: &mut RunningCheck) { |
| if is_ci { |
| check.error(msg); |
| } else { |
| check.warning(format!("{msg}. Some checks will be skipped.")); |
| } |
| } |
| |
| struct DiagCtxInner { |
| running_checks: HashSet<CheckId>, |
| finished_checks: HashSet<FinishedCheck>, |
| verbose: bool, |
| root_path: PathBuf, |
| } |
| |
| impl DiagCtxInner { |
| fn start_check(&mut self, id: CheckId) { |
| if self.has_check_id(&id) { |
| panic!("Starting a check named `{id:?}` for the second time"); |
| } |
| |
| self.running_checks.insert(id); |
| } |
| |
| fn finish_check(&mut self, check: FinishedCheck) { |
| assert!( |
| self.running_checks.remove(&check.id), |
| "Finishing check `{:?}` that was not started", |
| check.id |
| ); |
| |
| if check.bad { |
| output_message("FAIL", Some(&check.id), Some(COLOR_ERROR)); |
| } else if self.verbose { |
| output_message("OK", Some(&check.id), Some(COLOR_SUCCESS)); |
| } |
| |
| self.finished_checks.insert(check); |
| } |
| |
| fn has_check_id(&self, id: &CheckId) -> bool { |
| self.running_checks |
| .iter() |
| .chain(self.finished_checks.iter().map(|c| &c.id)) |
| .any(|c| c == id) |
| } |
| } |
| |
| /// Identifies a single step |
| #[derive(PartialEq, Eq, Hash, Clone, Debug)] |
| pub struct CheckId { |
| pub name: String, |
| pub path: Option<PathBuf>, |
| } |
| |
| impl CheckId { |
| pub fn new(name: &'static str) -> Self { |
| Self { name: name.to_string(), path: None } |
| } |
| |
| pub fn path(self, path: &Path) -> Self { |
| Self { path: Some(path.to_path_buf()), ..self } |
| } |
| } |
| |
| impl From<&'static str> for CheckId { |
| fn from(name: &'static str) -> Self { |
| Self::new(name) |
| } |
| } |
| |
| impl Display for CheckId { |
| fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
| write!(f, "{}", self.name)?; |
| if let Some(path) = &self.path { |
| write!(f, " ({})", path.display())?; |
| } |
| Ok(()) |
| } |
| } |
| |
| #[derive(PartialEq, Eq, Hash, Debug)] |
| pub struct FinishedCheck { |
| id: CheckId, |
| bad: bool, |
| } |
| |
| impl FinishedCheck { |
| pub fn id(&self) -> &CheckId { |
| &self.id |
| } |
| } |
| |
| /// Represents a single tidy check, identified by its `name`, running. |
| pub struct RunningCheck { |
| id: CheckId, |
| bad: bool, |
| ctx: Arc<Mutex<DiagCtxInner>>, |
| #[cfg(test)] |
| errors: Vec<String>, |
| } |
| |
| impl RunningCheck { |
| /// Creates a new instance of a running check without going through the diag |
| /// context. |
| /// Useful if you want to run some functions from tidy without configuring |
| /// diagnostics. |
| pub fn new_noop() -> Self { |
| let ctx = TidyCtx::new(Path::new(""), false, None, TidyFlags::default()); |
| ctx.start_check("noop") |
| } |
| |
| /// Immediately output an error and mark the check as failed. |
| pub fn error<T: Display>(&mut self, msg: T) { |
| self.mark_as_bad(); |
| let msg = msg.to_string(); |
| output_message(&msg, Some(&self.id), Some(COLOR_ERROR)); |
| #[cfg(test)] |
| self.errors.push(msg); |
| } |
| |
| /// Immediately output a warning. |
| pub fn warning<T: Display>(&mut self, msg: T) { |
| output_message(&msg.to_string(), Some(&self.id), Some(COLOR_WARNING)); |
| } |
| |
| /// Output an informational message |
| pub fn message<T: Display>(&mut self, msg: T) { |
| output_message(&msg.to_string(), Some(&self.id), None); |
| } |
| |
| /// Output a message only if verbose output is enabled. |
| pub fn verbose_msg<T: Display>(&mut self, msg: T) { |
| if self.is_verbose_enabled() { |
| self.message(msg); |
| } |
| } |
| |
| /// Has an error already occurred for this check? |
| pub fn is_bad(&self) -> bool { |
| self.bad |
| } |
| |
| /// Is verbose output enabled? |
| pub fn is_verbose_enabled(&self) -> bool { |
| self.ctx.lock().unwrap().verbose |
| } |
| |
| #[cfg(test)] |
| pub fn get_errors(&self) -> Vec<String> { |
| self.errors.clone() |
| } |
| |
| fn mark_as_bad(&mut self) { |
| self.bad = true; |
| } |
| } |
| |
| impl Drop for RunningCheck { |
| fn drop(&mut self) { |
| self.ctx.lock().unwrap().finish_check(FinishedCheck { id: self.id.clone(), bad: self.bad }) |
| } |
| } |
| |
| pub const COLOR_SUCCESS: Color = Color::Green; |
| pub const COLOR_ERROR: Color = Color::Red; |
| pub const COLOR_WARNING: Color = Color::Yellow; |
| |
| /// Output a message to stderr. |
| /// The message can be optionally scoped to a certain check, and it can also have a certain color. |
| pub fn output_message(msg: &str, id: Option<&CheckId>, color: Option<Color>) { |
| use termcolor::{ColorChoice, ColorSpec}; |
| |
| let stderr: &mut dyn termcolor::WriteColor = if cfg!(test) { |
| &mut StderrForUnitTests |
| } else { |
| &mut termcolor::StandardStream::stderr(ColorChoice::Auto) |
| }; |
| |
| if let Some(color) = &color { |
| stderr.set_color(ColorSpec::new().set_fg(Some(*color))).unwrap(); |
| } |
| |
| match id { |
| Some(id) => { |
| write!(stderr, "tidy [{}", id.name).unwrap(); |
| if let Some(path) = &id.path { |
| write!(stderr, " ({})", path.display()).unwrap(); |
| } |
| write!(stderr, "]").unwrap(); |
| } |
| None => { |
| write!(stderr, "tidy").unwrap(); |
| } |
| } |
| if color.is_some() { |
| stderr.set_color(&ColorSpec::new()).unwrap(); |
| } |
| |
| writeln!(stderr, ": {msg}").unwrap(); |
| } |
| |
| /// An implementation of `io::Write` and `termcolor::WriteColor` that writes |
| /// to stderr via `eprint!`, so that the output can be properly captured when |
| /// running tidy's unit tests. |
| struct StderrForUnitTests; |
| |
| impl io::Write for StderrForUnitTests { |
| fn write(&mut self, buf: &[u8]) -> io::Result<usize> { |
| eprint!("{}", String::from_utf8_lossy(buf)); |
| Ok(buf.len()) |
| } |
| |
| fn flush(&mut self) -> io::Result<()> { |
| Ok(()) |
| } |
| } |
| |
| impl termcolor::WriteColor for StderrForUnitTests { |
| fn supports_color(&self) -> bool { |
| false |
| } |
| |
| fn set_color(&mut self, _spec: &termcolor::ColorSpec) -> io::Result<()> { |
| Ok(()) |
| } |
| |
| fn reset(&mut self) -> io::Result<()> { |
| Ok(()) |
| } |
| } |