use anyhow::anyhow;
use serde;
use serde::{Deserialize, Serialize};
use serde_json::error::Category;
#[derive(Deserialize, Debug, PartialEq)]
#[serde(tag = "type")]
pub(crate) enum Test {
#[serde(rename = "suite")]
Suite {
event: String,
test_count: Option<usize>,
passed: Option<usize>,
failed: Option<usize>,
errors: Option<usize>,
allowed_fail: Option<usize>,
ignored: Option<usize>,
measured: Option<usize>,
filtered_out: Option<usize>,
exec_time: Option<f64>,
},
#[serde(rename = "test")]
Test {
event: String,
name: String,
stdout: Option<String>,
exec_time: Option<f64>,
},
}
#[derive(Serialize, Debug, PartialEq, Clone)]
pub(crate) struct ParsedTestSuite {
#[serde(rename = "suite_name")]
pub(crate) suite_name: String,
#[serde(rename = "tests")]
pub(crate) test_count: usize,
pub(crate) passed: usize,
#[serde(rename = "failures")]
pub(crate) failed: usize,
#[serde(rename = "errors")]
pub(crate) errors: usize,
pub(crate) allowed_fail: usize,
#[serde(rename = "skipped")]
pub(crate) ignored: usize,
pub(crate) measured: usize,
pub(crate) filtered_out: usize,
pub(crate) exec_time: f64,
pub(crate) tests: Vec<ParsedTest>,
}
#[derive(Serialize, Debug, PartialEq, Clone)]
pub(crate) struct ParsedTest {
pub(crate) full_name: String,
#[serde(rename = "name")]
pub(crate) name: String,
#[serde(rename = "module")]
pub(crate) module: Option<String>,
pub(crate) exec_time: Option<f64>,
pub(crate) status: TestStatus,
pub(crate) std_out: Option<String>,
}
#[derive(Serialize, Debug, Copy, Clone, Eq, PartialEq)]
pub(crate) enum TestStatus {
Ok,
Failed,
Ignored,
}
pub(crate) trait TestParser {
type Error: Into<anyhow::Error>;
fn multi_line(&self) -> bool;
fn parse(&mut self, text: &str) -> Result<Option<ParsedTestSuite>, Self::Error>;
fn reset(&mut self);
}
impl Test {
#[cfg(test)]
pub(crate) fn new_suite_started(event: String, test_count: usize) -> Test {
Test::Suite {
event,
test_count: Some(test_count),
passed: None,
failed: None,
errors: None,
allowed_fail: None,
ignored: None,
measured: None,
filtered_out: None,
exec_time: None,
}
}
#[cfg(test)]
pub(crate) fn new_suite_ok(
event: String,
passed: usize,
failed: usize,
errors: Option<usize>,
allowed_fail: usize,
ignored: usize,
measured: usize,
filtered_out: usize,
exec_time: f64,
) -> Test {
Test::Suite {
event,
test_count: None,
passed: Some(passed),
failed: Some(failed),
errors,
allowed_fail: Some(allowed_fail),
ignored: Some(ignored),
measured: Some(measured),
filtered_out: Some(filtered_out),
exec_time: Some(exec_time),
}
}
}
pub(crate) struct CargoTestParserOptions {
pub(crate) ignore_parse_errors: bool,
}
impl Default for CargoTestParserOptions {
fn default() -> Self {
Self {
ignore_parse_errors: false,
}
}
}
pub(crate) struct CargoTestParser {
opts: CargoTestParserOptions,
header: Option<Test>,
bottom: Option<Test>,
tests: Vec<ParsedTest>,
}
impl CargoTestParser {
pub(crate) fn new(opts: CargoTestParserOptions) -> CargoTestParser {
CargoTestParser {
opts,
header: None,
bottom: None,
tests: vec![],
}
}
}
impl TestParser for CargoTestParser {
type Error = anyhow::Error;
fn multi_line(&self) -> bool {
true
}
fn parse(&mut self, text: &str) -> Result<Option<ParsedTestSuite>, Self::Error> {
let test = parse_test(text, self.opts.ignore_parse_errors)?;
let test = if let Some(t) = test {
t
} else {
return Ok(None);
};
match test {
Test::Suite { ref event, .. } => {
if event.to_string() == "started".to_string() {
self.header = Some(test)
} else {
self.bottom = Some(test)
}
}
Test::Test {
ref event,
ref name,
ref stdout,
ref exec_time,
} => {
let full_name = name.to_string();
let split_name = name.find("::");
let module = split_name.map(|i| name[0..i].to_string());
let name = split_name.map(|i| name[i + 2..].to_string());
if event == "ok" {
self.tests.push(ParsedTest {
full_name: full_name.clone(),
name: name.unwrap_or(full_name),
module,
exec_time: *exec_time,
status: TestStatus::Ok,
std_out: stdout.clone(),
})
} else if event == "started" {
} else if event == "ignored" {
self.tests.push(ParsedTest {
full_name: full_name.clone(),
name: name.unwrap_or(full_name),
module,
exec_time: *exec_time,
status: TestStatus::Ignored,
std_out: stdout.clone(),
})
} else {
self.tests.push(ParsedTest {
full_name: full_name.clone(),
name: name.unwrap_or(full_name),
module,
exec_time: *exec_time,
status: TestStatus::Failed,
std_out: stdout.clone(),
})
}
}
}
if let Some(Test::Suite {
event: _,
test_count: _,
ref passed,
ref failed,
ref errors,
ref allowed_fail,
ref ignored,
ref measured,
ref filtered_out,
ref exec_time,
}) = self.bottom
{
if let Some(Test::Suite {
event: _,
ref test_count,
..
}) = self.header
{
let mut tests: Vec<ParsedTest> = vec![];
std::mem::swap(&mut tests, &mut self.tests);
let suite_name = tests
.iter()
.map(|f| f.module.as_ref())
.filter(|f| f.is_some())
.map(|f| f.unwrap().clone())
.next();
return Ok(Some(ParsedTestSuite {
suite_name: suite_name.unwrap_or("test".to_string()),
test_count: test_count.unwrap(),
passed: passed.unwrap(),
failed: failed.unwrap(),
errors: errors.unwrap_or(0),
allowed_fail: allowed_fail.unwrap_or(0),
ignored: ignored.unwrap(),
measured: measured.unwrap(),
filtered_out: filtered_out.unwrap(),
exec_time: exec_time.unwrap(),
tests,
}));
}
}
Ok(None)
}
fn reset(&mut self) {
self.header = None;
self.bottom = None;
self.tests = vec![];
}
}
pub(crate) fn parse_test(
str: &str,
ignore_parse_error: bool,
) -> Result<Option<Test>, anyhow::Error> {
if str.starts_with("{") {
let parsed = serde_json::from_str::<'_, Test>(str);
match parsed {
Ok(p) => Ok(Some(p)),
Err(e) => match e.classify() {
Category::Data if ignore_parse_error => return Ok(None),
_ => Err(anyhow!(e).context(format!("failed to parse test text: {}", str))),
},
}
} else {
Ok(None)
}
}