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#[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 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#[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
139pub 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 pub fn new_noop() -> Self {
154 let ctx = DiagCtx::new(Path::new(""), false);
155 ctx.start_check("noop")
156 }
157
158 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 pub fn warning<T: Display>(&mut self, msg: T) {
169 output_message(&msg.to_string(), Some(&self.id), Some(COLOR_WARNING));
170 }
171
172 pub fn message<T: Display>(&mut self, msg: T) {
174 output_message(&msg.to_string(), Some(&self.id), None);
175 }
176
177 pub fn verbose_msg<T: Display>(&mut self, msg: T) {
179 if self.is_verbose_enabled() {
180 self.message(msg);
181 }
182 }
183
184 pub fn is_bad(&self) -> bool {
186 self.bad
187 }
188
189 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
214pub 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}