junitify 0.1.18

Takes cargo test JSON and transform to JUnit XML
//     junitify - Takes cargo test JSON and transform to JUnit XML
//
//         The MIT License (MIT)
//
//      Copyright (c) KoresFramework (https://gitlab.com/Kores/)
//      Copyright (c) contributors
//
//      Permission is hereby granted, free of charge, to any person obtaining a copy
//      of this software and associated documentation files (the "Software"), to deal
//      in the Software without restriction, including without limitation the rights
//      to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//      copies of the Software, and to permit persons to whom the Software is
//      furnished to do so, subject to the following conditions:
//
//      The above copyright notice and this permission notice shall be included in
//      all copies or substantial portions of the Software.
//
//      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//      IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//      FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//      AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//      LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//      OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//      THE SOFTWARE.
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)
    }
}