use std::fs::File;
use std::io::Write;
use std::path::Path;
use regex::Regex;
use super::Testcase;
use crate::report::ReportError;
use crate::util::path::create_dir_all;
const TAP_REPORT_VERSION_MARKER: &str = "TAP version 13";
pub fn write_report(filename: &Path, testcases: &[Testcase]) -> Result<(), ReportError> {
let mut all_testcases = vec![];
let existing_testcases = parse_tap_file(filename)?;
for testcase in existing_testcases.iter() {
all_testcases.push(testcase);
}
for testcase in testcases {
all_testcases.push(testcase);
}
write_tap_file(filename, &all_testcases)
}
fn write_tap_file(filename: &Path, testcases: &[&Testcase]) -> Result<(), ReportError> {
create_dir_all(filename)
.map_err(|e| ReportError::from_io_error(&e, filename, "Issue writing TAP report"))?;
let mut file = File::create(filename)
.map_err(|e| ReportError::from_io_error(&e, filename, "Issue writing TAP report"))?;
let start = 1;
let end = testcases.len();
let mut s = format!("{TAP_REPORT_VERSION_MARKER}\n");
s.push_str(format!("{start}..{end}\n").as_str());
for (i, testcase) in testcases.iter().enumerate() {
let state = if testcase.success { "ok" } else { "not ok" };
let number = i + 1;
let description = &testcase.description;
s.push_str(format!("{state} {number} - {description}\n").as_str());
}
file.write_all(s.as_bytes())
.map_err(|e| ReportError::from_io_error(&e, filename, "Issue writing TAP report"))?;
Ok(())
}
fn parse_tap_file(filename: &Path) -> Result<Vec<Testcase>, ReportError> {
if !filename.exists() {
return Ok(vec![]);
}
let s = std::fs::read_to_string(filename)
.map_err(|e| ReportError::from_io_error(&e, filename, "Issue reading TAP report"))?;
parse_tap_report(&s)
}
fn parse_tap_report(s: &str) -> Result<Vec<Testcase>, ReportError> {
let mut testcases = vec![];
let mut lines: Vec<&str> = s.lines().collect::<Vec<&str>>();
if !lines.is_empty() {
let mut header = lines.remove(0);
if header.eq_ignore_ascii_case(TAP_REPORT_VERSION_MARKER) {
header = lines.remove(0);
}
let re = Regex::new(r"^1\.\.\d+.*$").unwrap();
if !re.is_match(header) {
return Err(ReportError::from_string(&format!(
"Invalid TAP Header <{header}>"
)));
}
for line in lines {
let line = line.trim();
if !line.is_empty() {
let testcase = Testcase::parse(line)?;
testcases.push(testcase);
}
}
}
Ok(testcases)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_tap_report() {
let s = r#"1..3
ok 1 - tests_ok/test.1.hurl
ok 2 -tests_ok/test.2.hurl
not ok 3 - tests_ok/test.3.hurl
"#;
assert_eq!(
parse_tap_report(s).unwrap(),
vec![
Testcase {
description: "tests_ok/test.1.hurl".to_string(),
success: true
},
Testcase {
description: "tests_ok/test.2.hurl".to_string(),
success: true
},
Testcase {
description: "tests_ok/test.3.hurl".to_string(),
success: false
}
]
);
}
#[test]
fn test_parse_tap_report_with_version() {
let s = r#"TAP version 13
1..3
ok 1 - tests_ok/test.1.hurl
ok 2 -tests_ok/test.2.hurl
not ok 3 - tests_ok/test.3.hurl
"#;
assert_eq!(
parse_tap_report(s).unwrap(),
vec![
Testcase {
description: "tests_ok/test.1.hurl".to_string(),
success: true
},
Testcase {
description: "tests_ok/test.2.hurl".to_string(),
success: true
},
Testcase {
description: "tests_ok/test.3.hurl".to_string(),
success: false
}
]
);
let s = r#"TAP version 13
1..5 # TAP header can have comments
ok 1 - test.1.hurl
ok 2 - test.2.hurl
not ok 3 - test.3.hurl
not ok 4 - test.4.hurl
ok 5 - test.5.hurl
"#;
assert_eq!(
parse_tap_report(s).unwrap(),
vec![
Testcase {
description: "test.1.hurl".to_string(),
success: true
},
Testcase {
description: "test.2.hurl".to_string(),
success: true
},
Testcase {
description: "test.3.hurl".to_string(),
success: false
},
Testcase {
description: "test.4.hurl".to_string(),
success: false
},
Testcase {
description: "test.5.hurl".to_string(),
success: true
}
]
);
}
#[test]
fn test_parse_error() {
let s = r#"Dummy header
ok 1 - test.1.hurl
ok 2 - test.2.hurl
not ok 3 - test.3.hurl
"#;
assert!(parse_tap_report(s).is_err());
}
}