From 76e3f578314576818e411c7b3a535f5919a2ce07 Mon Sep 17 00:00:00 2001 From: Paul Wagener Date: Mon, 12 Jun 2023 15:00:14 +0200 Subject: [PATCH] Add json output format Closes #1 --- src/args.rs | 6 ++++- src/lib.rs | 2 +- src/printer.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++- tests/common/mod.rs | 5 ++++ tests/mixed_bag.rs | 53 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 124 insertions(+), 3 deletions(-) diff --git a/src/args.rs b/src/args.rs index 26cc26b..dfe61cd 100644 --- a/src/args.rs +++ b/src/args.rs @@ -112,7 +112,8 @@ pub struct Arguments { value_name = "pretty|terse|json", help = "Configure formatting of output: \n\ - pretty = Print verbose output\n\ - - terse = Display one character per test\n", + - terse = Display one character per test\n\ + - json = Print json events\n", )] pub format: Option, @@ -176,6 +177,9 @@ pub enum FormatSetting { /// One character per test. Usefull for test suites with many tests. Terse, + + /// Json output + Json, } impl Default for FormatSetting { diff --git a/src/lib.rs b/src/lib.rs index b80e471..032813f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -451,7 +451,7 @@ pub fn run(args: &Arguments, mut tests: Vec) -> Conclusion { let mut failed_tests = Vec::new(); let mut handle_outcome = |outcome: Outcome, test: TestInfo, printer: &mut Printer| { - printer.print_single_outcome(&outcome); + printer.print_single_outcome(&test, &outcome); // Handle outcome match outcome { diff --git a/src/printer.rs b/src/printer.rs index d0766f0..a0d858d 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -92,6 +92,12 @@ impl Printer { writeln!(self.out).unwrap(); writeln!(self.out, "running {} test{}", num_tests, plural_s).unwrap(); } + FormatSetting::Json => writeln!( + self.out, + "{{ \"type\": \"suite\", \"event\": \"started\", \"test_count\": {} }}", + num_tests + ) + .unwrap(), } } @@ -121,12 +127,20 @@ impl Printer { // In terse mode, nothing is printed before the job. Only // `print_single_outcome` prints one character. } + FormatSetting::Json => { + writeln!( + self.out, + "{{ \"type\": \"test\", \"event\": \"started\", \"name\": \"{}\" }}", + name + ) + .unwrap(); + } } } /// Prints the outcome of a single tests. `ok` or `FAILED` in pretty mode /// and `.` or `F` in terse mode. - pub(crate) fn print_single_outcome(&mut self, outcome: &Outcome) { + pub(crate) fn print_single_outcome(&mut self, info: &TestInfo, outcome: &Outcome) { match self.format { FormatSetting::Pretty => { self.print_outcome_pretty(outcome); @@ -150,6 +164,34 @@ impl Printer { write!(self.out, "{}", c).unwrap(); self.out.reset().unwrap(); } + FormatSetting::Json => { + if let Outcome::Measured(Measurement { avg, variance }) = outcome { + writeln!( + self.out, + r#"{{ "type": "bench", "name": "{}", "median": {}, "deviation": {} }}"#, + info.name, avg, variance + ) + .unwrap(); + } else { + writeln!( + self.out, + r#"{{ "type": "test", "name": "{}", "event": "{}"{} }}"#, + info.name, + match outcome { + Outcome::Passed => "ok", + Outcome::Failed(_) => "failed", + Outcome::Ignored => "ignored", + Outcome::Measured(_) => unreachable!(), + }, + match outcome { + Outcome::Failed(Failed { msg: Some(msg) }) => + format!(r#", "stdout": "Error: \"{}\"\n""#, msg.escape_default()), + _ => "".into(), + } + ) + .unwrap(); + } + } } } @@ -179,6 +221,20 @@ impl Printer { ).unwrap(); writeln!(self.out).unwrap(); } + FormatSetting::Json => { + writeln!( + self.out, + "{{ \"type\": \"suite\", \"event\": \"{}\", \"passed\": {}, \"failed\": {}, \"ignored\": {}, \"measured\": {}, \"filtered_out\": {}, \"exec_time\": {} }}", + if conclusion.num_failed > 0 { "failed" } else { "ok" }, + conclusion.num_passed, + conclusion.num_failed, + conclusion.num_ignored, + conclusion.num_measured, + conclusion.num_filtered_out, + execution_time.as_secs_f64() + ) + .unwrap(); + } } } @@ -221,6 +277,9 @@ impl Printer { /// Prints a list of failed tests with their messages. This is only called /// if there were any failures. pub(crate) fn print_failures(&mut self, fails: &[(TestInfo, Option)]) { + if self.format == FormatSetting::Json { + return; + } writeln!(self.out).unwrap(); writeln!(self.out, "failures:").unwrap(); writeln!(self.out).unwrap(); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 33bc417..c2c16a2 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -97,6 +97,11 @@ macro_rules! assert_log { } } + if let Some(pos) = actual.rfind("\"exec_time\":") { + actual.truncate(pos); + actual.push_str("\"exec_time\": 0.000000000 }"); + } + assert_eq!(actual, expected); }; } diff --git a/tests/mixed_bag.rs b/tests/mixed_bag.rs index c27557a..3851378 100644 --- a/tests/mixed_bag.rs +++ b/tests/mixed_bag.rs @@ -597,3 +597,56 @@ fn terse_output() { finished in 0.00s "); } + +#[test] +fn json_output() { + let (c, out) = do_run(args(["--format", "json", "--test-threads", "1"]), tests()); + assert_eq!( + c, + Conclusion { + num_filtered_out: 0, + num_passed: 4, + num_failed: 4, + num_ignored: 8, + num_measured: 0, + } + ); + + assert_log!( + out, + r#"{ "type": "suite", "event": "started", "test_count": 16 } +{ "type": "test", "event": "started", "name": "cat" } +{ "type": "test", "name": "cat", "event": "ok" } +{ "type": "test", "event": "started", "name": "dog" } +{ "type": "test", "name": "dog", "event": "failed", "stdout": "Error: \"was not a good boy\"\n" } +{ "type": "test", "event": "started", "name": "fox" } +{ "type": "test", "name": "fox", "event": "ok" } +{ "type": "test", "event": "started", "name": "bunny" } +{ "type": "test", "name": "bunny", "event": "failed", "stdout": "Error: \"jumped too high\"\n" } +{ "type": "test", "event": "started", "name": "frog" } +{ "type": "test", "name": "frog", "event": "ignored" } +{ "type": "test", "event": "started", "name": "owl" } +{ "type": "test", "name": "owl", "event": "ignored" } +{ "type": "test", "event": "started", "name": "fly" } +{ "type": "test", "name": "fly", "event": "ignored" } +{ "type": "test", "event": "started", "name": "bear" } +{ "type": "test", "name": "bear", "event": "ignored" } +{ "type": "test", "event": "started", "name": "red" } +{ "type": "test", "name": "red", "event": "ok" } +{ "type": "test", "event": "started", "name": "blue" } +{ "type": "test", "name": "blue", "event": "failed", "stdout": "Error: \"sky fell down\"\n" } +{ "type": "test", "event": "started", "name": "yellow" } +{ "type": "test", "name": "yellow", "event": "ok" } +{ "type": "test", "event": "started", "name": "green" } +{ "type": "test", "name": "green", "event": "failed", "stdout": "Error: \"was poisoned\"\n" } +{ "type": "test", "event": "started", "name": "purple" } +{ "type": "test", "name": "purple", "event": "ignored" } +{ "type": "test", "event": "started", "name": "cyan" } +{ "type": "test", "name": "cyan", "event": "ignored" } +{ "type": "test", "event": "started", "name": "orange" } +{ "type": "test", "name": "orange", "event": "ignored" } +{ "type": "test", "event": "started", "name": "pink" } +{ "type": "test", "name": "pink", "event": "ignored" } +{ "type": "suite", "event": "failed", "passed": 4, "failed": 4, "ignored": 8, "measured": 0, "filtered_out": 0, "exec_time": 0.000000000 }"# + ); +}