Skip to content

Commit c6dd12d

Browse files
authored
Merge pull request #316 from kirtchev-adacore/wip/issue-315-add-libtest-json-emitter
Add libtest JSON emitter
2 parents e3d2173 + 430a334 commit c6dd12d

File tree

14 files changed

+427
-26
lines changed

14 files changed

+427
-26
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
* support for JSON output via argument `--format=json`
13+
1214
### Fixed
1315

1416
* missing lines in diff output

src/config/args.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ pub struct Args {
4040
/// Possible choices for styling the output.
4141
#[derive(Debug, Copy, Clone, Default)]
4242
pub enum Format {
43+
/// JSON format
44+
JSON,
4345
/// Print one line per test
4446
#[default]
4547
Pretty,
@@ -76,8 +78,9 @@ impl Args {
7678
// We ignore this flag for now.
7779
} else if let Some(format) = parse_value("--format", &arg, &mut iter)? {
7880
self.format = match &*format {
79-
"terse" => Format::Terse,
81+
"json" => Format::JSON,
8082
"pretty" => Format::Pretty,
83+
"terse" => Format::Terse,
8184
_ => bail!("unsupported format `{format}`"),
8285
};
8386
} else if let Some(skip) = parse_value("--skip", &arg, &mut iter)? {

src/lib.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,18 +81,16 @@ pub fn run_tests(mut config: Config) -> Result<()> {
8181
#[cfg(feature = "gha")]
8282
let name = display(&config.root_dir);
8383

84-
let text = match args.format {
85-
Format::Terse => status_emitter::Text::quiet(),
86-
Format::Pretty => status_emitter::Text::verbose(),
87-
};
84+
let emitter: Box<dyn StatusEmitter> = args.format.into();
85+
8886
config.with_args(&args);
8987

9088
run_tests_generic(
9189
vec![config],
9290
default_file_filter,
9391
default_per_file_config,
9492
(
95-
text,
93+
emitter,
9694
#[cfg(feature = "gha")]
9795
status_emitter::Gha::<true> { name },
9896
),
@@ -180,7 +178,7 @@ pub fn run_tests_generic(
180178
mut configs: Vec<Config>,
181179
file_filter: impl Fn(&Path, &Config) -> Option<bool> + Sync,
182180
per_file_config: impl Copy + Fn(&mut Config, &Spanned<Vec<u8>>) + Send + Sync + 'static,
183-
status_emitter: impl StatusEmitter + Send,
181+
status_emitter: impl StatusEmitter,
184182
) -> Result<()> {
185183
if nextest::emulate(&mut configs) {
186184
return Ok(());

src/status_emitter.rs

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,42 @@
11
//! Various schemes for reporting messages during testing or after testing is done.
2-
3-
use crate::{test_result::TestResult, Errors};
2+
//!
3+
//! The testing framework employs the implementations of the various emitter traits
4+
//! as follows:
5+
//!
6+
//! The framework first creates an instance of a `StatusEmitter`.
7+
//!
8+
//! The framework then searches for tests in its perview, and if it finds one, it
9+
//! calls `StatusEmitter::register_test()` to obtain a `TestStatus` for that test.
10+
//! The tests are then executed in an asynchonous manner.
11+
//!
12+
//! Once a single test finish executing, the framework calls `TestStatus::done()`.
13+
//!
14+
//! Once all tests finish executing, the framework calls `StatusEmitter::finalize()`
15+
//! to obtain a Summary.
16+
//!
17+
//! For each failed test, the framework calls both `TestStatus::failed_test()` and
18+
//! `Summary::test_failure()`.
19+
20+
use crate::{test_result::TestResult, Errors, Format};
421

522
use std::{
23+
boxed::Box,
624
fmt::Debug,
725
panic::RefUnwindSafe,
826
path::{Path, PathBuf},
927
};
10-
pub use text::*;
1128
pub mod debug;
12-
mod text;
1329
#[cfg(feature = "gha")]
1430
pub use gha::*;
1531
#[cfg(feature = "gha")]
1632
mod gha;
33+
pub use json::*;
34+
mod json;
35+
pub use text::*;
36+
mod text;
1737

1838
/// A generic way to handle the output of this crate.
19-
pub trait StatusEmitter: Sync + RefUnwindSafe {
39+
pub trait StatusEmitter: Send + Sync + RefUnwindSafe {
2040
/// Invoked the moment we know a test will later be run.
2141
/// Useful for progress bars and such.
2242
fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus + 'static>;
@@ -33,6 +53,16 @@ pub trait StatusEmitter: Sync + RefUnwindSafe {
3353
) -> Box<dyn Summary>;
3454
}
3555

56+
impl From<Format> for Box<dyn StatusEmitter> {
57+
fn from(format: Format) -> Box<dyn StatusEmitter> {
58+
match format {
59+
Format::JSON => Box::new(JSON::new()),
60+
Format::Pretty => Box::new(Text::verbose()),
61+
Format::Terse => Box::new(Text::quiet()),
62+
}
63+
}
64+
}
65+
3666
/// Some configuration options for revisions
3767
#[derive(Debug, Clone, Copy)]
3868
pub enum RevisionStyle {
@@ -52,7 +82,7 @@ pub trait TestStatus: Send + Sync + RefUnwindSafe {
5282
fn for_path(&self, path: &Path) -> Box<dyn TestStatus>;
5383

5484
/// Invoked before each failed test prints its errors along with a drop guard that can
55-
/// gets invoked afterwards.
85+
/// get invoked afterwards.
5686
fn failed_test<'a>(
5787
&'a self,
5888
cmd: &'a str,

src/status_emitter/json.rs

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
use super::RevisionStyle;
2+
use super::StatusEmitter;
3+
use super::Summary;
4+
use super::TestStatus;
5+
use crate::test_result::TestResult;
6+
use crate::TestOk;
7+
8+
use std::boxed::Box;
9+
use std::fmt::Debug;
10+
use std::path::{Path, PathBuf};
11+
12+
use bstr::ByteSlice;
13+
14+
// MAINTENANCE REGION START
15+
16+
// When integrating with a new libtest version, update all emit_xxx functions.
17+
18+
fn emit_suite_end(failed: usize, filtered_out: usize, ignored: usize, passed: usize, status: &str) {
19+
// Adapted from test::formatters::json::write_run_finish().
20+
println!(
21+
r#"{{ "type": "suite", "event": "{status}", "passed": {passed}, "failed": {failed}, "ignored": {ignored}, "measured": 0, "filtered_out": {filtered_out} }}"#
22+
);
23+
}
24+
25+
fn emit_suite_start() {
26+
// Adapted from test::formatters::json::write_run_start().
27+
println!(r#"{{ "type": "suite", "event": "started" }}"#);
28+
}
29+
30+
fn emit_test_end(name: &String, revision: &String, path: &Path, status: &str, diags: &str) {
31+
let displayed_path = path.display();
32+
let stdout = if diags.is_empty() {
33+
String::new()
34+
} else {
35+
let triaged_diags = serde_json::to_string(diags).unwrap();
36+
format!(r#", "stdout": {triaged_diags}"#)
37+
};
38+
39+
// Adapted from test::formatters::json::write_event().
40+
println!(
41+
r#"{{ "type": "test", "event": "{status}", "name": "{name} ({revision}) - {displayed_path}"{stdout} }}"#
42+
);
43+
}
44+
45+
fn emit_test_start(name: &String, revision: &String, path: &Path) {
46+
let displayed_path = path.display();
47+
48+
// Adapted from test::formatters::json::write_test_start().
49+
println!(
50+
r#"{{ "type": "test", "event": "started", "name": "{name} ({revision}) - {displayed_path}" }}"#
51+
);
52+
}
53+
54+
// MAINTENANCE REGION END
55+
56+
/// A JSON output emitter.
57+
#[derive(Clone)]
58+
pub struct JSON {}
59+
60+
impl JSON {
61+
/// Create a new instance of a JSON output emitter.
62+
pub fn new() -> Self {
63+
emit_suite_start();
64+
65+
JSON {}
66+
}
67+
}
68+
69+
impl Default for JSON {
70+
fn default() -> Self {
71+
Self::new()
72+
}
73+
}
74+
75+
impl StatusEmitter for JSON {
76+
/// Create a report about the entire test run at the end.
77+
fn finalize(
78+
&self,
79+
failed: usize,
80+
succeeded: usize,
81+
ignored: usize,
82+
filtered: usize,
83+
aborted: bool,
84+
) -> Box<dyn Summary> {
85+
let status = if aborted || failed > 0 {
86+
"failed"
87+
} else {
88+
"ok"
89+
};
90+
91+
emit_suite_end(failed, filtered, ignored, succeeded, status);
92+
93+
Box::new(())
94+
}
95+
96+
/// Invoked the moment we know a test will later be run.
97+
/// Emits a JSON start event.
98+
fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus + 'static> {
99+
let name = path.to_str().unwrap().to_string();
100+
let revision = String::new();
101+
102+
emit_test_start(&name, &revision, &path);
103+
104+
Box::new(JSONStatus {
105+
name,
106+
path,
107+
revision: String::new(),
108+
style: RevisionStyle::Show,
109+
})
110+
}
111+
}
112+
113+
/// Information about a specific test run.
114+
pub struct JSONStatus {
115+
name: String,
116+
path: PathBuf,
117+
revision: String,
118+
style: RevisionStyle,
119+
}
120+
121+
impl TestStatus for JSONStatus {
122+
/// A test has finished, handle the result immediately.
123+
fn done(&self, result: &TestResult, aborted: bool) {
124+
let status = if aborted {
125+
"timeout"
126+
} else {
127+
match result {
128+
Ok(TestOk::Ignored) => "ignored",
129+
Ok(TestOk::Ok) => "ok",
130+
Err(_) => "failed",
131+
}
132+
};
133+
let diags = if let Err(errored) = result {
134+
let command = errored.command.as_str();
135+
let stdout = errored.stderr.to_str_lossy();
136+
let stderr = errored.stdout.to_str_lossy();
137+
138+
format!(r#"command: <{command}> stdout: <{stdout}> stderr: <{stderr}>"#)
139+
} else {
140+
String::new()
141+
};
142+
143+
emit_test_end(&self.name, &self.revision, self.path(), status, &diags);
144+
}
145+
146+
/// Invoked before each failed test prints its errors along with a drop guard that can
147+
/// get invoked afterwards.
148+
fn failed_test<'a>(
149+
&'a self,
150+
_cmd: &'a str,
151+
_stderr: &'a [u8],
152+
_stdout: &'a [u8],
153+
) -> Box<dyn Debug + 'a> {
154+
Box::new(())
155+
}
156+
157+
/// Create a copy of this test for a new path.
158+
fn for_path(&self, path: &Path) -> Box<dyn TestStatus> {
159+
let status = JSONStatus {
160+
name: self.name.clone(),
161+
path: path.to_path_buf(),
162+
revision: self.revision.clone(),
163+
style: self.style,
164+
};
165+
Box::new(status)
166+
}
167+
168+
/// Create a copy of this test for a new revision.
169+
fn for_revision(&self, revision: &str, style: RevisionStyle) -> Box<dyn TestStatus> {
170+
let status = JSONStatus {
171+
name: self.name.clone(),
172+
path: self.path.clone(),
173+
revision: revision.to_owned(),
174+
style,
175+
};
176+
Box::new(status)
177+
}
178+
179+
/// The path of the test file.
180+
fn path(&self) -> &Path {
181+
&self.path
182+
}
183+
184+
/// The revision, usually an empty string.
185+
fn revision(&self) -> &str {
186+
&self.revision
187+
}
188+
}

src/status_emitter/text.rs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ use crate::test_result::TestOk;
1111
use crate::test_result::TestResult;
1212
use crate::Error;
1313
use crate::Errors;
14-
use crate::Format;
1514
use annotate_snippets::Renderer;
1615
use annotate_snippets::Snippet;
1716
use colored::Colorize;
@@ -341,15 +340,6 @@ impl Text {
341340
}
342341
}
343342

344-
impl From<Format> for Text {
345-
fn from(format: Format) -> Self {
346-
match format {
347-
Format::Terse => Text::quiet(),
348-
Format::Pretty => Text::verbose(),
349-
}
350-
}
351-
}
352-
353343
struct TextTest {
354344
text: Text,
355345
#[cfg(feature = "indicatif")]

tests/integration.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ fn main() -> Result<()> {
9797
config.stdout_filter(r#"exit status: 0xc0000409"#, "signal: 6 (SIGABRT)");
9898
config.filter("\"--target=[^\"]+\"", "");
9999

100-
let text = ui_test::status_emitter::Text::from(args.format);
100+
let emitter: Box<dyn ui_test::status_emitter::StatusEmitter> = args.format.into();
101101

102102
let mut pass_config = config.clone();
103103
pass_config.comment_defaults.base().exit_status = Some(Spanned::dummy(0)).into();
@@ -139,7 +139,7 @@ fn main() -> Result<()> {
139139
},
140140
|_, _| {},
141141
(
142-
text,
142+
emitter,
143143
#[cfg(feature = "gha")]
144144
ui_test::status_emitter::Gha::<true> {
145145
name: "integration tests".into(),

tests/integrations/basic-fail/Cargo.stderr

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
Error: tests failed
22

3+
Location:
4+
$DIR/src/lib.rs:LL:CC
5+
error: test failed, to rerun pass `--test json`
6+
7+
Caused by:
8+
process didn't exit successfully: `$DIR/target/ui/1/tests/integrations/basic-fail/debug/deps/json-HASH` (exit status: 1)
9+
Error: tests failed
10+
311
Location:
412
$DIR/src/lib.rs:LL:CC
513
error: test failed, to rerun pass `--test ui_tests`
@@ -32,7 +40,8 @@ error: test failed, to rerun pass `--test ui_tests_invalid_program2`
3240

3341
Caused by:
3442
process didn't exit successfully: `$DIR/target/ui/1/tests/integrations/basic-fail/debug/deps/ui_tests_invalid_program2-HASH` (exit status: 1)
35-
error: 4 targets failed:
43+
error: 5 targets failed:
44+
`--test json`
3645
`--test ui_tests`
3746
`--test ui_tests_diff_only`
3847
`--test ui_tests_invalid_program`

0 commit comments

Comments
 (0)