Skip to content

Commit 939a0b3

Browse files
parse eval properties
Adds the ability to parse "eval" properties given a specially crafted name.
1 parent 880ca79 commit 939a0b3

19 files changed

+605
-8
lines changed

benches/binary.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use rand::rngs::SmallRng;
66
use rand::seq::SliceRandom as _;
77
use rand::{Rng, SeedableRng};
88
use test_results_parser::binary::*;
9-
use test_results_parser::{Outcome, Testrun, ValidatedString};
9+
use test_results_parser::{Outcome, PropertiesValue, Testrun, ValidatedString};
1010

1111
criterion_group!(benches, binary);
1212
criterion_main!(benches);
@@ -184,6 +184,7 @@ fn create_random_testcases(
184184
filename: None,
185185
build_url: None,
186186
computed_name: ValidatedString::default(),
187+
properties: PropertiesValue(None),
187188
}
188189
})
189190
.collect();

src/binary/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ mod tests {
1919
use timestamps::DAY;
2020

2121
use crate::{
22-
testrun::{Outcome, Testrun},
22+
testrun::{Outcome, PropertiesValue, Testrun},
2323
validated_string::ValidatedString,
2424
};
2525

@@ -36,6 +36,7 @@ mod tests {
3636
filename: None,
3737
build_url: None,
3838
computed_name: ValidatedString::default(),
39+
properties: PropertiesValue(None),
3940
}
4041
}
4142

src/junit.rs

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
use anyhow::{Context, Result};
22
use pyo3::prelude::*;
3+
use serde_json::Value;
34
use std::collections::HashSet;
5+
use std::fmt;
46

57
use quick_xml::events::attributes::{Attribute, Attributes};
68
use quick_xml::events::{BytesStart, Event};
79
use quick_xml::reader::Reader;
810

911
use crate::compute_name::{compute_name, unescape_str};
10-
use crate::testrun::{check_testsuites_name, Framework, Outcome, Testrun};
12+
use crate::testrun::{check_testsuites_name, Framework, Outcome, PropertiesValue, Testrun};
1113
use crate::validated_string::ValidatedString;
1214
use crate::warning::WarningInfo;
1315
use thiserror::Error;
@@ -126,6 +128,7 @@ fn populate(
126128
filename: file,
127129
build_url: None,
128130
computed_name: ValidatedString::default(),
131+
properties: PropertiesValue(None),
129132
};
130133

131134
let framework = framework.or_else(|| t.framework());
@@ -158,11 +161,131 @@ pub fn get_position_info(input: &[u8], byte_offset: usize) -> (usize, usize) {
158161
(line, column)
159162
}
160163

164+
#[derive(Error, Debug)]
165+
struct NotEvalsPropertyError;
166+
167+
impl fmt::Display for NotEvalsPropertyError {
168+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169+
write!(f, "not evals property")
170+
}
171+
}
172+
173+
/// Parses the `property` element found in the `testcase` element.
174+
///
175+
/// This function is used to parse the `evals` attribute of the `testcase` element.
176+
/// It will update the `properties` field of the `testrun` object with the new value.
177+
///
178+
/// The `name` attribute in `property` encodes the hierarchy of the `value` attribute
179+
/// inside `Testrun.properties` (which is a JSON object).
180+
/// For example
181+
/// &lt;property name="evals.scores.isUseful.type" value="boolean" /&gt;
182+
/// &lt;property name="evals.scores.isUseful.value" value="true" /&gt;
183+
/// &lt;property name="evals.scores.isUseful.sum" value="1" /&gt;
184+
/// &lt;property name="evals.scores.isUseful.llm_judge" value="gemini_2.5pro" /&gt;
185+
///
186+
/// will be parsed as:
187+
/// {
188+
/// "scores": {
189+
/// "isUseful": {
190+
/// "type": "boolean",
191+
/// "value": "true",
192+
/// "sum": "1",
193+
/// "llm_judge": "gemini_2.5pro"
194+
/// }
195+
/// }
196+
/// }
197+
fn parse_property_element(e: &BytesStart, existing_properties: &mut PropertiesValue) -> Result<()> {
198+
// Early return if not an evals property
199+
let name = get_attribute(e, "name")?
200+
.filter(|n| n.starts_with("evals"))
201+
.ok_or(NotEvalsPropertyError)?;
202+
203+
let value = get_attribute(e, "value")?
204+
.ok_or_else(|| anyhow::anyhow!("Property must have value attribute"))?;
205+
206+
let name_parts: Vec<&str> = name.split(".").collect();
207+
if name_parts.len() < 2 {
208+
anyhow::bail!("Property name must have at least 2 parts");
209+
}
210+
211+
// Initialize properties if needed
212+
if existing_properties.0.is_none() {
213+
*existing_properties = PropertiesValue(Some(serde_json::json!({})));
214+
}
215+
216+
let mut current = existing_properties.0.as_mut().unwrap();
217+
218+
// Navigate through intermediate parts (skip first "evals" and last key)
219+
for part in &name_parts[1..name_parts.len() - 1] {
220+
current = match current {
221+
Value::Object(map) => {
222+
map.entry(part.to_string()).or_insert_with(|| {
223+
if *part == "evaluations" {
224+
serde_json::json!([])
225+
} else {
226+
serde_json::json!({})
227+
}
228+
});
229+
map.get_mut(*part).unwrap()
230+
}
231+
Value::Array(array) => {
232+
let idx = part
233+
.parse::<usize>()
234+
.map_err(|_| anyhow::anyhow!("Invalid array index: {}", part))?;
235+
if idx >= array.len() {
236+
array.resize(idx + 1, serde_json::json!({}));
237+
}
238+
array.get_mut(idx).unwrap()
239+
}
240+
_ => anyhow::bail!(
241+
"Cannot drill down into non-object/non-array value at part: {}",
242+
part
243+
),
244+
};
245+
}
246+
247+
// Set the final value
248+
match current {
249+
Value::Object(map) => {
250+
map.insert(name_parts.last().unwrap().to_string(), Value::String(value));
251+
}
252+
_ => anyhow::bail!("Cannot set value in non-object at final key"),
253+
}
254+
255+
Ok(())
256+
}
257+
161258
enum TestrunOrSkipped {
162259
Testrun(Testrun),
163260
Skipped,
164261
}
165262

