perspt_coding/diag/
pytest.rs1use perspt_sdk::ResidualClass;
10
11use super::types::StructuredDiagnostic;
12
13pub const PARSER_ID: &str = "pytest-junit-v1";
15
16pub fn parse(raw: &str) -> Vec<StructuredDiagnostic> {
18 let mut failures = Vec::new();
19 let mut rest = raw;
20 while let Some(start) = rest.find("<testcase") {
21 rest = &rest[start..];
22 let Some(tag_end) = rest.find('>') else { break };
23 let tag = &rest[..tag_end + 1];
24 let self_closing = tag.ends_with("/>");
25 let body_end = if self_closing {
26 tag_end + 1
27 } else {
28 match rest.find("</testcase>") {
29 Some(end) => end,
30 None => break,
31 }
32 };
33 let body = &rest[tag_end + 1..body_end];
34 if !self_closing && (body.contains("<failure") || body.contains("<error")) {
35 let classname = attribute(tag, "classname").unwrap_or_default();
36 let name = attribute(tag, "name").unwrap_or_default();
37 let test_id = if classname.is_empty() {
38 name.clone()
39 } else {
40 format!("{classname}::{name}")
41 };
42 failures.push(StructuredDiagnostic {
43 class: ResidualClass::TestFailure,
44 code: Some(test_id.clone()),
45 message: failure_message(body).unwrap_or_else(|| format!("{test_id} failed")),
46 path: attribute(tag, "file"),
47 line: attribute(tag, "line").and_then(|line| line.parse().ok()),
48 column: None,
49 primary: true,
50 suggestion: None,
51 });
52 }
53 rest = &rest[body_end..];
54 }
55 failures
56}
57
58fn attribute(tag: &str, name: &str) -> Option<String> {
59 let marker = format!("{name}=\"");
62 let mut search = 0;
63 loop {
64 let found = tag[search..].find(&marker)? + search;
65 let boundary = found == 0 || tag[..found].ends_with(char::is_whitespace);
66 if boundary {
67 let start = found + marker.len();
68 let end = tag[start..].find('"')? + start;
69 return Some(unescape(&tag[start..end]));
70 }
71 search = found + marker.len();
72 }
73}
74
75fn failure_message(body: &str) -> Option<String> {
76 let start = body.find("<failure").or_else(|| body.find("<error"))?;
77 let tag = &body[start..];
78 attribute(&tag[..tag.find('>')? + 1], "message")
79}
80
81fn unescape(value: &str) -> String {
82 value
83 .replace("<", "<")
84 .replace(">", ">")
85 .replace(""", "\"")
86 .replace("'", "'")
87 .replace("&", "&")
88}