use perspt_sdk::ResidualClass;
use super::types::StructuredDiagnostic;
pub const PARSER_ID: &str = "pytest-junit-v1";
pub fn parse(raw: &str) -> Vec<StructuredDiagnostic> {
let mut failures = Vec::new();
let mut rest = raw;
while let Some(start) = rest.find("<testcase") {
rest = &rest[start..];
let Some(tag_end) = rest.find('>') else { break };
let tag = &rest[..tag_end + 1];
let self_closing = tag.ends_with("/>");
let body_end = if self_closing {
tag_end + 1
} else {
match rest.find("</testcase>") {
Some(end) => end,
None => break,
}
};
let body = &rest[tag_end + 1..body_end];
if !self_closing && (body.contains("<failure") || body.contains("<error")) {
let classname = attribute(tag, "classname").unwrap_or_default();
let name = attribute(tag, "name").unwrap_or_default();
let test_id = if classname.is_empty() {
name.clone()
} else {
format!("{classname}::{name}")
};
failures.push(StructuredDiagnostic {
class: ResidualClass::TestFailure,
code: Some(test_id.clone()),
message: failure_message(body).unwrap_or_else(|| format!("{test_id} failed")),
path: attribute(tag, "file"),
line: attribute(tag, "line").and_then(|line| line.parse().ok()),
column: None,
primary: true,
suggestion: None,
});
}
rest = &rest[body_end..];
}
failures
}
fn attribute(tag: &str, name: &str) -> Option<String> {
let marker = format!("{name}=\"");
let mut search = 0;
loop {
let found = tag[search..].find(&marker)? + search;
let boundary = found == 0 || tag[..found].ends_with(char::is_whitespace);
if boundary {
let start = found + marker.len();
let end = tag[start..].find('"')? + start;
return Some(unescape(&tag[start..end]));
}
search = found + marker.len();
}
}
fn failure_message(body: &str) -> Option<String> {
let start = body.find("<failure").or_else(|| body.find("<error"))?;
let tag = &body[start..];
attribute(&tag[..tag.find('>')? + 1], "message")
}
fn unescape(value: &str) -> String {
value
.replace("<", "<")
.replace(">", ">")
.replace(""", "\"")
.replace("'", "'")
.replace("&", "&")
}