1use crate::{Details, Outcome, Report, SkipReason};
2
3#[must_use]
8pub fn verdict(reports: &[Report]) -> Outcome {
9 if reports.iter().any(|report| !report.passed()) {
10 Outcome::Fail
11 } else {
12 Outcome::Pass
13 }
14}
15
16#[must_use]
18pub fn markdown(reports: &[Report]) -> String {
19 if reports.is_empty() {
20 return "No reports were found.\n".to_owned();
21 }
22 reports.iter().map(line).collect()
23}
24
25fn line(report: &Report) -> String {
26 let detail = detail(report);
27 let (check, outcome) = (&report.check, report.outcome);
28 if detail.is_empty() {
29 format!("- {check}: {outcome}\n")
30 } else {
31 format!("- {check}: {outcome} ({detail})\n")
32 }
33}
34
35fn detail(report: &Report) -> String {
36 let parts = match &report.details {
37 Some(Details::Tests { summary, items: _ }) => vec![
38 count(summary.passed, "passed", "passed"),
39 count(summary.failed, "failed", "failed"),
40 count(summary.skipped, "skipped", "skipped"),
41 ],
42 Some(Details::Diagnostics { summary, items: _ }) => vec![
43 count(summary.errors, "error", "errors"),
44 count(summary.warnings, "warning", "warnings"),
45 ],
46 Some(Details::Diff { items }) => {
47 vec![count(items.len() as u64, "file differs", "files differ")]
48 }
49 None => vec![plain(report)],
50 };
51 parts.into_iter().flatten().collect::<Vec<_>>().join(", ")
52}
53
54fn plain(report: &Report) -> Option<String> {
55 match (report.skipped, report.outcome, report.status) {
56 (Some(SkipReason::NotApplicable), _, _) => Some("not applicable".to_owned()),
57 (Some(SkipReason::NoOrigin), _, _) => Some("no origin".to_owned()),
58 (None, Outcome::Fail, Some(status)) => Some(format!("status {status}")),
59 (None, _, _) => None,
60 }
61}
62
63fn count(number: u64, one: &str, many: &str) -> Option<String> {
64 match number {
65 0 => None,
66 1 => Some(format!("1 {one}")),
67 _ => Some(format!("{number} {many}")),
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::{markdown, verdict};
74 use crate::{Outcome, Report, SkipReason};
75
76 fn report(json: &str) -> Report {
77 facet_json::from_str(json).expect("report literal")
78 }
79
80 #[test]
81 fn a_failure_decides_the_verdict_and_skips_do_not() {
82 let pass = Report::plain("a", Outcome::Pass, Some(0));
83 let skip = Report::skipped("b", SkipReason::NoOrigin);
84 let fail = Report::plain("c", Outcome::Fail, Some(1));
85 assert_eq!(verdict(&[]), Outcome::Pass);
86 assert_eq!(verdict(&[pass.clone(), skip.clone()]), Outcome::Pass);
87 assert_eq!(verdict(&[pass, skip, fail]), Outcome::Fail);
88 }
89
90 #[test]
91 fn markdown_has_one_line_per_report_in_order() {
92 let reports = [
93 report(
94 r#"{"check":"nextest","outcome":"pass","details":{"kind":"tests","summary":{"passed":305,"failed":0,"skipped":2},"items":[]}}"#,
95 ),
96 report(
97 r#"{"check":"clippy","outcome":"fail","details":{"kind":"diagnostics","summary":{"errors":3,"warnings":0},"items":[]}}"#,
98 ),
99 report(r#"{"check":"semver","outcome":"skip","skipped":"no-origin"}"#),
100 report(r#"{"check":"lint","outcome":"fail","status":42}"#),
101 report(r#"{"check":"docs","outcome":"pass","status":0}"#),
102 ];
103 assert_eq!(
104 markdown(&reports),
105 "- nextest: pass (305 passed, 2 skipped)\n- clippy: fail (3 errors)\n- semver: skip \
106 (no origin)\n- lint: fail (status 42)\n- docs: pass\n"
107 );
108 }
109
110 #[test]
111 fn markdown_says_when_there_are_no_reports() {
112 assert_eq!(markdown(&[]), "No reports were found.\n");
113 }
114}