1use std::error::Error;
10use std::fmt::Debug;
11
12use git_workarea::CommitId;
13
14use crate::commit::{Commit, Content, Topic};
15use crate::context::CheckGitContext;
16
17#[derive(Debug, Default, Clone)]
19pub struct CheckResult {
20 warnings: Vec<String>,
22 alerts: Vec<String>,
26 errors: Vec<String>,
30
31 temporary: bool,
33 allow: bool,
35 pass: bool,
37}
38
39pub enum Severity {
41 Warning,
43 Error,
45 Alert {
47 blocking: bool,
49 },
50}
51
52impl CheckResult {
53 pub fn new() -> Self {
55 Self {
56 warnings: Vec::new(),
57 alerts: Vec::new(),
58 errors: Vec::new(),
59
60 temporary: false,
61 allow: false,
62 pass: true,
63 }
64 }
65
66 pub fn add_message<S>(&mut self, severity: Severity, message: S) -> &mut Self
68 where
69 S: Into<String>,
70 {
71 match severity {
72 Severity::Warning => &mut self.warnings,
73 Severity::Error => {
74 self.pass = false;
75 &mut self.errors
76 },
77 Severity::Alert {
78 blocking,
79 } => {
80 if blocking {
81 self.pass = false;
82 }
83 &mut self.alerts
84 },
85 }
86 .push(message.into());
87
88 self
89 }
90
91 pub fn add_warning<S: Into<String>>(&mut self, warning: S) -> &mut Self {
93 self.add_message(Severity::Warning, warning.into())
94 }
95
96 pub fn add_alert<S: Into<String>>(&mut self, alert: S, should_block: bool) -> &mut Self {
101 self.add_message(
102 Severity::Alert {
103 blocking: should_block,
104 },
105 alert.into(),
106 )
107 }
108
109 pub fn add_error<S: Into<String>>(&mut self, error: S) -> &mut Self {
113 self.add_message(Severity::Error, error.into())
114 }
115
116 pub fn make_temporary(&mut self) -> &mut Self {
118 self.temporary = true;
119
120 self
121 }
122
123 pub fn whitelist(&mut self) -> &mut Self {
125 self.allow = true;
126
127 self
128 }
129
130 pub fn warnings(&self) -> &Vec<String> {
132 &self.warnings
133 }
134
135 pub fn alerts(&self) -> &Vec<String> {
137 &self.alerts
138 }
139
140 pub fn errors(&self) -> &Vec<String> {
142 &self.errors
143 }
144
145 pub fn temporary(&self) -> bool {
147 self.temporary
148 }
149
150 pub fn allowed(&self) -> bool {
152 self.allow
153 }
154
155 pub fn pass(&self) -> bool {
157 self.pass
158 }
159
160 pub fn combine(self, other: Self) -> Self {
162 Self {
163 warnings: self.warnings.into_iter().chain(other.warnings).collect(),
164 alerts: self.alerts.into_iter().chain(other.alerts).collect(),
165 errors: self.errors.into_iter().chain(other.errors).collect(),
166
167 temporary: self.temporary || other.temporary,
168 allow: self.allow || other.allow,
169 pass: self.pass && other.pass,
170 }
171 }
172}
173
174pub trait Check: Debug + Send + Sync {
176 fn name(&self) -> &str;
178
179 fn check(&self, ctx: &CheckGitContext, commit: &Commit) -> Result<CheckResult, Box<dyn Error>>;
181}
182
183pub trait BranchCheck: Debug + Send + Sync {
188 fn name(&self) -> &str;
190
191 fn check(
193 &self,
194 ctx: &CheckGitContext,
195 commit: &CommitId,
196 ) -> Result<CheckResult, Box<dyn Error>>;
197}
198
199pub trait TopicCheck: Debug + Send + Sync {
203 fn name(&self) -> &str;
205
206 fn check(&self, ctx: &CheckGitContext, topic: &Topic) -> Result<CheckResult, Box<dyn Error>>;
208}
209
210pub trait ContentCheck: Debug + Send + Sync {
215 fn name(&self) -> &str;
217
218 fn check(
220 &self,
221 ctx: &CheckGitContext,
222 content: &dyn Content,
223 ) -> Result<CheckResult, Box<dyn Error>>;
224}
225
226impl<T> Check for T
227where
228 T: ContentCheck,
229{
230 fn name(&self) -> &str {
231 self.name()
232 }
233
234 fn check(&self, ctx: &CheckGitContext, commit: &Commit) -> Result<CheckResult, Box<dyn Error>> {
235 self.check(ctx, commit)
236 }
237}
238
239impl<T> TopicCheck for T
240where
241 T: ContentCheck,
242{
243 fn name(&self) -> &str {
244 self.name()
245 }
246
247 fn check(&self, ctx: &CheckGitContext, topic: &Topic) -> Result<CheckResult, Box<dyn Error>> {
248 self.check(ctx, topic)
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use crate::CheckResult;
255
256 #[test]
257 fn test_check_result_add_warning() {
258 let mut result = CheckResult::new();
259 assert!(result.warnings().is_empty());
260 result.add_warning("warning");
261 assert!(!result.warnings().is_empty());
262 }
263
264 #[test]
265 fn test_check_result_add_error() {
266 let mut result = CheckResult::new();
267 assert!(result.errors().is_empty());
268 result.add_error("error");
269 assert!(!result.errors().is_empty());
270 }
271
272 #[test]
273 fn test_check_result_add_alert() {
274 let mut result = CheckResult::new();
275 assert!(result.alerts().is_empty());
276 result.add_alert("error", true);
277 assert!(!result.alerts().is_empty());
278 }
279
280 #[test]
281 fn test_check_result_make_temporary() {
282 let mut result = CheckResult::new();
283 assert!(!result.temporary());
284 result.make_temporary();
285 assert!(result.temporary());
286 }
287
288 #[test]
289 fn test_check_result_whitelist() {
290 let mut result = CheckResult::new();
291 assert!(!result.allowed());
292 result.whitelist();
293 assert!(result.allowed());
294 }
295
296 #[test]
297 fn test_check_result_combine_temporary() {
298 let temp_result = {
299 let mut result = CheckResult::new();
300 result.make_temporary();
301 result
302 };
303 let non_temp_result = CheckResult::new();
304
305 let items = &[
306 (&temp_result, &non_temp_result, true),
307 (&temp_result, &temp_result, true),
308 (&non_temp_result, &non_temp_result, false),
309 (&non_temp_result, &temp_result, true),
310 ];
311
312 for (l, r, e) in items {
313 assert_eq!((**l).clone().combine((**r).clone()).temporary(), *e);
314 }
315 }
316
317 #[test]
318 fn test_check_result_combine_whitelist() {
319 let temp_result = {
320 let mut result = CheckResult::new();
321 result.whitelist();
322 result
323 };
324 let non_temp_result = CheckResult::new();
325
326 let items = &[
327 (&temp_result, &non_temp_result, true),
328 (&temp_result, &temp_result, true),
329 (&non_temp_result, &non_temp_result, false),
330 (&non_temp_result, &temp_result, true),
331 ];
332
333 for (l, r, e) in items {
334 assert_eq!((**l).clone().combine((**r).clone()).allowed(), *e);
335 }
336 }
337
338 mod mock {
339 use std::sync::Mutex;
340
341 use crate::impl_prelude::*;
342
343 #[derive(Debug)]
344 pub struct MockCheck {
345 checked: Mutex<bool>,
346 }
347
348 impl Default for MockCheck {
349 fn default() -> Self {
350 Self {
351 checked: Mutex::new(false),
352 }
353 }
354 }
355
356 impl Drop for MockCheck {
357 fn drop(&mut self) {
358 let flag = self.checked.lock().expect("poisoned mock check lock");
359 assert!(*flag);
360 }
361 }
362
363 impl MockCheck {
364 fn trip(&self) {
365 let mut flag = self.checked.lock().expect("poisoned mock check lock");
366 *flag = true;
367 }
368 }
369
370 impl ContentCheck for MockCheck {
371 fn name(&self) -> &str {
372 self.trip();
373 "mock-check"
374 }
375
376 fn check(
377 &self,
378 _: &CheckGitContext,
379 _: &dyn Content,
380 ) -> Result<CheckResult, Box<dyn Error>> {
381 self.trip();
382 Ok(CheckResult::new())
383 }
384 }
385 }
386
387 const TARGET_COMMIT: &str = "27ff3ef5532d76afa046f76f4dd8f588dc3e83c3";
388 const SIMPLE_COMMIT: &str = "43adb8173eb6d7a39f98e1ec3351cf27414c9aa1";
389
390 fn test_run_check(conf: &crate::GitCheckConfiguration, name: &str) {
391 use std::path::Path;
392
393 use git_workarea::{CommitId, GitContext, Identity};
394
395 let gitdir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../.git"));
396 if !gitdir.exists() {
397 panic!("The tests must be run from a git checkout.");
398 }
399
400 let ctx = GitContext::new(gitdir);
401 let identity = Identity::new(
402 "Rust Git Checks Core Tests",
403 "rust-git-checks-core@example.com",
404 );
405 conf.run_topic(
406 &ctx,
407 name,
408 &CommitId::new(TARGET_COMMIT),
409 &CommitId::new(SIMPLE_COMMIT),
410 &identity,
411 )
412 .unwrap();
413 }
414
415 #[test]
416 fn test_impl_check_for_content_name() {
417 use crate::{Check, ContentCheck};
418
419 let check = mock::MockCheck::default();
420
421 assert_eq!(Check::name(&check), ContentCheck::name(&check));
422 }
423
424 #[test]
425 fn test_impl_check_for_content_check() {
426 let check = mock::MockCheck::default();
427 let mut conf = crate::GitCheckConfiguration::new();
428 conf.add_check(&check);
429 test_run_check(&conf, "test_impl_check_for_content_check");
430 }
431
432 #[test]
433 fn test_impl_topiccheck_for_content_name() {
434 use crate::{ContentCheck, TopicCheck};
435
436 let check = mock::MockCheck::default();
437
438 assert_eq!(TopicCheck::name(&check), ContentCheck::name(&check));
439 }
440
441 #[test]
442 fn test_impl_topiccheck_for_content_check() {
443 let check = mock::MockCheck::default();
444 let mut conf = crate::GitCheckConfiguration::new();
445 conf.add_topic_check(&check);
446 test_run_check(&conf, "test_impl_topiccheck_for_content_check");
447 }
448}