diff --git a/src/compute_name.rs b/src/compute_name.rs index 6a65701..693822e 100644 --- a/src/compute_name.rs +++ b/src/compute_name.rs @@ -1,9 +1,8 @@ use crate::testrun::Framework; -use pyo3::prelude::*; use quick_xml::escape::unescape; -use std::borrow::Cow; +use std::{borrow::Cow, collections::HashSet}; -fn compute_pytest(classname: &str, name: &str, filename: &str) -> String { +fn compute_pytest_using_filename(classname: &str, name: &str, filename: &str) -> String { let path_components = filename.split('/').count(); let classname_components = classname.split("."); @@ -13,19 +12,53 @@ fn compute_pytest(classname: &str, name: &str, filename: &str) -> String { .collect::>() .join("::"); - format!("{}::{}::{}", filename, actual_classname, name) + if !actual_classname.is_empty() { + format!("{}::{}::{}", filename, actual_classname, name) + } else { + format!("{}::{}", filename, name) + } +} + +fn path_from_classname(classname: &[&str]) -> String { + format!("{}.py", classname.join("/")) +} + +fn compute_pytest_using_network(classname: &str, name: &str, network: &HashSet) -> String { + let classname_components = classname.split(".").collect::>(); + let mut path_component_count = 0; + let start = classname_components.len(); + + while path_component_count < start { + let path = path_from_classname(&classname_components[..start - path_component_count]); + if network.contains(&path) { + if path_component_count > 0 { + let actual_classname = classname_components + .into_iter() + .skip(start - path_component_count) + .collect::>() + .join("::"); + return format!("{}::{}::{}", path, actual_classname, name); + } else { + return format!("{}::{}", path, name); + } + } + + path_component_count += 1; + } + + format!("{}::{}", classname, name) } pub fn unescape_str(s: &str) -> Cow<'_, str> { unescape(s).unwrap_or(Cow::Borrowed(s)) } -#[pyfunction(signature = (classname, name, framework, filename=None))] pub fn compute_name( classname: &str, name: &str, framework: Framework, filename: Option<&str>, + network: Option<&HashSet>, ) -> String { let name = unescape_str(name); let classname = unescape_str(classname); @@ -35,7 +68,9 @@ pub fn compute_name( Framework::Jest => name.to_string(), Framework::Pytest => { if let Some(filename) = filename { - compute_pytest(&classname, &name, &filename) + compute_pytest_using_filename(&classname, &name, &filename) + } else if let Some(network) = network { + compute_pytest_using_network(&classname, &name, network) } else { format!("{}::{}", classname, name) } @@ -48,3 +83,87 @@ pub fn compute_name( } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_compute_name() { + assert_eq!( + compute_name("a.b.c", "d", Framework::Pytest, None, None), + "a.b.c::d" + ); + } + + #[test] + fn test_compute_name_with_filename() { + assert_eq!( + compute_name("a.b.c", "d", Framework::Pytest, Some("a/b/c.py"), None), + "a/b/c.py::d" + ); + } + + #[test] + fn test_compute_name_with_network() { + let network = ["a/b/c.py"].iter().map(|e| e.to_string()).collect(); + assert_eq!( + compute_name("a.b.c", "d", Framework::Pytest, None, Some(&network)), + "a/b/c.py::d" + ); + } + + #[test] + fn test_compute_name_with_network_actual_classname() { + let network = ["a/b.py"].iter().map(|e| e.to_string()).collect(); + assert_eq!( + compute_name("a.b.c", "d", Framework::Pytest, None, Some(&network)), + "a/b.py::c::d" + ); + } + + #[test] + fn test_compute_name_with_network_actual_classname_no_match() { + let network = ["d.py"].iter().map(|e| e.to_string()).collect(); + assert_eq!( + compute_name("a.b.c", "d", Framework::Pytest, None, Some(&network)), + "a.b.c::d" + ); + } + + #[test] + fn test_compute_name_jest() { + assert_eq!( + compute_name( + "it does the thing > it does the thing", + "it does the thing > it does the thing", + Framework::Jest, + None, + None + ), + "it does the thing > it does the thing" + ); + } + + #[test] + fn test_compute_name_vitest() { + assert_eq!( + compute_name( + "tests/thing.js", + "it does the thing > it does the thing", + Framework::Vitest, + None, + None + ), + "tests/thing.js > it does the thing > it does the thing" + ); + } + + #[test] + fn test_compute_name_phpunit() { + assert_eq!( + compute_name("class.className", "test1", Framework::PHPUnit, None, None), + "class.className::test1" + ); + } +} diff --git a/src/junit.rs b/src/junit.rs index 1610d8b..5542fc8 100644 --- a/src/junit.rs +++ b/src/junit.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use pyo3::prelude::*; use quick_xml::events::attributes::Attributes; @@ -52,6 +54,7 @@ fn populate( testsuite: String, testsuite_time: Option<&str>, framework: Option, + network: Option<&HashSet>, ) -> PyResult<(Testrun, Option)> { let classname = rel_attrs.classname.unwrap_or_default(); @@ -79,7 +82,7 @@ fn populate( let framework = framework.or_else(|| t.framework()); if let Some(f) = framework { - let computed_name = compute_name(&t.classname, &t.name, f, t.filename.as_deref()); + let computed_name = compute_name(&t.classname, &t.name, f, t.filename.as_deref(), network); t.computed_name = Some(computed_name); }; @@ -87,15 +90,23 @@ fn populate( } #[pyfunction] -pub fn parse_junit_xml(file_bytes: &[u8]) -> PyResult { +#[pyo3(signature = (file_bytes, filepaths=None))] +pub fn parse_junit_xml(file_bytes: &[u8], filepaths: Option>) -> PyResult { let mut reader = Reader::from_reader(file_bytes); + reader.config_mut().trim_text(true); - let thing = use_reader(&mut reader).map_err(|e| { + + let network = match filepaths { + None => None, + Some(filepaths) => Some(filepaths.into_iter().collect()), + }; + + let parsing_info = use_reader(&mut reader, network).map_err(|e| { let pos = reader.buffer_position(); let (line, col) = get_position_info(file_bytes, pos.try_into().unwrap()); ParserError::new_err(format!("Error at {}:{}: {}", line, col, e)) })?; - Ok(thing) + Ok(parsing_info) } fn get_position_info(input: &[u8], byte_offset: usize) -> (usize, usize) { @@ -114,7 +125,10 @@ fn get_position_info(input: &[u8], byte_offset: usize) -> (usize, usize) { (line, column) } -fn use_reader(reader: &mut Reader<&[u8]>) -> PyResult { +fn use_reader( + reader: &mut Reader<&[u8]>, + network: Option>, +) -> PyResult { let mut testruns: Vec = Vec::new(); let mut saved_testrun: Option = None; @@ -153,6 +167,7 @@ fn use_reader(reader: &mut Reader<&[u8]>) -> PyResult { .unwrap_or_default(), testsuite_times.iter().rev().find_map(|e| e.as_deref()), framework, + network.as_ref(), )?; saved_testrun = Some(testrun); framework = parsed_framework; @@ -218,6 +233,7 @@ fn use_reader(reader: &mut Reader<&[u8]>) -> PyResult { .unwrap_or_default(), testsuite_times.iter().rev().find_map(|e| e.as_deref()), framework, + network.as_ref(), )?; testruns.push(testrun); framework = parsed_framework; diff --git a/src/lib.rs b/src/lib.rs index 6b0fc21..e71de8d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,5 @@ fn test_results_parser(py: Python, m: &Bound) -> PyResult<()> { m.add_function(wrap_pyfunction!(failure_message::build_message, m)?)?; m.add_function(wrap_pyfunction!(failure_message::escape_message, m)?)?; m.add_function(wrap_pyfunction!(failure_message::shorten_file_paths, m)?)?; - m.add_function(wrap_pyfunction!(compute_name::compute_name, m)?)?; Ok(()) } diff --git a/tests/test_compute_name.py b/tests/test_compute_name.py deleted file mode 100644 index 2ea1f93..0000000 --- a/tests/test_compute_name.py +++ /dev/null @@ -1,34 +0,0 @@ -from test_results_parser import compute_name, Framework - - -def test_compute_name(): - assert compute_name( - name="test_junit[junit.xml--True]", - classname="tests.test_parsers.TestParsers", - filename="tests/test_parsers.py", - framework=Framework.Pytest, - ) == "tests/test_parsers.py::TestParsers::test_junit[junit.xml--True]" - - assert compute_name( - name="test_junit[junit.xml--True]", - classname="tests.test_parsers.TestParsers", - framework=Framework.Pytest, - ) == "tests.test_parsers.TestParsers::test_junit[junit.xml--True]" - - assert compute_name( - name="it does the thing > it does the thing", - classname="it does the thing > it does the thing", - framework=Framework.Jest, - ) == "it does the thing > it does the thing" - - assert compute_name( - name="it does the thing > it does the thing", - classname="tests/thing.js", - framework=Framework.Vitest, - ) == "tests/thing.js > it does the thing > it does the thing" - - assert compute_name( - name="test1", - classname="class.className", - framework=Framework.PHPUnit, - ) == "class.className::test1"