Skip to content

Commit 6480cf0

Browse files
committed
Improve terminal reporting, add ghmarkdown
1 parent 81f9b6c commit 6480cf0

File tree

11 files changed

+316
-39
lines changed

11 files changed

+316
-39
lines changed

fontspector-checkapi/src/checkresult.rs

+16-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
use serde::Serialize;
1+
use serde::{ser::SerializeStruct, Serialize};
22

33
use crate::{Check, CheckId, Status, StatusCode};
44

5-
#[derive(Debug, Clone, Serialize)]
5+
#[derive(Debug, Clone)]
66
pub struct CheckResult {
77
pub check_id: CheckId,
88
pub check_name: String,
@@ -12,6 +12,20 @@ pub struct CheckResult {
1212
pub subresults: Vec<Status>,
1313
}
1414

15+
impl Serialize for CheckResult {
16+
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
17+
let mut s = serializer.serialize_struct("CheckResult", 7)?;
18+
s.serialize_field("check_id", &self.check_id)?;
19+
s.serialize_field("check_name", &self.check_name)?;
20+
s.serialize_field("check_rationale", &self.check_rationale)?;
21+
s.serialize_field("filename", &self.filename)?;
22+
s.serialize_field("section", &self.section)?;
23+
s.serialize_field("subresults", &self.subresults)?;
24+
s.serialize_field("worst_status", &self.worst_status())?;
25+
s.end()
26+
}
27+
}
28+
1529
impl CheckResult {
1630
pub fn new(
1731
check: &Check,

fontspector-checkapi/src/status.rs

+13
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ pub enum StatusCode {
1919
Error,
2020
}
2121

22+
impl StatusCode {
23+
pub fn all() -> impl Iterator<Item = StatusCode> {
24+
vec![
25+
StatusCode::Error,
26+
StatusCode::Fail,
27+
StatusCode::Warn,
28+
StatusCode::Info,
29+
StatusCode::Skip,
30+
StatusCode::Pass,
31+
]
32+
.into_iter()
33+
}
34+
}
2235
impl std::fmt::Display for StatusCode {
2336
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2437
match *self {

fontspector-cli/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ serde = {version = "1.0.130", features=["derive"] }
2323

2424
# Terminal reporter
2525
termimad = "0.14"
26+
colored = "2.1.0"
2627

2728
# JSON reporter
2829
serde_json = "1.0"

fontspector-cli/src/args.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,13 @@ pub struct Args {
6262
pub skip_network: bool,
6363

6464
/// Write a JSON formatted report to the given filename
65-
#[clap(short, long, help_heading = "Reports")]
65+
#[clap(long, help_heading = "Reports")]
6666
pub json: Option<String>,
6767

68+
/// Write a GitHub-Markdown formatted report to the given filename
69+
#[clap(long, help_heading = "Reports")]
70+
pub ghmarkdown: Option<String>,
71+
6872
/// Input files
6973
pub inputs: Vec<String>,
7074
}

fontspector-cli/src/main.rs

+8-8
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ use indicatif::ParallelProgressIterator;
1111
use profile_googlefonts::GoogleFonts;
1212
use profile_universal::Universal;
1313
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
14-
use reporters::{json::JsonReporter, terminal::TerminalReporter, Reporter, RunResults};
14+
use reporters::{
15+
json::JsonReporter, markdown::MarkdownReporter, terminal::TerminalReporter, Reporter,
16+
RunResults,
17+
};
1518
use serde_json::Map;
1619

1720
/// Filter out checks that don't apply
@@ -150,19 +153,16 @@ fn main() {
150153
if let Some(jsonfile) = args.json.as_ref() {
151154
reporters.push(Box::new(JsonReporter::new(jsonfile)));
152155
}
156+
if let Some(mdfile) = args.ghmarkdown.as_ref() {
157+
reporters.push(Box::new(MarkdownReporter::new(mdfile)));
158+
}
153159

154160
for reporter in reporters {
155161
reporter.report(&results, &args, &registry);
156162
}
157163

158164
if !args.quiet {
159-
// Summary report
160-
let summary = results.summary();
161-
print!("\nSummary:\n ");
162-
for (status, count) in summary.iter() {
163-
print!("{:}: {:} ", status, count);
164-
}
165-
println!();
165+
TerminalReporter::summary_report(results.summary());
166166
}
167167
if worst_status >= args.error_code_on {
168168
std::process::exit(1);
+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
use std::collections::HashMap;
2+
3+
use crate::reporters::{Reporter, RunResults};
4+
use crate::Args;
5+
use fontspector_checkapi::Registry;
6+
use serde_json::json;
7+
use tera::{Context, Tera, Value};
8+
9+
pub(crate) struct MarkdownReporter {
10+
filename: String,
11+
tera: Tera,
12+
}
13+
14+
fn percent_of(v: &Value, options: &HashMap<String, Value>) -> tera::Result<Value> {
15+
let v = v.as_f64().unwrap_or(0.0);
16+
let total = options.get("total").unwrap().as_f64().unwrap_or(100.0);
17+
Ok(format!("{:.0}%", v / total * 100.0).into())
18+
}
19+
20+
fn unindent(v: &Value, _options: &HashMap<String, Value>) -> tera::Result<Value> {
21+
let v = v.as_str().unwrap_or("");
22+
let v = v.trim_start();
23+
Ok(v.into())
24+
}
25+
26+
fn emoticon(v: &Value, _options: &HashMap<String, Value>) -> tera::Result<Value> {
27+
let v = v.as_str().unwrap_or("");
28+
let v = match v {
29+
"ERROR" => "💥",
30+
"FATAL" => "☠",
31+
"FAIL" => "🔥",
32+
"WARN" => "⚠️",
33+
"INFO" => "ℹ️",
34+
"SKIP" => "⏩",
35+
"PASS" => "✅",
36+
"DEBUG" => "🔎",
37+
_ => "❓",
38+
};
39+
Ok(v.into())
40+
}
41+
42+
impl MarkdownReporter {
43+
pub fn new(filename: &str) -> Self {
44+
let mut tera = Tera::new("templates/markdown/*").unwrap_or_else(|e| {
45+
log::error!("Error parsing Markdown templates: {:?}", e);
46+
std::process::exit(1);
47+
});
48+
tera.register_filter("percent", percent_of);
49+
tera.register_filter("unindent", unindent);
50+
tera.register_filter("emoticon", emoticon);
51+
52+
tera.register_tester("omitted", |_value: Option<&Value>, _params: &[Value]| {
53+
// XXX
54+
Ok(false)
55+
});
56+
Self {
57+
tera,
58+
filename: filename.to_string(),
59+
}
60+
}
61+
}
62+
impl Reporter for MarkdownReporter {
63+
fn report(&self, results: &RunResults, args: &Args, registry: &Registry) {
64+
let mut fatal_checks = HashMap::new();
65+
let mut experimental_checks = HashMap::new();
66+
let mut other_checks = HashMap::new();
67+
let all_fonts = "All fonts".to_string();
68+
for result in results.iter() {
69+
let filename = result.filename.as_ref().unwrap_or(&all_fonts).as_str();
70+
if registry.is_experimental(&result.check_id) {
71+
experimental_checks
72+
.entry(filename)
73+
.or_insert_with(Vec::new)
74+
.push(result);
75+
} else if result.is_error() {
76+
fatal_checks
77+
.entry(filename)
78+
.or_insert_with(Vec::new)
79+
.push(result);
80+
} else {
81+
other_checks
82+
.entry(filename)
83+
.or_insert_with(Vec::new)
84+
.push(result);
85+
}
86+
}
87+
let summary = results.summary();
88+
89+
let proposals: HashMap<String, String> = registry
90+
.checks
91+
.iter()
92+
.map(|(k, v)| (k.clone(), v.proposal.to_string()))
93+
.collect();
94+
95+
let val: serde_json::Value = json!({
96+
"version": env!("CARGO_PKG_VERSION"),
97+
"summary": &summary,
98+
"summary_keys": summary.keys().collect::<Vec<_>>(),
99+
// "omitted": vec![],
100+
"fatal_checks": fatal_checks,
101+
"other_checks": other_checks,
102+
"experimental_checks": experimental_checks,
103+
"succinct": args.succinct,
104+
"total": results.len(),
105+
"proposal": proposals,
106+
});
107+
let context = &Context::from_serialize(val).unwrap_or_else(|e| {
108+
log::error!("Error creating Markdown context: {:}", e);
109+
std::process::exit(1);
110+
});
111+
112+
let rendered = self
113+
.tera
114+
.render("main.markdown", context)
115+
.unwrap_or_else(|e| {
116+
log::error!("Error rendering Markdown report: {:?}", e);
117+
std::process::exit(1);
118+
});
119+
std::fs::write(&self.filename, rendered).unwrap_or_else(|e| {
120+
eprintln!(
121+
"Error writing Markdown report to {:}: {:}",
122+
self.filename, e
123+
);
124+
std::process::exit(1);
125+
});
126+
}
127+
}

fontspector-cli/src/reporters/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ impl RunResults {
5757
organised_results
5858
}
5959

60-
fn len(&self) -> usize {
60+
pub fn len(&self) -> usize {
6161
self.results.len()
6262
}
6363
}
+61-27
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
use super::RunResults;
2+
use crate::{reporters::Reporter, Args};
3+
use colored::{ColoredString, Colorize};
14
use fontspector_checkapi::{Registry, StatusCode, Testable};
25
use itertools::Itertools;
6+
use std::{collections::HashMap, path::Path};
37

4-
use crate::{reporters::Reporter, Args};
5-
6-
use super::RunResults;
78
pub(crate) struct TerminalReporter {
89
succinct: bool,
910
}
@@ -14,6 +15,21 @@ impl TerminalReporter {
1415
}
1516
}
1617

18+
fn colored_status(c: StatusCode, s: Option<&str>) -> ColoredString {
19+
let string = match s {
20+
Some(s) => s.to_string(),
21+
None => c.to_string(),
22+
};
23+
match c {
24+
StatusCode::Error => string.on_red(),
25+
StatusCode::Fail => string.red(),
26+
StatusCode::Warn => string.yellow(),
27+
StatusCode::Info => string.cyan(),
28+
StatusCode::Skip => string.blue(),
29+
StatusCode::Pass => string.green(),
30+
}
31+
}
32+
1733
impl Reporter for TerminalReporter {
1834
fn report(&self, results: &RunResults, args: &Args, registry: &Registry) {
1935
let organised_results = results.organize();
@@ -25,33 +41,41 @@ impl Reporter for TerminalReporter {
2541
for (sectionname, results) in sectionresults.iter() {
2642
let mut sectionheading_done = false;
2743
for result in results.iter() {
44+
let subresults = result
45+
.subresults
46+
.iter()
47+
.filter(|c| c.severity >= args.loglevel)
48+
.collect::<Vec<_>>();
49+
if subresults.is_empty() {
50+
continue;
51+
}
52+
2853
if self.succinct {
2954
println!(
30-
"{:}: {:} {:}",
31-
filename,
32-
result.check_id,
33-
result
34-
.subresults
55+
"{:}: {:} {:} [{}]",
56+
Path::new(filename).file_name().unwrap().to_string_lossy(),
57+
result.check_id.bright_cyan(),
58+
colored_status(result.worst_status(), None),
59+
subresults
3560
.iter()
36-
.flat_map(|r| r.code.as_ref())
37-
.join(", ")
61+
.map(|r| colored_status(
62+
r.severity,
63+
r.code.as_ref().map(|x| x.as_str())
64+
))
65+
.join(" ")
3866
);
3967
continue;
4068
}
4169

42-
for subresult in result
43-
.subresults
44-
.iter()
45-
.filter(|c| c.severity >= args.loglevel)
46-
{
47-
if !fileheading_done {
48-
println!("Testing: {:}", filename);
49-
fileheading_done = true;
50-
}
51-
if !sectionheading_done {
52-
println!(" Section: {:}\n", sectionname);
53-
sectionheading_done = true;
54-
}
70+
if !fileheading_done {
71+
println!("Testing: {:}", filename);
72+
fileheading_done = true;
73+
}
74+
if !sectionheading_done {
75+
println!(" Section: {:}\n", sectionname);
76+
sectionheading_done = true;
77+
}
78+
for subresult in subresults {
5579
println!(">> {:}", result.check_id);
5680
if args.verbose > 1 {
5781
println!(" {:}", result.check_name);
@@ -61,10 +85,6 @@ impl Reporter for TerminalReporter {
6185
));
6286
}
6387
termimad::print_inline(&format!("{:}\n", subresult));
64-
if subresult.severity != StatusCode::Fail {
65-
println!();
66-
continue;
67-
}
6888
#[allow(clippy::unwrap_used)]
6989
// This is a genuine can't-happen. We put it in the hashmap earlier!
7090
let check = registry.checks.get(&result.check_id).unwrap();
@@ -86,3 +106,17 @@ impl Reporter for TerminalReporter {
86106
}
87107
}
88108
}
109+
110+
impl TerminalReporter {
111+
pub fn summary_report(summary: HashMap<StatusCode, i32>) {
112+
print!("\nSummary:\n ");
113+
for code in StatusCode::all() {
114+
print!(
115+
"{:}: {:} ",
116+
colored_status(code, None),
117+
summary.get(&code).unwrap_or(&0)
118+
);
119+
}
120+
println!();
121+
}
122+
}

templates/markdown/check.markdown

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<details>
2+
<summary>{{check.worst_status | emoticon}} <b>{{check.worst_status}}</b> {{check.check_name}} ({{check.check_id}})</summary>
3+
<div>
4+
5+
{% if not succinct %}
6+
{% for line in check.check_rationale | split(pat="\n") %}> {{line | unindent | replace(from="\n", to="") }}
7+
{% endfor %}
8+
{% endif %}
9+
10+
{% if not succinct and proposals[check.check_id]%}
11+
Original proposal: {{proposals[check.check_id]}}
12+
{% endif %}
13+
14+
{% for result in check.subresults |sort(attribute="severity") %}
15+
{% if not result is omitted %}
16+
17+
- {{result.severity | emoticon }} **{{result.severity}}** {% if result is containing("message") %}{{result.message}}{% endif %} {%if result.code%}[code: {{result.code}}]{%endif%}
18+
{% endif %}
19+
{% endfor %}
20+
21+
</div>
22+
</details>

0 commit comments

Comments
 (0)