git_checks_core/
check.rs

1// Copyright Kitware, Inc.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use 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/// The results of a check.
18#[derive(Debug, Default, Clone)]
19pub struct CheckResult {
20    /// The warnings from running checks.
21    warnings: Vec<String>,
22    /// The alerts from running checks.
23    ///
24    /// These are meant to be directed towards administrators of the repository.
25    alerts: Vec<String>,
26    /// The errors from running checks.
27    ///
28    /// Errors cause the checks to be in a "failed" state.
29    errors: Vec<String>,
30
31    /// Whether any messages may be temporary.
32    temporary: bool,
33    /// Whether errors should be ignored or not.
34    allow: bool,
35    /// Whether the checks succeeded or not.
36    pass: bool,
37}
38
39/// The severity of a message.
40pub enum Severity {
41    /// The message is a warning.
42    Warning,
43    /// The message is an error.
44    Error,
45    /// The message should be brought to the attention of project maintainers.
46    Alert {
47        /// Whether the checks should fail due to the message or not.
48        blocking: bool,
49    },
50}
51
52impl CheckResult {
53    /// Create a new results structure.
54    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    /// Add a message to the result.
67    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    /// Adds a warning message to the results.
92    pub fn add_warning<S: Into<String>>(&mut self, warning: S) -> &mut Self {
93        self.add_message(Severity::Warning, warning.into())
94    }
95
96    /// Adds an alert to the results.
97    ///
98    /// These messages should be brought to the attention of those maintaining the deployment of
99    /// the checks.
100    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    /// Adds a error message to the results.
110    ///
111    /// Also marks the checks as having failed.
112    pub fn add_error<S: Into<String>>(&mut self, error: S) -> &mut Self {
113        self.add_message(Severity::Error, error.into())
114    }
115
116    /// Indicates that there are messages which may be temporary.
117    pub fn make_temporary(&mut self) -> &mut Self {
118        self.temporary = true;
119
120        self
121    }
122
123    /// Allows the checks to pass no matter what.
124    pub fn whitelist(&mut self) -> &mut Self {
125        self.allow = true;
126
127        self
128    }
129
130    /// The warnings from the checks.
131    pub fn warnings(&self) -> &Vec<String> {
132        &self.warnings
133    }
134
135    /// The alerts from the checks.
136    pub fn alerts(&self) -> &Vec<String> {
137        &self.alerts
138    }
139
140    /// The errors from the checks.
141    pub fn errors(&self) -> &Vec<String> {
142        &self.errors
143    }
144
145    /// Whether there are temporary messages or not.
146    pub fn temporary(&self) -> bool {
147        self.temporary
148    }
149
150    /// Whether the checks will allow the commit no matter what.
151    pub fn allowed(&self) -> bool {
152        self.allow
153    }
154
155    /// Whether the checks passed or failed.
156    pub fn pass(&self) -> bool {
157        self.pass
158    }
159
160    /// Combine two results together.
161    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
174/// Interface for checks which run for each commit.
175pub trait Check: Debug + Send + Sync {
176    /// The name of the check.
177    fn name(&self) -> &str;
178
179    /// Run the check.
180    fn check(&self, ctx: &CheckGitContext, commit: &Commit) -> Result<CheckResult, Box<dyn Error>>;
181}
182
183/// Interface for checks which runs once for the entire branch.
184///
185/// This is intended for checks which do not need to check the content, but instead, look at
186/// metadata such as the author or the topology of the branch.
187pub trait BranchCheck: Debug + Send + Sync {
188    /// The name of the check.
189    fn name(&self) -> &str;
190
191    /// Run the check.
192    fn check(
193        &self,
194        ctx: &CheckGitContext,
195        commit: &CommitId,
196    ) -> Result<CheckResult, Box<dyn Error>>;
197}
198
199/// Interface for checks which runs once for the entire branch, but with access to the content.
200///
201/// These checks are given the content of the entire topic against a base branch to check.
202pub trait TopicCheck: Debug + Send + Sync {
203    /// The name of the check.
204    fn name(&self) -> &str;
205
206    /// Run the check.
207    fn check(&self, ctx: &CheckGitContext, topic: &Topic) -> Result<CheckResult, Box<dyn Error>>;
208}
209
210/// Interface for checks which check the content of files.
211///
212/// These checks are not given any metadata, but only information about the content of a commit or
213/// topic.
214pub trait ContentCheck: Debug + Send + Sync {
215    /// The name of the check.
216    fn name(&self) -> &str;
217
218    /// Run the check.
219    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}