use super::RevisionStyle;
use super::StatusEmitter;
use super::Summary;
use super::TestStatus;
use crate::test_result::TestResult;
use crate::TestOk;
use std::boxed::Box;
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use bstr::ByteSlice;
fn suite_end_event(
failed: usize,
filtered_out: usize,
ignored: usize,
passed: usize,
status: &str,
) -> String {
format!(
r#"{{ "type": "suite", "event": "{status}", "passed": {passed}, "failed": {failed}, "ignored": {ignored}, "measured": 0, "filtered_out": {filtered_out} }}"#
)
}
fn suite_start_event() -> String {
String::from(r#"{ "type": "suite", "event": "started" }"#)
}
fn test_end_event(name: &str, revision: &str, path: &Path, status: &str, diags: &str) -> String {
let name_attribute = make_name_attribute(name, revision, path);
let stdout_attribute = make_stdout_attribute(diags);
format!(r#"{{ "type": "test", "event": "{status}"{name_attribute}{stdout_attribute} }}"#)
}
fn test_start_event(name: &str, revision: &str, path: &Path) -> String {
let name_attribute = make_name_attribute(name, revision, path);
format!(r#"{{ "type": "test", "event": "started"{name_attribute} }}"#)
}
fn make_name_attribute(name: &str, revision: &str, path: &Path) -> String {
let path_display = path.display();
let escaped_value =
serde_json::to_string(&format!("{name} ({revision}) - {path_display}")).unwrap();
format!(r#", "name": {escaped_value}"#)
}
fn make_stdout_attribute(diags: &str) -> String {
if diags.is_empty() {
String::new()
} else {
let escaped_diags = serde_json::to_string(diags).unwrap();
format!(r#", "stdout": {escaped_diags}"#)
}
}
#[derive(Clone)]
pub struct JSON {}
impl JSON {
pub fn new() -> Self {
println!("{}", suite_start_event());
JSON {}
}
}
impl Default for JSON {
fn default() -> Self {
Self::new()
}
}
impl StatusEmitter for JSON {
fn finalize(
&self,
failed: usize,
succeeded: usize,
ignored: usize,
filtered: usize,
aborted: bool,
) -> Box<dyn Summary> {
let status = if aborted || failed > 0 {
"failed"
} else {
"ok"
};
println!(
"{}",
suite_end_event(failed, filtered, ignored, succeeded, status)
);
Box::new(())
}
fn register_test(&self, path: PathBuf) -> Box<dyn TestStatus + 'static> {
let name = path.to_str().unwrap().to_string();
let revision = String::new();
println!("{}", test_start_event(&name, &revision, &path));
Box::new(JSONStatus {
name,
path,
revision: String::new(),
style: RevisionStyle::Show,
})
}
}
pub struct JSONStatus {
name: String,
path: PathBuf,
revision: String,
style: RevisionStyle,
}
impl TestStatus for JSONStatus {
fn done(&self, result: &TestResult, aborted: bool) {
let status = if aborted {
"timeout"
} else {
match result {
Ok(TestOk::Ignored) => "ignored",
Ok(TestOk::Ok) => "ok",
Err(_) => "failed",
}
};
let diags = if let Err(errored) = result {
let command = errored.command.as_str();
let stdout = errored.stderr.to_str_lossy();
let stderr = errored.stdout.to_str_lossy();
format!(r#"command: <{command}> stdout: <{stdout}> stderr: <{stderr}>"#)
} else {
String::new()
};
println!(
"{}",
test_end_event(&self.name, &self.revision, self.path(), status, &diags)
);
}
fn failed_test<'a>(
&'a self,
_cmd: &'a str,
_stderr: &'a [u8],
_stdout: &'a [u8],
) -> Box<dyn Debug + 'a> {
Box::new(())
}
fn for_path(&self, path: &Path) -> Box<dyn TestStatus> {
let status = JSONStatus {
name: self.name.clone(),
path: path.to_path_buf(),
revision: self.revision.clone(),
style: self.style,
};
Box::new(status)
}
fn for_revision(&self, revision: &str, style: RevisionStyle) -> Box<dyn TestStatus> {
let status = JSONStatus {
name: self.name.clone(),
path: self.path.clone(),
revision: revision.to_owned(),
style,
};
Box::new(status)
}
fn path(&self) -> &Path {
&self.path
}
fn revision(&self) -> &str {
&self.revision
}
}
#[test]
fn suite_end_event_constructs_event() {
assert_eq!(
suite_end_event(
12, 34, 56, 78, "status"
),
r#"{ "type": "suite", "event": "status", "passed": 78, "failed": 12, "ignored": 56, "measured": 0, "filtered_out": 34 }"#
);
}
#[test]
fn suite_start_event_constructs_event() {
assert_eq!(
suite_start_event(),
r#"{ "type": "suite", "event": "started" }"#
);
}
#[test]
fn test_end_event_constructs_event() {
assert_eq!(
test_end_event("name", "revision", Path::new("aaa/bbb"), "status", "diags"),
r#"{ "type": "test", "event": "status", "name": "name (revision) - aaa/bbb", "stdout": "diags" }"#
);
}
#[test]
fn test_end_event_constructs_event_with_escapes() {
assert_eq!(
test_end_event(
r#"aaa\bbb"#,
r#"ccc ddd\eee"#,
Path::new(r#"fff\ggg"#),
"status",
r#""rustc" "--error-format=json""#
),
r#"{ "type": "test", "event": "status", "name": "aaa\\bbb (ccc ddd\\eee) - fff\\ggg", "stdout": "\"rustc\" \"--error-format=json\"" }"#
);
}
#[test]
fn test_end_event_constructs_event_without_revision() {
assert_eq!(
test_end_event("name", "", Path::new("aaa/bbb"), "status", "diags"),
r#"{ "type": "test", "event": "status", "name": "name () - aaa/bbb", "stdout": "diags" }"#
);
}
#[test]
fn test_end_event_constructs_event_without_stdout() {
assert_eq!(
test_end_event("name", "revision", Path::new("aaa/bbb"), "status", ""),
r#"{ "type": "test", "event": "status", "name": "name (revision) - aaa/bbb" }"#
);
}
#[test]
fn test_start_event_constructs_event() {
assert_eq!(
test_start_event("name", "revision", Path::new("aaa/bbb")),
r#"{ "type": "test", "event": "started", "name": "name (revision) - aaa/bbb" }"#
);
}
#[test]
fn test_start_event_constructs_event_with_escapes() {
assert_eq!(
test_start_event(r#"aaa\bbb"#, r#"ccc ddd\eee"#, Path::new(r#"fff\ggg"#)),
r#"{ "type": "test", "event": "started", "name": "aaa\\bbb (ccc ddd\\eee) - fff\\ggg" }"#
);
}