From eee349e65d9047caf03f8047b5f9a720932023c3 Mon Sep 17 00:00:00 2001 From: joseph-sentry Date: Thu, 19 Dec 2024 13:36:30 -0500 Subject: [PATCH 1/4] deps: prepare deps for parse raw upload we will need to handle msgpacking, base64 decoding and zlib decompressing --- Cargo.lock | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 ++ pyproject.toml | 1 + uv.lock | 32 +++++++++++++++++++ 4 files changed, 122 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index f08414b..5506ed3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aho-corasick" version = "1.1.3" @@ -17,6 +23,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "basic-toml" version = "0.1.9" @@ -26,12 +38,37 @@ dependencies = [ "serde", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "heck" version = "0.5.0" @@ -108,6 +145,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "nom" version = "7.1.3" @@ -118,12 +164,27 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -296,6 +357,28 @@ dependencies = [ "serde", ] +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "rustc-hash" version = "2.0.0" @@ -361,10 +444,13 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" name = "test_results_parser" version = "0.5.1" dependencies = [ + "base64", + "flate2", "pyo3", "quick-xml", "regex", "rinja", + "rmp-serde", "serde", "serde_json", ] diff --git a/Cargo.toml b/Cargo.toml index 89e30bf..c3728f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,6 @@ regex = "1.11.1" serde = { version = "1.0.215", features = ["derive"] } serde_json = "1.0.133" rinja = "0.3.5" +rmp-serde = "1.3.0" +base64 = "0.22.1" +flate2 = "1.0.35" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 86067b3..c268531 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,4 +21,5 @@ dev-dependencies = [ "pytest-cov>=5.0.0", "pytest-reportlog>=0.4.0", "maturin>=1.7.4", + "msgpack>=1.1.0", ] diff --git a/uv.lock b/uv.lock index 77bb595..1210158 100644 --- a/uv.lock +++ b/uv.lock @@ -77,6 +77,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/38/8e27282ab6ff94291f04e3eb11ba960dfb487605d73cec75177d3a29879a/maturin-1.7.4-py3-none-win_arm64.whl", hash = "sha256:f3d38a6d0c7fd7b04bec30dd470b2173cf9bd184ab6220c1acaf49df6b48faf5", size = 6388008 }, ] +[[package]] +name = "msgpack" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/d0/7555686ae7ff5731205df1012ede15dd9d927f6227ea151e901c7406af4f/msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", size = 167260 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/d6/716b7ca1dbde63290d2973d22bbef1b5032ca634c3ff4384a958ec3f093a/msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d", size = 152421 }, + { url = "https://files.pythonhosted.org/packages/70/da/5312b067f6773429cec2f8f08b021c06af416bba340c912c2ec778539ed6/msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2", size = 85277 }, + { url = "https://files.pythonhosted.org/packages/28/51/da7f3ae4462e8bb98af0d5bdf2707f1b8c65a0d4f496e46b6afb06cbc286/msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420", size = 82222 }, + { url = "https://files.pythonhosted.org/packages/33/af/dc95c4b2a49cff17ce47611ca9ba218198806cad7796c0b01d1e332c86bb/msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2", size = 392971 }, + { url = "https://files.pythonhosted.org/packages/f1/54/65af8de681fa8255402c80eda2a501ba467921d5a7a028c9c22a2c2eedb5/msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39", size = 401403 }, + { url = "https://files.pythonhosted.org/packages/97/8c/e333690777bd33919ab7024269dc3c41c76ef5137b211d776fbb404bfead/msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f", size = 385356 }, + { url = "https://files.pythonhosted.org/packages/57/52/406795ba478dc1c890559dd4e89280fa86506608a28ccf3a72fbf45df9f5/msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247", size = 383028 }, + { url = "https://files.pythonhosted.org/packages/e7/69/053b6549bf90a3acadcd8232eae03e2fefc87f066a5b9fbb37e2e608859f/msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c", size = 391100 }, + { url = "https://files.pythonhosted.org/packages/23/f0/d4101d4da054f04274995ddc4086c2715d9b93111eb9ed49686c0f7ccc8a/msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b", size = 394254 }, + { url = "https://files.pythonhosted.org/packages/1c/12/cf07458f35d0d775ff3a2dc5559fa2e1fcd06c46f1ef510e594ebefdca01/msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b", size = 69085 }, + { url = "https://files.pythonhosted.org/packages/73/80/2708a4641f7d553a63bc934a3eb7214806b5b39d200133ca7f7afb0a53e8/msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f", size = 75347 }, + { url = "https://files.pythonhosted.org/packages/c8/b0/380f5f639543a4ac413e969109978feb1f3c66e931068f91ab6ab0f8be00/msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf", size = 151142 }, + { url = "https://files.pythonhosted.org/packages/c8/ee/be57e9702400a6cb2606883d55b05784fada898dfc7fd12608ab1fdb054e/msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330", size = 84523 }, + { url = "https://files.pythonhosted.org/packages/7e/3a/2919f63acca3c119565449681ad08a2f84b2171ddfcff1dba6959db2cceb/msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734", size = 81556 }, + { url = "https://files.pythonhosted.org/packages/7c/43/a11113d9e5c1498c145a8925768ea2d5fce7cbab15c99cda655aa09947ed/msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e", size = 392105 }, + { url = "https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca", size = 399979 }, + { url = "https://files.pythonhosted.org/packages/82/8c/cf64ae518c7b8efc763ca1f1348a96f0e37150061e777a8ea5430b413a74/msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915", size = 383816 }, + { url = "https://files.pythonhosted.org/packages/69/86/a847ef7a0f5ef3fa94ae20f52a4cacf596a4e4a010197fbcc27744eb9a83/msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d", size = 380973 }, + { url = "https://files.pythonhosted.org/packages/aa/90/c74cf6e1126faa93185d3b830ee97246ecc4fe12cf9d2d31318ee4246994/msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434", size = 387435 }, + { url = "https://files.pythonhosted.org/packages/7a/40/631c238f1f338eb09f4acb0f34ab5862c4e9d7eda11c1b685471a4c5ea37/msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c", size = 399082 }, + { url = "https://files.pythonhosted.org/packages/e9/1b/fa8a952be252a1555ed39f97c06778e3aeb9123aa4cccc0fd2acd0b4e315/msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc", size = 69037 }, + { url = "https://files.pythonhosted.org/packages/b6/bc/8bd826dd03e022153bfa1766dcdec4976d6c818865ed54223d71f07862b3/msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f", size = 75140 }, +] + [[package]] name = "packaging" version = "24.2" @@ -143,6 +173,7 @@ source = { editable = "." } [package.dev-dependencies] dev = [ { name = "maturin" }, + { name = "msgpack" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-reportlog" }, @@ -153,6 +184,7 @@ dev = [ [package.metadata.requires-dev] dev = [ { name = "maturin", specifier = ">=1.7.4" }, + { name = "msgpack", specifier = ">=1.1.0" }, { name = "pytest", specifier = ">=8.3.3" }, { name = "pytest-cov", specifier = ">=5.0.0" }, { name = "pytest-reportlog", specifier = ">=0.4.0" }, From 0aff0d2dfeab3079b1b6bc31ae5b59aab387bd04 Mon Sep 17 00:00:00 2001 From: joseph-sentry Date: Thu, 19 Dec 2024 13:38:30 -0500 Subject: [PATCH 2/4] docs: update the readme for parse_raw_upload we want to document the new function that we're going to implement --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9a8450c..0ea48ca 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,15 @@ The CI uses the maturin-action to build wheels and an sdist The version of the wheels built are determined by the value of the version in the cargo.toml -There are 2 parsing function currently implemented: + +There are 2 parsing functions currently implemented: - `parse_junit_xml`: this parses `junit.xml` files -- `parse_pytest_reportlog`: this parses files produced by the `pytest-reportlog` extension -Both these functions take the path to the file to parse as an arg and return a list of `Testrun` objects. +This function takes the path to the file to parse as an arg and returns a list of `Testrun` objects. The `Testrun` objects look like this: + ``` Outcome: Pass, @@ -33,4 +34,8 @@ Testrun: outcome: Outcome duration: float testsuite: str -``` \ No newline at end of file +``` + +- `parse_raw_upload`: this parses an entire raw test results upload + +this function takes in the raw upload bytes and returns a message packed list of Testrun objects From 7920ff8c8d325f5811165ed19ec32db905d2fd57 Mon Sep 17 00:00:00 2001 From: joseph-sentry Date: Thu, 19 Dec 2024 15:08:34 -0500 Subject: [PATCH 3/4] feat: add parse_raw_upload we want to support parsing raw upload files instead of individual JUnit XML files the input of this new function is the raw upload in byte form the output is a messagepacked binary payload containing the results of the parsing and the raw upload in readable format in byte form --- src/lib.rs | 2 + src/raw_upload.rs | 101 ++++++++++ src/testrun.rs | 25 ++- tests/test_parse_raw_upload.py | 347 +++++++++++++++++++++++++++++++++ 4 files changed, 468 insertions(+), 7 deletions(-) create mode 100644 src/raw_upload.rs create mode 100644 tests/test_parse_raw_upload.py diff --git a/src/lib.rs b/src/lib.rs index e71de8d..e1a4722 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ use pyo3::prelude::*; mod compute_name; mod failure_message; mod junit; +mod raw_upload; mod testrun; pyo3::create_exception!(test_results_parser, ParserError, PyException); @@ -18,6 +19,7 @@ fn test_results_parser(py: Python, m: &Bound) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + m.add_function(wrap_pyfunction!(raw_upload::parse_raw_upload, m)?)?; m.add_function(wrap_pyfunction!(junit::parse_junit_xml, m)?)?; m.add_function(wrap_pyfunction!(failure_message::build_message, m)?)?; m.add_function(wrap_pyfunction!(failure_message::escape_message, m)?)?; diff --git a/src/raw_upload.rs b/src/raw_upload.rs new file mode 100644 index 0000000..39ed8cd --- /dev/null +++ b/src/raw_upload.rs @@ -0,0 +1,101 @@ +use base64::prelude::*; +use pyo3::prelude::*; +use std::collections::HashSet; +use std::io::prelude::*; + +use flate2::bufread::ZlibDecoder; + +use quick_xml::reader::Reader; +use serde::Deserialize; + +use crate::junit::{get_position_info, use_reader}; +use crate::testrun::ParsingInfo; +use crate::ParserError; + +#[derive(Deserialize, Debug, Clone)] +struct TestResultFile { + filename: String, + #[serde(skip_deserializing)] + _format: String, + data: String, + #[serde(skip_deserializing)] + _labels: Vec, +} +#[derive(Deserialize, Debug, Clone)] +struct RawTestResultUpload { + #[serde(default)] + network: Option>, + test_results_files: Vec, +} + +#[derive(Debug, Clone)] +struct ReadableFile { + filename: Vec, + data: Vec, +} + +const LEGACY_FORMAT_PREFIX: &[u8] = b"# path="; +const LEGACY_FORMAT_SUFFIX: &[u8] = b"<<<<<< EOF"; + +fn serialize_to_legacy_format(readable_files: Vec) -> Vec { + let mut res = Vec::new(); + for file in readable_files { + res.extend_from_slice(LEGACY_FORMAT_PREFIX); + res.extend_from_slice(&file.filename); + res.extend_from_slice(b"\n"); + res.extend_from_slice(&file.data); + res.extend_from_slice(b"\n"); + res.extend_from_slice(LEGACY_FORMAT_SUFFIX); + res.extend_from_slice(b"\n"); + } + res +} + +#[pyfunction] +#[pyo3(signature = (raw_upload_bytes))] +pub fn parse_raw_upload(raw_upload_bytes: &[u8]) -> PyResult<(Vec, Vec)> { + let upload: RawTestResultUpload = serde_json::from_slice(raw_upload_bytes) + .map_err(|e| ParserError::new_err(format!("Error deserializing json: {}", e)))?; + let network: Option> = upload.network.map(|v| v.into_iter().collect()); + + let mut results: Vec = Vec::new(); + let mut readable_files: Vec = Vec::new(); + + for file in upload.test_results_files { + let decoded_file_bytes = BASE64_STANDARD + .decode(file.data) + .map_err(|e| ParserError::new_err(format!("Error decoding base64: {}", e)))?; + + let mut decoder = ZlibDecoder::new(&decoded_file_bytes[..]); + + let mut decompressed_file_bytes = Vec::new(); + decoder + .read_to_end(&mut decompressed_file_bytes) + .map_err(|e| ParserError::new_err(format!("Error decompressing file: {}", e)))?; + + let mut reader = Reader::from_reader(&decompressed_file_bytes[..]); + reader.config_mut().trim_text(true); + let reader_result = use_reader(&mut reader, network.as_ref()).map_err(|e| { + let pos = reader.buffer_position(); + let (line, col) = get_position_info(&decompressed_file_bytes, pos.try_into().unwrap()); + ParserError::new_err(format!( + "Error parsing JUnit XML at {}:{}: {}", + line, col, e + )) + })?; + results.push(reader_result); + + let readable_file = ReadableFile { + data: decompressed_file_bytes, + filename: file.filename.into_bytes(), + }; + readable_files.push(readable_file); + } + + let results_bytes = rmp_serde::to_vec_named(&results) + .map_err(|_| ParserError::new_err("Error serializing pr comment summary"))?; + + let readable_file = serialize_to_legacy_format(readable_files); + + Ok((results_bytes, readable_file)) +} diff --git a/src/testrun.rs b/src/testrun.rs index 14a74db..5ccd820 100644 --- a/src/testrun.rs +++ b/src/testrun.rs @@ -3,6 +3,8 @@ use std::fmt::Display; use pyo3::class::basic::CompareOp; use pyo3::{prelude::*, pyclass}; +use serde::Serialize; + #[derive(Clone, Copy, Debug, PartialEq)] // See https://github.com/PyO3/pyo3/issues/4723 #[allow(ambiguous_associated_items)] @@ -37,13 +39,22 @@ impl Outcome { } } +impl Serialize for Outcome { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + impl Display for Outcome { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self { - Outcome::Pass => write!(f, "Pass"), - Outcome::Failure => write!(f, "Failure"), - Outcome::Error => write!(f, "Error"), - Outcome::Skip => write!(f, "Skip"), + Outcome::Pass => write!(f, "pass"), + Outcome::Failure => write!(f, "failure"), + Outcome::Error => write!(f, "error"), + Outcome::Skip => write!(f, "skip"), } } } @@ -77,7 +88,7 @@ pub fn check_testsuites_name(testsuites_name: &str) -> Option { .next() } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize)] #[pyclass] pub struct Testrun { #[pyo3(get, set)] @@ -189,7 +200,7 @@ impl Testrun { } } -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Serialize)] #[pyclass(eq, eq_int)] pub enum Framework { Pytest, @@ -221,7 +232,7 @@ impl Display for Framework { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] #[pyclass] pub struct ParsingInfo { #[pyo3(get, set)] diff --git a/tests/test_parse_raw_upload.py b/tests/test_parse_raw_upload.py new file mode 100644 index 0000000..96797de --- /dev/null +++ b/tests/test_parse_raw_upload.py @@ -0,0 +1,347 @@ +import pytest +import base64 +import zlib +import json +import msgpack +from test_results_parser import parse_raw_upload + +class TestParsers: + @pytest.mark.parametrize( + "filename,expected", + [ + ( + "./tests/junit.xml", + { + "framework": "Pytest", + "testruns": [ + { + "name": "test_junit[junit.xml--True]", + "classname": "tests.test_parsers.TestParsers", + "duration": 0.001, + "outcome": "failure", + "testsuite": "pytest", + "failure_message": """self = , filename = 'junit.xml', expected = '', check = True + + @pytest.mark.parametrize( + \"filename,expected,check\", + [(\"junit.xml\", \"\", True), (\"jest-junit.xml\", \"\", False)], + ) + def test_junit(self, filename, expected, check): + with open(filename) as f: + junit_string = f.read() + res = parse_junit_xml(junit_string) + print(res) + if check: +> assert res == expected +E AssertionError: assert [{'duration': '0.010', 'name': 'tests.test_parsers.TestParsers.test_junit[junit.xml-]', 'outcome': 'failure'}, {'duration': '0.063', 'name': 'tests.test_parsers.TestParsers.test_junit[jest-junit.xml-]', 'outcome': 'pass'}] == '' + +tests/test_parsers.py:16: AssertionError""", + "filename": None, + "build_url": None, + "computed_name": "tests.test_parsers.TestParsers::test_junit[junit.xml--True]", + }, + { + "name": "test_junit[jest-junit.xml--False]", + "classname": "tests.test_parsers.TestParsers", + "duration": 0.064, + "outcome": "pass", + "testsuite": "pytest", + "failure_message": None, + "filename": None, + "build_url": None, + "computed_name": "tests.test_parsers.TestParsers::test_junit[jest-junit.xml--False]", + }, + ], + }, + ), + ( + "./tests/junit-no-testcase-timestamp.xml", + { + "framework": "Pytest", + "testruns": [ + { + "name": "test_junit[junit.xml--True]", + "classname": "tests.test_parsers.TestParsers", + "duration": 0.186, + "outcome": "failure", + "testsuite": "pytest", + "failure_message": "aaaaaaa", + "filename": None, + "build_url": None, + "computed_name": "tests.test_parsers.TestParsers::test_junit[junit.xml--True]", + }, + { + "name": "test_junit[jest-junit.xml--False]", + "classname": "tests.test_parsers.TestParsers", + "duration": 0.186, + "outcome": "pass", + "testsuite": "pytest", + "failure_message": None, + "filename": None, + "build_url": None, + "computed_name": "tests.test_parsers.TestParsers::test_junit[jest-junit.xml--False]", + }, + ], + }, + ), + ( + "./tests/junit-nested-testsuite.xml", + { + "framework": "Pytest", + "testruns": [ + { + "name": "test_junit[junit.xml--True]", + "classname": "tests.test_parsers.TestParsers", + "duration": 0.186, + "outcome": "failure", + "testsuite": "nested_testsuite", + "failure_message": "aaaaaaa", + "filename": None, + "build_url": None, + "computed_name": None, + }, + { + "name": "test_junit[jest-junit.xml--False]", + "classname": "tests.test_parsers.TestParsers", + "duration": 0.186, + "outcome": "pass", + "testsuite": "pytest", + "failure_message": None, + "filename": None, + "build_url": None, + "computed_name": "tests.test_parsers.TestParsers::test_junit[jest-junit.xml--False]", + }, + ], + }, + ), + ( + "./tests/jest-junit.xml", + { + "framework": "Jest", + "testruns": [ + { + "name": "Title when rendered renders pull title", + "classname": "Title when rendered renders pull title", + "duration": 0.036, + "outcome": "pass", + "testsuite": "Title", + "failure_message": None, + "filename": None, + "build_url": None, + "computed_name": "Title when rendered renders pull title", + }, + { + "name": "Title when rendered renders pull author", + "classname": "Title when rendered renders pull author", + "duration": 0.005, + "outcome": "pass", + "testsuite": "Title", + "failure_message": None, + "filename": None, + "build_url": None, + "computed_name": "Title when rendered renders pull author", + }, + { + "name": "Title when rendered renders pull updatestamp", + "classname": "Title when rendered renders pull updatestamp", + "duration": 0.002, + "outcome": "pass", + "testsuite": "Title", + "failure_message": None, + "filename": None, + "build_url": None, + "computed_name": "Title when rendered renders pull updatestamp", + }, + { + "name": "Title when rendered for first pull request renders pull title", + "classname": "Title when rendered for first pull request renders pull title", + "duration": 0.006, + "outcome": "pass", + "testsuite": "Title", + "failure_message": None, + "filename": None, + "build_url": None, + "computed_name": "Title when rendered for first pull request renders pull title", + }, + ], + }, + ), + ( + "./tests/vitest-junit.xml", + { + "framework": "Vitest", + "testruns": [ + { + "name": "first test file > 2 + 2 should equal 4", + "classname": "__tests__/test-file-1.test.ts", + "duration": 0.01, + "outcome": "failure", + "testsuite": "__tests__/test-file-1.test.ts", + "failure_message": """AssertionError: expected 5 to be 4 // Object.is equality + ❯ __tests__/test-file-1.test.ts:20:28""", + "filename": None, + "build_url": None, + "computed_name": "__tests__/test-file-1.test.ts > first test file > 2 + 2 should equal 4", + }, + { + "name": "first test file > 4 - 2 should equal 2", + "classname": "__tests__/test-file-1.test.ts", + "duration": 0.0, + "outcome": "pass", + "testsuite": "__tests__/test-file-1.test.ts", + "failure_message": None, + "filename": None, + "build_url": None, + "computed_name": "__tests__/test-file-1.test.ts > first test file > 4 - 2 should equal 2", + }, + ], + }, + ), + ( + "./tests/empty_failure.junit.xml", + { + "framework": None, + "testruns": [ + { + "name": "test.test works", + "classname": "test.test", + "duration": 0.234, + "outcome": "pass", + "testsuite": "test", + "failure_message": None, + "filename": "./test.rb", + "build_url": None, + "computed_name": None, + }, + { + "name": "test.test fails", + "classname": "test.test", + "duration": 1.0, + "outcome": "failure", + "testsuite": "test", + "failure_message": "TestError", + "filename": "./test.rb", + "build_url": None, + "computed_name": None, + }, + ], + }, + ), + ( + "./tests/phpunit.junit.xml", + { + "framework": "PHPUnit", + "testruns": [ + { + "name": "test1", + "classname": "class.className", + "duration": 0.1, + "outcome": "pass", + "testsuite": "Thing", + "failure_message": None, + "filename": "/file1.php", + "build_url": None, + "computed_name": "class.className::test1", + }, + { + "name": "test2", + "classname": "", + "duration": 0.1, + "outcome": "pass", + "testsuite": "Thing", + "failure_message": None, + "filename": "/file1.php", + "build_url": None, + "computed_name": "::test2", + }, + ], + }, + ), + ( + "./tests/ctest.xml", + { + "framework": None, + "testruns": [ + { + "name": "a_unit_test", + "classname": "a_unit_test", + "duration": 33.4734, + "outcome": "failure", + "testsuite": "Linux-c++", + "failure_message": "Failed", + "filename": None, + "build_url": None, + "computed_name": None, + } + ], + }, + ), + ( + "./tests/no-testsuite-name.xml", + { + "framework": None, + "testruns": [ + { + "name": "a_unit_test", + "classname": "a_unit_test", + "duration": 33.4734, + "outcome": "failure", + "testsuite": "", + "failure_message": "Failed", + "filename": None, + "build_url": None, + "computed_name": None, + } + ], + }, + ), + ("./tests/testsuites.xml", {"framework": None, "testruns": []}), + ], + ) + def test_junit(self, filename, expected): + with open(filename, "b+r") as f: + file_bytes = f.read() + thing = { + "network": [ + "a/b/c.py", + ], + "test_results_files": [ + { + "filename": filename, + "format": "base64+compressed", + "data": base64.b64encode(zlib.compress(file_bytes)).decode( + "utf-8" + ), + } + ] + } + json_bytes = json.dumps(thing).encode("utf-8") + msgpack_bytes, readable_files_bytes = parse_raw_upload(json_bytes) + + + + res_list = msgpack.unpackb( + bytes(msgpack_bytes) + ) + + readable_files = bytes(readable_files_bytes) + + assert readable_files == f"""# path={filename}\n{file_bytes.decode()}\n<<<<<< EOF\n""".encode() + + + assert res_list[0]["framework"] == expected["framework"] + assert res_list[0]["testruns"] == expected["testruns"] + assert len(res_list[0]["testruns"]) == len(expected["testruns"]) + for restest, extest in zip(res_list[0]["testruns"], expected["testruns"]): + print( + restest["classname"], + restest["duration"], + restest["filename"], + restest["name"], + restest["outcome"], + restest["testsuite"], + extest["failure_message"], + extest["filename"], + extest["computed_name"], + ) + assert restest == extest From e69c111eccc822432f8eff9f8efa397ec7a88eb1 Mon Sep 17 00:00:00 2001 From: joseph-sentry Date: Fri, 20 Dec 2024 16:31:25 -0500 Subject: [PATCH 4/4] feat: small improvement to error message --- src/raw_upload.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/raw_upload.rs b/src/raw_upload.rs index 39ed8cd..098f67e 100644 --- a/src/raw_upload.rs +++ b/src/raw_upload.rs @@ -79,8 +79,8 @@ pub fn parse_raw_upload(raw_upload_bytes: &[u8]) -> PyResult<(Vec, Vec)> let pos = reader.buffer_position(); let (line, col) = get_position_info(&decompressed_file_bytes, pos.try_into().unwrap()); ParserError::new_err(format!( - "Error parsing JUnit XML at {}:{}: {}", - line, col, e + "Error parsing JUnit XML in {} at {}:{}: {}", + file.filename, line, col, e )) })?; results.push(reader_result);