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 crate::test_parser::{ParsedTest, TestStatus};
use crate::{ParsedTestSuite, TestParser};
use serde;
use serde::{Deserialize, Serialize};
use serde_xml_rs::Error;
use std::time::Duration;

#[derive(Deserialize, Debug, PartialEq)]
#[serde(rename = "valgrindoutput")]
pub(crate) struct ValgrindOutput {
    pub(crate) preamble: Vec<ValgrindPreamble>,
    pub(crate) pid: usize,
    #[serde(rename = "ppid")]
    pub(crate) parent_pid: usize,
    pub(crate) tool: String, // TODO: Use Enum
    #[serde(rename = "protocolversion")]
    pub(crate) protocol_version: usize,
    #[serde(rename = "protocoltool")]
    pub(crate) protocol_tool: String,
    #[serde(rename = "args")]
    pub(crate) args: Option<ValgrindArgs>,
    #[serde(rename = "status")]
    pub(crate) status: Option<Vec<ValgrindStatus>>,
    #[serde(rename = "error")]
    pub(crate) errors: Option<Vec<ValgrindError>>,
}

#[derive(Deserialize, Debug, PartialEq)]
#[serde(rename = "preamble")]
pub(crate) struct ValgrindPreamble {
    #[serde(rename = "line")]
    pub(crate) lines: Vec<String>,
}

#[derive(Deserialize, Debug, PartialEq)]
#[serde(rename = "args")]
pub(crate) struct ValgrindArgs {
    #[serde(rename = "vargv")]
    pub(crate) vargv: Argv,
    #[serde(rename = "argv")]
    pub(crate) argv: Argv,
}

#[derive(Deserialize, Debug, PartialEq)]
#[serde(rename = "status")]
pub(crate) struct ValgrindStatus {
    pub(crate) state: String,
    pub(crate) time: String,
}

#[derive(Deserialize, Debug, PartialEq)]
pub(crate) struct Argv {
    pub(crate) exe: String,
    pub(crate) arg: Option<Vec<String>>,
}

#[derive(Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename = "error")]
pub(crate) struct ValgrindError {
    pub(crate) unique: String,
    pub(crate) tid: String,
    pub(crate) kind: String,
    #[serde(rename = "what")]
    pub(crate) what: Option<String>,
    #[serde(rename = "xwhat")]
    pub(crate) x_what: Option<XWhat>,
    #[serde(rename = "stack")]
    pub(crate) stack: Option<ValgrindStack>,
}

#[derive(Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename = "xwhat")]
pub(crate) struct XWhat {
    pub(crate) text: String,
    #[serde(rename = "leakedbytes")]
    pub(crate) leaked_bytes: usize,
    #[serde(rename = "leakedblocks")]
    pub(crate) leaked_blocks: usize,
}

#[derive(Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename = "stack")]
pub(crate) struct ValgrindStack {
    #[serde(rename = "frame")]
    pub(crate) frames: Vec<ValgrindStackFrame>,
}

#[derive(Deserialize, Serialize, Debug, PartialEq)]
pub(crate) struct ValgrindStackFrame {
    pub(crate) ip: String,
    pub(crate) obj: Option<String>,
    #[serde(rename = "fn")]
    pub(crate) function: Option<String>,
    pub(crate) dir: Option<String>,
    pub(crate) file: Option<String>,
    pub(crate) line: Option<usize>,
}

/// A parser for Valgrind memcheck, only supports Protocol Version 4 defined in
/// [https://github.com/mozilla-b2g/valgrind/blob/master/docs/internals/xml-output-protocol4.txt].
pub(crate) struct ValgrindTestParser {}

impl ValgrindTestParser {
    pub(crate) fn new() -> Self {
        Self {}
    }
}

impl TestParser for ValgrindTestParser {
    type Error = Error;

    fn multi_line(&self) -> bool {
        false
    }

    fn parse(&mut self, text: &str) -> Result<Option<ParsedTestSuite>, Error> {
        let valgrind: ValgrindOutput = serde_xml_rs::from_str(text)?;
        let finish = valgrind
            .status
            .map(|v| {
                v.iter()
                    .find(|s| s.state.to_lowercase() == "finished")
                    .map(|s| parse_duration(&s.time))
            })
            .flatten()
            .flatten();

        let tests = valgrind
            .errors
            .map(|v| {
                v.iter()
                    .map(|e| {
                        let pretty_out = serde_json::to_string_pretty(e).ok();
                        let name = e
                            .what
                            .as_ref()
                            .map(|w| w.to_string())
                            .or(e.x_what.as_ref().map(|x| x.text.clone()))
                            .unwrap_or_default();

                        ParsedTest {
                            full_name: format!("{} ({})", name, e.kind),
                            name,
                            module: Some(format!("{}", e.kind)),
                            exec_time: None,
                            status: TestStatus::Failed,
                            std_out: pretty_out,
                        }
                    })
                    .collect::<Vec<_>>()
            })
            .unwrap_or(vec![]);

        Ok(Some(ParsedTestSuite {
            suite_name: valgrind.tool.clone(),
            test_count: tests.len(),
            passed: 0,
            failed: tests.len(),
            errors: 0,
            allowed_fail: 0,
            ignored: 0,
            measured: 0,
            filtered_out: 0,
            exec_time: finish.map(|d| d.as_secs_f64()).unwrap_or(0.0f64),
            tests,
        }))
    }

    fn reset(&mut self) {}
}

fn parse_duration(duration: &str) -> Option<Duration> {
    let parts = duration.split(":").take(4).collect::<Vec<&str>>();
    if parts.len() < 4 {
        None
    } else {
        let days = parts[0].parse::<u64>().ok()?;
        let hours = parts[1].parse::<u64>().ok()?;
        let mins = parts[2].parse::<u64>().ok()?;
        let (s, ms) = parts[3].split_once(".")?;
        let seconds = s.parse::<u64>().ok()?;
        let ms = ms.parse::<u32>().ok()?;
        Some(Duration::new(
            (days * 24 * 60 * 60) + (hours * 60 * 60) + (mins * 60) + seconds,
            ms * 1000000,
        ))
    }
}