263+
fn handle_property_element(
264+
e: &BytesStart,
265+
saved_testrun: &mut Option<TestrunOrSkipped>,
266+
buffer_position: u64,
267+
warnings: &mut Vec<WarningInfo>,
268+
) -> Result<()> {
269+
// Check if there is a testrun currently being processed
270+
if saved_testrun.is_none() {
271+
return Ok(());
272+
}
273+
let saved = saved_testrun
274+
.as_mut()
275+
.context("Error accessing saved testrun")?;
276+
if let TestrunOrSkipped::Testrun(testrun) = saved {
277+
if let Err(e) = parse_property_element(e, &mut testrun.properties) {
278+
if !e.is::<NotEvalsPropertyError>() {
279+
warnings.push(WarningInfo::new(
280+
format!("Error parsing `property` element: {}", e),
281+
buffer_position,
282+
));
283+
}
284+
}
285+
};
286+
Ok(())
287+
}
288+
166289
pub fn use_reader(
167290
reader: &mut Reader<&[u8]>,
168291
network: Option<&HashSet<String>>,
@@ -286,6 +409,12 @@ pub fn use_reader(
286409
let testsuites_name = get_attribute(&e, "name")?;
287410
framework = testsuites_name.and_then(|name| check_testsuites_name(&name))
288411
}
412+
b"property" => handle_property_element(
413+
&e,
414+
&mut saved_testrun,
415+
reader.buffer_position(),
416+
&mut warnings,
417+
)?,
289418
_ => {}
290419
},
291420
Event::End(e) => match e.name().as_ref() {
@@ -378,6 +507,12 @@ pub fn use_reader(
378507
TestrunOrSkipped::Skipped => {}
379508
}
380509
}
510+
b"property" => handle_property_element(
511+
&e,
512+
&mut saved_testrun,
513+
reader.buffer_position(),
514+
&mut warnings,
515+
)?,
381516
_ => {}
382517
},
383518
Event::Text(mut xml_failure_message) => {

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ mod testrun;
1010
mod validated_string;
1111
mod warning;
1212

13-
pub use testrun::{Outcome, Testrun};
13+
pub use testrun::{Outcome, PropertiesValue, Testrun};
1414
pub use validated_string::ValidatedString;
1515
pyo3::create_exception!(test_results_parser, ComputeNameError, PyException);
1616

src/snapshots/test_results_parser__raw_upload__tests__parse_raw_upload_success@ctest.xml.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
source: src/raw_upload.rs
3+
assertion_line: 173
34
expression: results
45
input_file: tests/ctest.xml
56
---
@@ -14,4 +15,5 @@ input_file: tests/ctest.xml
1415
filename: ~
1516
build_url: ~
1617
computed_name: "a_unit_test::a_unit_test"
18+
properties: ~
1719
warnings: []

src/snapshots/test_results_parser__raw_upload__tests__parse_raw_upload_success@empty_failure.junit.xml.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
source: src/raw_upload.rs
3+
assertion_line: 173
34
expression: results
45
input_file: tests/empty_failure.junit.xml
56
---
@@ -14,6 +15,7 @@ input_file: tests/empty_failure.junit.xml
1415
filename: "./test.rb"
1516
build_url: ~
1617
computed_name: "test.test::test.test works"
18+
properties: ~
1719
- name: test.test fails
1820
classname: test.test
1921
duration: 1
@@ -23,4 +25,5 @@ input_file: tests/empty_failure.junit.xml
2325
filename: "./test.rb"
2426
build_url: ~
2527
computed_name: "test.test::test.test fails"
28+
properties: ~
2629
warnings: []

src/snapshots/test_results_parser__raw_upload__tests__parse_raw_upload_success@jest-junit.xml.snap

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
source: src/raw_upload.rs
3+
assertion_line: 173
34
expression: results
45
input_file: tests/jest-junit.xml
56
---
@@ -14,6 +15,7 @@ input_file: tests/jest-junit.xml
1415
filename: ~
1516
build_url: ~
1617
computed_name: Title when rendered renders pull title
18+
properties: ~
1719
- name: Title when rendered renders pull author
1820
classname: Title when rendered renders pull author
1921
duration: 0.005
@@ -23,6 +25,7 @@ input_file: tests/jest-junit.xml
2325
filename: ~
2426
build_url: ~
2527
computed_name: Title when rendered renders pull author
28+
properties: ~
2629
- name: Title when rendered renders pull updatestamp
2730
classname: Title when rendered renders pull updatestamp
2831
duration: 0.002
@@ -32,6 +35,7 @@ input_file: tests/jest-junit.xml
3235
filename: ~
3336
build_url: ~
3437
computed_name: Title when rendered renders pull updatestamp
38+
properties: ~
3539
- name: Title when rendered for first pull request renders pull title
3640
classname: Title when rendered for first pull request renders pull title
3741
duration: 0.006
@@ -41,4 +45,5 @@ input_file: tests/jest-junit.xml
4145
filename: ~
4246
build_url: ~
4347
computed_name: Title when rendered for first pull request renders pull title
48+
properties: ~
4449
warnings: []

src/snapshots/test_results_parser__raw_upload__tests__parse_raw_upload_success@junit-nested-testsuite.xml.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
source: src/raw_upload.rs
3+
assertion_line: 173
34
expression: results
45
input_file: tests/junit-nested-testsuite.xml
56
---
@@ -14,6 +15,7 @@ input_file: tests/junit-nested-testsuite.xml
1415
filename: ~
1516
build_url: ~
1617
computed_name: "tests.test_parsers.TestParsers::test_junit[junit.xml--True]"
18+
properties: ~
1719
- name: "test_junit[jest-junit.xml--False]"
1820
classname: tests.test_parsers.TestParsers
1921
duration: 0.186
@@ -23,4 +25,5 @@ input_file: tests/junit-nested-testsuite.xml
2325
filename: ~
2426
build_url: ~
2527
computed_name: "tests.test_parsers.TestParsers::test_junit[jest-junit.xml--False]"
28+
properties: ~
2629
warnings: []

src/snapshots/test_results_parser__raw_upload__tests__parse_raw_upload_success@junit-no-testcase-timestamp.xml.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
source: src/raw_upload.rs
3+
assertion_line: 173
34
expression: results
45
input_file: tests/junit-no-testcase-timestamp.xml
56
---
@@ -14,6 +15,7 @@ input_file: tests/junit-no-testcase-timestamp.xml
1415
filename: ~
1516
build_url: ~
1617
computed_name: "tests.test_parsers.TestParsers::test_junit[junit.xml--True]"
18+
properties: ~
1719
- name: "test_junit[jest-junit.xml--False]"
1820
classname: tests.test_parsers.TestParsers
1921
duration: 0.186
@@ -23,4 +25,5 @@ input_file: tests/junit-no-testcase-timestamp.xml
2325
filename: ~
2426
build_url: ~
2527
computed_name: "tests.test_parsers.TestParsers::test_junit[jest-junit.xml--False]"
28+
properties: ~
2629
warnings: []

src/snapshots/test_results_parser__raw_upload__tests__parse_raw_upload_success@junit.xml.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
source: src/raw_upload.rs
3+
assertion_line: 173
34
expression: results
45
input_file: tests/junit.xml
56
---
@@ -14,6 +15,7 @@ input_file: tests/junit.xml
1415
filename: ~
1516
build_url: ~
1617
computed_name: "tests.test_parsers.TestParsers::test_junit[junit.xml--True]"
18+
properties: ~
1719
- name: "test_junit[jest-junit.xml--False]"
1820
classname: tests.test_parsers.TestParsers
1921
duration: 0.064
@@ -23,4 +25,5 @@ input_file: tests/junit.xml
2325
filename: ~
2426
build_url: ~
2527
computed_name: "tests.test_parsers.TestParsers::test_junit[jest-junit.xml--False]"
28+
properties: ~
2629
warnings: []

0 commit comments

Comments
 (0)