use std::error::Error;
use std::fmt::Debug;
use git_workarea::CommitId;
use crate::commit::{Commit, Content, Topic};
use crate::context::CheckGitContext;
#[derive(Debug, Default, Clone)]
pub struct CheckResult {
warnings: Vec<String>,
alerts: Vec<String>,
errors: Vec<String>,
temporary: bool,
allow: bool,
pass: bool,
}
pub enum Severity {
Warning,
Error,
Alert {
blocking: bool,
},
}
impl CheckResult {
pub fn new() -> Self {
Self {
warnings: Vec::new(),
alerts: Vec::new(),
errors: Vec::new(),
temporary: false,
allow: false,
pass: true,
}
}
pub fn add_message<S>(&mut self, severity: Severity, message: S) -> &mut Self
where
S: Into<String>,
{
match severity {
Severity::Warning => &mut self.warnings,
Severity::Error => {
self.pass = false;
&mut self.errors
},
Severity::Alert {
blocking,
} => {
if blocking {
self.pass = false;
}
&mut self.alerts
},
}
.push(message.into());
self
}
pub fn add_warning<S: Into<String>>(&mut self, warning: S) -> &mut Self {
self.add_message(Severity::Warning, warning.into())
}
pub fn add_alert<S: Into<String>>(&mut self, alert: S, should_block: bool) -> &mut Self {
self.add_message(
Severity::Alert {
blocking: should_block,
},
alert.into(),
)
}
pub fn add_error<S: Into<String>>(&mut self, error: S) -> &mut Self {
self.add_message(Severity::Error, error.into())
}
pub fn make_temporary(&mut self) -> &mut Self {
self.temporary = true;
self
}
pub fn whitelist(&mut self) -> &mut Self {
self.allow = true;
self
}
pub fn warnings(&self) -> &Vec<String> {
&self.warnings
}
pub fn alerts(&self) -> &Vec<String> {
&self.alerts
}
pub fn errors(&self) -> &Vec<String> {
&self.errors
}
pub fn temporary(&self) -> bool {
self.temporary
}
pub fn allowed(&self) -> bool {
self.allow
}
pub fn pass(&self) -> bool {
self.pass
}
pub fn combine(self, other: Self) -> Self {
Self {
warnings: self.warnings.into_iter().chain(other.warnings).collect(),
alerts: self.alerts.into_iter().chain(other.alerts).collect(),
errors: self.errors.into_iter().chain(other.errors).collect(),
temporary: self.temporary || other.temporary,
allow: self.allow || other.allow,
pass: self.pass && other.pass,
}
}
}
pub trait Check: Debug + Send + Sync {
fn name(&self) -> &str;
fn check(&self, ctx: &CheckGitContext, commit: &Commit) -> Result<CheckResult, Box<dyn Error>>;
}
pub trait BranchCheck: Debug + Send + Sync {
fn name(&self) -> &str;
fn check(
&self,
ctx: &CheckGitContext,
commit: &CommitId,
) -> Result<CheckResult, Box<dyn Error>>;
}
pub trait TopicCheck: Debug + Send + Sync {
fn name(&self) -> &str;
fn check(&self, ctx: &CheckGitContext, topic: &Topic) -> Result<CheckResult, Box<dyn Error>>;
}
pub trait ContentCheck: Debug + Send + Sync {
fn name(&self) -> &str;
fn check(
&self,
ctx: &CheckGitContext,
content: &dyn Content,
) -> Result<CheckResult, Box<dyn Error>>;
}
impl<T> Check for T
where
T: ContentCheck,
{
fn name(&self) -> &str {
self.name()
}
fn check(&self, ctx: &CheckGitContext, commit: &Commit) -> Result<CheckResult, Box<dyn Error>> {
self.check(ctx, commit)
}
}
impl<T> TopicCheck for T
where
T: ContentCheck,
{
fn name(&self) -> &str {
self.name()
}
fn check(&self, ctx: &CheckGitContext, topic: &Topic) -> Result<CheckResult, Box<dyn Error>> {
self.check(ctx, topic)
}
}
#[cfg(test)]
mod tests {
use crate::CheckResult;
#[test]
fn test_check_result_add_warning() {
let mut result = CheckResult::new();
assert!(result.warnings().is_empty());
result.add_warning("warning");
assert!(!result.warnings().is_empty());
}
#[test]
fn test_check_result_add_error() {
let mut result = CheckResult::new();
assert!(result.errors().is_empty());
result.add_error("error");
assert!(!result.errors().is_empty());
}
#[test]
fn test_check_result_add_alert() {
let mut result = CheckResult::new();
assert!(result.alerts().is_empty());
result.add_alert("error", true);
assert!(!result.alerts().is_empty());
}
#[test]
fn test_check_result_make_temporary() {
let mut result = CheckResult::new();
assert!(!result.temporary());
result.make_temporary();
assert!(result.temporary());
}
#[test]
fn test_check_result_whitelist() {
let mut result = CheckResult::new();
assert!(!result.allowed());
result.whitelist();
assert!(result.allowed());
}
#[test]
fn test_check_result_combine_temporary() {
let temp_result = {
let mut result = CheckResult::new();
result.make_temporary();
result
};
let non_temp_result = CheckResult::new();
let items = &[
(&temp_result, &non_temp_result, true),
(&temp_result, &temp_result, true),
(&non_temp_result, &non_temp_result, false),
(&non_temp_result, &temp_result, true),
];
for (l, r, e) in items {
assert_eq!((**l).clone().combine((**r).clone()).temporary(), *e);
}
}
#[test]
fn test_check_result_combine_whitelist() {
let temp_result = {
let mut result = CheckResult::new();
result.whitelist();
result
};
let non_temp_result = CheckResult::new();
let items = &[
(&temp_result, &non_temp_result, true),
(&temp_result, &temp_result, true),
(&non_temp_result, &non_temp_result, false),
(&non_temp_result, &temp_result, true),
];
for (l, r, e) in items {
assert_eq!((**l).clone().combine((**r).clone()).allowed(), *e);
}
}
mod mock {
use std::sync::Mutex;
use crate::impl_prelude::*;
#[derive(Debug)]
pub struct MockCheck {
checked: Mutex<bool>,
}
impl Default for MockCheck {
fn default() -> Self {
Self {
checked: Mutex::new(false),
}
}
}
impl Drop for MockCheck {
fn drop(&mut self) {
let flag = self.checked.lock().expect("poisoned mock check lock");
assert!(*flag);
}
}
impl MockCheck {
fn trip(&self) {
let mut flag = self.checked.lock().expect("poisoned mock check lock");
*flag = true;
}
}
impl ContentCheck for MockCheck {
fn name(&self) -> &str {
self.trip();
"mock-check"
}
fn check(
&self,
_: &CheckGitContext,
_: &dyn Content,
) -> Result<CheckResult, Box<dyn Error>> {
self.trip();
Ok(CheckResult::new())
}
}
}
const TARGET_COMMIT: &str = "27ff3ef5532d76afa046f76f4dd8f588dc3e83c3";
const SIMPLE_COMMIT: &str = "43adb8173eb6d7a39f98e1ec3351cf27414c9aa1";
fn test_run_check(conf: &crate::GitCheckConfiguration, name: &str) {
use std::path::Path;
use git_workarea::{CommitId, GitContext, Identity};
let gitdir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../.git"));
if !gitdir.exists() {
panic!("The tests must be run from a git checkout.");
}
let ctx = GitContext::new(gitdir);
let identity = Identity::new(
"Rust Git Checks Core Tests",
"rust-git-checks-core@example.com",
);
conf.run_topic(
&ctx,
name,
&CommitId::new(TARGET_COMMIT),
&CommitId::new(SIMPLE_COMMIT),
&identity,
)
.unwrap();
}
#[test]
fn test_impl_check_for_content_name() {
use crate::{Check, ContentCheck};
let check = mock::MockCheck::default();
assert_eq!(Check::name(&check), ContentCheck::name(&check));
}
#[test]
fn test_impl_check_for_content_check() {
let check = mock::MockCheck::default();
let mut conf = crate::GitCheckConfiguration::new();
conf.add_check(&check);
test_run_check(&conf, "test_impl_check_for_content_check");
}
#[test]
fn test_impl_topiccheck_for_content_name() {
use crate::{ContentCheck, TopicCheck};
let check = mock::MockCheck::default();
assert_eq!(TopicCheck::name(&check), ContentCheck::name(&check));
}
#[test]
fn test_impl_topiccheck_for_content_check() {
let check = mock::MockCheck::default();
let mut conf = crate::GitCheckConfiguration::new();
conf.add_topic_check(&check);
test_run_check(&conf, "test_impl_topiccheck_for_content_check");
}
}