Skip to content

Commit

Permalink
feat: compute_name function now accepts network
Browse files Browse the repository at this point in the history
- this covers the case where a user will upload their test results file
  for pytest without -o junit_family=legacy, but with their network set
- this is the network that is usually uploaded from the CLI
- the network is optional argument and defaults to None
- since it's usually a list of file paths that contain the paths to
  source files we can use it to deduce the path associated with a pytest
  test result if the filename has not been provided
- this commit also moves the tests from python to rust and removes the
  python interface for the compute_name function since it's no longer
  needed
- there's possibly future work that can be done to optimize this process
  of matching the test result classname to files in the network, but
  this is a minimal working solution, it's also possible that with the
  updated instructions in the docs
  • Loading branch information
joseph-sentry committed Dec 6, 2024
1 parent d9346a6 commit 3f879b6
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 46 deletions.
131 changes: 125 additions & 6 deletions src/compute_name.rs
Original file line number Diff line number Diff line change
@@ -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(".");
Expand All @@ -13,19 +12,53 @@ fn compute_pytest(classname: &str, name: &str, filename: &str) -> String {
.collect::<Vec<_>>()
.join("::");

format!("{}::{}::{}", filename, actual_classname, name)
if !actual_classname.is_empty() {
format!("{}::{}::{}", filename, actual_classname, name)

Check warning on line 16 in src/compute_name.rs

View check run for this annotation

Codecov Notifications / codecov/patch

src/compute_name.rs#L16

Added line #L16 was not covered by tests
} 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>) -> String {
let classname_components = classname.split(".").collect::<Vec<_>>();
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::<Vec<_>>()
.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>>,
) -> String {
let name = unescape_str(name);
let classname = unescape_str(classname);
Expand All @@ -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)
}
Expand All @@ -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 &gt; it does the thing",
"it does the thing &gt; 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 &gt; 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"
);
}
}
26 changes: 21 additions & 5 deletions src/junit.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashSet;

use pyo3::prelude::*;

use quick_xml::events::attributes::Attributes;
Expand Down Expand Up @@ -52,6 +54,7 @@ fn populate(
testsuite: String,
testsuite_time: Option<&str>,
framework: Option<Framework>,
network: Option<&HashSet<String>>,
) -> PyResult<(Testrun, Option<Framework>)> {
let classname = rel_attrs.classname.unwrap_or_default();

Expand Down Expand Up @@ -79,23 +82,31 @@ 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);
};

Ok((t, framework))
}

#[pyfunction]
pub fn parse_junit_xml(file_bytes: &[u8]) -> PyResult<ParsingInfo> {
#[pyo3(signature = (file_bytes, filepaths=None))]
pub fn parse_junit_xml(file_bytes: &[u8], filepaths: Option<Vec<String>>) -> PyResult<ParsingInfo> {
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()),

Check warning on line 101 in src/junit.rs

View check run for this annotation

Codecov Notifications / codecov/patch

src/junit.rs#L101

Added line #L101 was not covered by tests
};

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) {
Expand All @@ -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<ParsingInfo> {
fn use_reader(
reader: &mut Reader<&[u8]>,
network: Option<HashSet<String>>,
) -> PyResult<ParsingInfo> {
let mut testruns: Vec<Testrun> = Vec::new();
let mut saved_testrun: Option<Testrun> = None;

Expand Down Expand Up @@ -153,6 +167,7 @@ fn use_reader(reader: &mut Reader<&[u8]>) -> PyResult<ParsingInfo> {
.unwrap_or_default(),
testsuite_times.iter().rev().find_map(|e| e.as_deref()),
framework,
network.as_ref(),
)?;
saved_testrun = Some(testrun);
framework = parsed_framework;
Expand Down Expand Up @@ -218,6 +233,7 @@ fn use_reader(reader: &mut Reader<&[u8]>) -> PyResult<ParsingInfo> {
.unwrap_or_default(),
testsuite_times.iter().rev().find_map(|e| e.as_deref()),
framework,
network.as_ref(),
)?;
testruns.push(testrun);
framework = parsed_framework;
Expand Down
1 change: 0 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,5 @@ fn test_results_parser(py: Python, m: &Bound<PyModule>) -> 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(())
}
34 changes: 0 additions & 34 deletions tests/test_compute_name.py

This file was deleted.

0 comments on commit 3f879b6

Please sign in to comment.