| // Copyright 2023 The Fuchsia Authors. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use std::iter::Peekable; |
| |
| use crate::helpers::TestType; |
| use anyhow::{anyhow, Error}; |
| use gtest_runner_lib::parser::{ |
| Failure, IndividualTestOutput, IndividualTestOutputStatus, TestOutput, TestSuiteOutput, |
| }; |
| use xml::reader::{EventReader, XmlEvent}; |
| |
| pub fn parse_results(test_type: TestType, contents: &str) -> Result<TestOutput, Error> { |
| match test_type { |
| TestType::Gtest | TestType::Gunit => { |
| serde_json::from_str(contents).map_err(|e| anyhow!("JSON parsing error: {e}")) |
| } |
| TestType::GtestXmlOutput => { |
| let mut errors = vec![]; |
| let mut iter = EventReader::new(contents.as_bytes()) |
| .into_iter() |
| .filter(|p| match p { |
| Ok(XmlEvent::Whitespace(..)) => false, |
| _ => true, |
| }) |
| .map_while(|r| match r { |
| Ok(event) => Some(event), |
| Err(err) => { |
| errors.push(err); |
| None |
| } |
| }) |
| .peekable(); |
| parse_xml_test_output(&mut iter) |
| .map_err(|e| anyhow!("Parsing error: {e} errors from XML parser: {errors:?}")) |
| } |
| _ => panic!("Do not know how to parse results for test type."), |
| } |
| } |
| |
| fn parse_xml_test_output<I>(iter: &mut Peekable<I>) -> Result<TestOutput, Error> |
| where |
| I: Iterator<Item = XmlEvent>, |
| { |
| match iter.next() { |
| Some(XmlEvent::StartDocument { .. }) => {} |
| _ => { |
| return Err(anyhow!("Expected document start event")); |
| } |
| } |
| if let Some(XmlEvent::StartElement { name, .. }) = iter.next() { |
| if name.borrow().local_name != "testsuites" { |
| return Err(anyhow!("Expected <testsuites> saw ${name}")); |
| } |
| |
| Ok(TestOutput { testsuites: parse_xml_testsuites(iter)? }) |
| } else { |
| Err(anyhow!("Expected <testsuites>")) |
| } |
| } |
| |
| fn parse_xml_testsuites<I>(iter: &mut Peekable<I>) -> Result<Vec<TestSuiteOutput>, Error> |
| where |
| I: Iterator<Item = XmlEvent>, |
| { |
| let mut suites = vec![]; |
| loop { |
| if let Some(XmlEvent::StartElement { name, attributes, .. }) = iter.next() { |
| if name.borrow().local_name != "testsuite" { |
| return Err(anyhow!("Expected <testsuite> got {name}")); |
| } |
| let mut output = TestSuiteOutput::default(); |
| for a in attributes { |
| match a.name.local_name.as_str() { |
| "name" => { |
| output.name = a.value.to_string(); |
| } |
| "tests" => { |
| output.tests = a |
| .value |
| .parse::<usize>() |
| .map_err(|e| anyhow!("Unable to parse number of tests: {e}"))?; |
| } |
| "failures" => { |
| output.failures = a |
| .value |
| .parse::<usize>() |
| .map_err(|e| anyhow!("Unable to parse number of failures: {e}"))?; |
| } |
| "time" => { |
| output.time = a.value.to_string(); |
| } |
| _ => {} |
| } |
| } |
| output.testsuite = parse_xml_testsuite(iter)?; |
| suites.push(output); |
| } else { |
| return Ok(suites); |
| } |
| } |
| } |
| |
| fn parse_xml_testsuite<I>(iter: &mut Peekable<I>) -> Result<Vec<IndividualTestOutput>, Error> |
| where |
| I: Iterator<Item = XmlEvent>, |
| { |
| let mut outputs = vec![]; |
| loop { |
| match iter.peek() { |
| Some(XmlEvent::StartElement { .. }) => { |
| outputs.push(parse_xml_testcase(iter)?); |
| } |
| Some(XmlEvent::EndElement { .. }) => { |
| iter.next(); |
| break; |
| } |
| _ => { |
| return Err(anyhow!("Expected <testcase> or </testsuite>")); |
| } |
| } |
| } |
| Ok(outputs) |
| } |
| |
| fn parse_xml_testcase<I>(iter: &mut Peekable<I>) -> Result<IndividualTestOutput, Error> |
| where |
| I: Iterator<Item = XmlEvent>, |
| { |
| if let Some(XmlEvent::StartElement { name, attributes, .. }) = iter.next() { |
| if name.local_name != "testcase" { |
| return Err(anyhow!("Expected <testcase> saw {name}")); |
| } |
| |
| let mut output = IndividualTestOutput::default(); |
| |
| // The XML format does not include a "result" field. Pretend that it's completed. |
| output.result = "COMPLETED".to_string(); |
| |
| for a in attributes { |
| match a.name.local_name.as_str() { |
| "name" => { |
| output.name = a.value.to_string(); |
| } |
| "time" => { |
| output.time = a.value.to_string(); |
| } |
| "status" => { |
| output.status = match a.value.as_str() { |
| "run" => IndividualTestOutputStatus::Run, |
| _ => IndividualTestOutputStatus::NotRun, |
| } |
| } |
| _ => {} |
| } |
| } |
| loop { |
| match iter.peek() { |
| Some(XmlEvent::EndElement { .. }) => { |
| iter.next(); |
| break; |
| } |
| Some(XmlEvent::StartElement { .. }) => { |
| output.failures = Some(parse_xml_failures(iter)?); |
| } |
| _ => { |
| return Err(anyhow!("Expected <failure> or end of <testcase>")); |
| } |
| } |
| } |
| Ok(output) |
| } else { |
| Err(anyhow!("Expected <testcase>")) |
| } |
| } |
| |
| fn parse_xml_failures<I>(iter: &mut Peekable<I>) -> Result<Vec<Failure>, Error> |
| where |
| I: Iterator<Item = XmlEvent>, |
| { |
| let mut failures = vec![]; |
| loop { |
| if let Some(XmlEvent::StartElement { name, attributes, .. }) = iter.peek() { |
| if name.local_name.as_str() != "failure" { |
| return Err(anyhow!("Expected <failure> got {name}")); |
| } |
| let mut failure = Failure::default(); |
| for a in attributes { |
| if a.name.local_name == "message" { |
| failure.failure = a.value.to_string(); |
| } |
| } |
| iter.next(); |
| if let Some(XmlEvent::EndElement { .. }) = iter.next() { |
| failures.push(failure); |
| } else { |
| return Err(anyhow!("Expected </failure>")); |
| } |
| } else { |
| break; |
| } |
| } |
| Ok(failures) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use crate::helpers::TestType; |
| use gtest_runner_lib::parser::*; |
| |
| use super::parse_results; |
| |
| fn expected_results() -> TestOutput { |
| TestOutput { |
| testsuites: vec![ |
| TestSuiteOutput { |
| name: "MathTest".to_string(), |
| tests: 2, |
| failures: 1, |
| disabled: 0, |
| time: "0.015s".to_string(), |
| testsuite: vec![ |
| IndividualTestOutput { |
| name: "Addition".to_string(), |
| status: IndividualTestOutputStatus::Run, |
| time: "0.007s".to_string(), |
| failures: Some(vec![ |
| Failure { |
| failure: "Value of: add(1, 1)\n Actual: 3\n Expected: 2" |
| .to_string(), |
| }, |
| Failure { |
| failure: "Value of: add(1, -1)\n Actual: 1\n Expected: 0" |
| .to_string(), |
| }, |
| ]), |
| result: "COMPLETED".to_string(), |
| }, |
| IndividualTestOutput { |
| name: "Subtraction".to_string(), |
| status: IndividualTestOutputStatus::Run, |
| time: "0.005s".to_string(), |
| failures: None, |
| result: "COMPLETED".to_string(), |
| }, |
| ], |
| }, |
| TestSuiteOutput { |
| name: "LogicTest".to_string(), |
| tests: 1, |
| failures: 0, |
| disabled: 0, |
| time: "0.005s".to_string(), |
| testsuite: vec![IndividualTestOutput { |
| name: "NonContradiction".to_string(), |
| status: IndividualTestOutputStatus::Run, |
| time: "0.005s".to_string(), |
| failures: None, |
| result: "COMPLETED".to_string(), |
| }], |
| }, |
| ], |
| } |
| } |
| |
| #[test] |
| fn parse_json_results() { |
| let results_json = r#" |
| { |
| "tests": 3, |
| "failures": 1, |
| "disabled": 0, |
| "errors": 0, |
| "time": "0.035s", |
| "timestamp": "2011-10-31T18:52:42Z", |
| "name": "AllTests", |
| "testsuites": [ |
| { |
| "name": "MathTest", |
| "tests": 2, |
| "failures": 1, |
| "disabled": 0, |
| "errors": 0, |
| "time": "0.015s", |
| "testsuite": [ |
| { |
| "name": "Addition", |
| "status": "RUN", |
| "result": "COMPLETED", |
| "time": "0.007s", |
| "classname": "", |
| "failures": [ |
| { |
| "failure": "Value of: add(1, 1)\n Actual: 3\n Expected: 2", |
| "type": "" |
| }, |
| { |
| "failure": "Value of: add(1, -1)\n Actual: 1\n Expected: 0", |
| "type": "" |
| } |
| ] |
| }, |
| { |
| "name": "Subtraction", |
| "status": "RUN", |
| "result": "COMPLETED", |
| "time": "0.005s", |
| "classname": "" |
| } |
| ] |
| }, |
| { |
| "name": "LogicTest", |
| "tests": 1, |
| "failures": 0, |
| "disabled": 0, |
| "errors": 0, |
| "time": "0.005s", |
| "testsuite": [ |
| { |
| "name": "NonContradiction", |
| "status": "RUN", |
| "result": "COMPLETED", |
| "time": "0.005s", |
| "classname": "" |
| } |
| ] |
| } |
| ] |
| } |
| "#; |
| |
| let results = parse_results(TestType::Gtest, results_json).unwrap(); |
| assert_eq!(results, expected_results()); |
| } |
| |
| #[test] |
| fn parse_xml_results() { |
| let results_xml = r#" |
| <?xml version="1.0" encoding="UTF-8"?> |
| <testsuites name="AllTests" tests="3" failures="1" disabled="0" errors="0" |
| timestamp="2011-10-31T18:52:42Z"> |
| <testsuite name="MathTest" tests="2" failures="1" disabled="0" errors="0" time="0.015s"> |
| <testcase name="Addition" status="run" time="0.007s" classname="addition"> |
| <failure message="Value of: add(1, 1) |
| Actual: 3 |
| Expected: 2" type=""></failure> |
| <failure message="Value of: add(1, -1) |
| Actual: 1 |
| Expected: 0" type=""></failure> |
| </testcase> |
| <testcase name="Subtraction" status="run" time="0.005s" classname="subtraction" /> |
| </testsuite> |
| <testsuite name="LogicTest" tests="1" failures="0" disabled="0" errors="0" time="0.005s"> |
| <testcase name="NonContradiction" status="run" time="0.005s" classname="logic_test" /> |
| </testsuite> |
| </testsuites> |
| "#; |
| |
| let results = parse_results(TestType::GtestXmlOutput, results_xml).unwrap(); |
| assert_eq!(results, expected_results()); |
| } |
| } |