#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Verdict {
Pass,
Fail,
Warn,
Skip,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Info,
Warning,
Error,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckResult {
pub name: String,
pub verdict: Verdict,
pub severity: Option<Severity>,
pub detail: Option<String>,
pub at: DateTime<Utc>,
pub duration_ms: Option<u64>,
}
impl CheckResult {
pub fn pass(name: impl Into<String>) -> Self {
Self {
name: name.into(),
verdict: Verdict::Pass,
severity: None,
detail: None,
at: Utc::now(),
duration_ms: None,
}
}
pub fn fail(name: impl Into<String>, severity: Severity) -> Self {
Self {
name: name.into(),
verdict: Verdict::Fail,
severity: Some(severity),
detail: None,
at: Utc::now(),
duration_ms: None,
}
}
pub fn warn(name: impl Into<String>, severity: Severity) -> Self {
Self {
name: name.into(),
verdict: Verdict::Warn,
severity: Some(severity),
detail: None,
at: Utc::now(),
duration_ms: None,
}
}
pub fn skip(name: impl Into<String>) -> Self {
Self {
name: name.into(),
verdict: Verdict::Skip,
severity: None,
detail: None,
at: Utc::now(),
duration_ms: None,
}
}
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
pub fn with_duration_ms(mut self, ms: u64) -> Self {
self.duration_ms = Some(ms);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Report {
pub schema_version: u32,
pub subject: String,
pub subject_version: String,
pub producer: Option<String>,
pub started_at: DateTime<Utc>,
pub finished_at: Option<DateTime<Utc>>,
pub checks: Vec<CheckResult>,
}
impl Report {
pub fn new(subject: impl Into<String>, subject_version: impl Into<String>) -> Self {
Self {
schema_version: 1,
subject: subject.into(),
subject_version: subject_version.into(),
producer: None,
started_at: Utc::now(),
finished_at: None,
checks: Vec::new(),
}
}
pub fn with_producer(mut self, producer: impl Into<String>) -> Self {
self.producer = Some(producer.into());
self
}
pub fn push(&mut self, result: CheckResult) {
self.checks.push(result);
}
pub fn finish(&mut self) {
self.finished_at = Some(Utc::now());
}
pub fn overall_verdict(&self) -> Verdict {
let mut saw_fail = false;
let mut saw_warn = false;
let mut saw_pass = false;
for c in &self.checks {
match c.verdict {
Verdict::Fail => saw_fail = true,
Verdict::Warn => saw_warn = true,
Verdict::Pass => saw_pass = true,
Verdict::Skip => {}
}
}
if saw_fail {
Verdict::Fail
} else if saw_warn {
Verdict::Warn
} else if saw_pass {
Verdict::Pass
} else {
Verdict::Skip
}
}
pub fn to_json(&self) -> serde_json::Result<String> {
serde_json::to_string_pretty(self)
}
pub fn from_json(s: &str) -> serde_json::Result<Self> {
serde_json::from_str(s)
}
}
pub trait Producer {
fn produce(&self) -> Report;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_and_roundtrip_a_report() {
let mut r = Report::new("widget", "0.1.0").with_producer("dev-report-self-test");
r.push(CheckResult::pass("compile"));
r.push(CheckResult::fail("unit::math", Severity::Error).with_detail("off by one"));
r.finish();
let json = r.to_json().unwrap();
let parsed = Report::from_json(&json).unwrap();
assert_eq!(parsed.subject, "widget");
assert_eq!(parsed.checks.len(), 2);
assert_eq!(parsed.overall_verdict(), Verdict::Fail);
}
#[test]
fn empty_report_is_skip() {
let r = Report::new("nothing", "0.0.0");
assert_eq!(r.overall_verdict(), Verdict::Skip);
}
}