|
24 | 24 | import math |
25 | 25 | import re |
26 | 26 | import xml.etree.ElementTree as ET |
27 | | -from pathlib import Path |
| 27 | +from pathlib import Path, PurePosixPath |
28 | 28 | from typing import Any, Literal |
29 | 29 |
|
30 | 30 | from .evaluation.judge_context import truncate |
@@ -235,20 +235,32 @@ def _task_case(row: dict[str, Any], run_dir: Path) -> ET.Element: |
235 | 235 | return case |
236 | 236 |
|
237 | 237 |
|
| 238 | +def _skipped_name(path: str) -> str: |
| 239 | + """Stable testcase name for a skipped task: suffix-stripped, ``/``-separated. |
| 240 | +
|
| 241 | + Uses the whole path rather than just the stem, because two skipped tasks |
| 242 | + sharing a basename (``suiteA/task.yaml``, ``suiteB/task.yaml``) would |
| 243 | + otherwise collapse into one identity that some JUnit ingesters merge. |
| 244 | +
|
| 245 | + Separators are normalized to ``/`` and the path is parsed with |
| 246 | + ``PurePosixPath`` so the emitted name does not depend on the OS that |
| 247 | + generated the report — the same logical run must produce the same testcase |
| 248 | + identity on Windows and Linux, or CI history/flake tracking splits in two. |
| 249 | + """ |
| 250 | + return str(PurePosixPath(path.replace("\\", "/")).with_suffix("")) |
| 251 | + |
| 252 | + |
238 | 253 | def _skipped_suite(summary: RunSummary) -> ET.Element | None: |
239 | 254 | """Build the synthetic ``skipped`` testsuite from ``RunSummary.skipped_tasks``.""" |
240 | 255 | if not summary.skipped_tasks: |
241 | 256 | return None |
242 | 257 | suite = ET.Element("testsuite", {"name": "skipped"}) |
243 | 258 | cases: list[ET.Element] = [] |
244 | 259 | for entry in summary.skipped_tasks: |
245 | | - # Use the suffix-stripped PATH, not just the stem: two skipped tasks |
246 | | - # sharing a basename (suiteA/task.yaml, suiteB/task.yaml) would otherwise |
247 | | - # collapse into one identity that some JUnit ingesters merge. |
248 | 260 | case = ET.SubElement( |
249 | 261 | suite, |
250 | 262 | "testcase", |
251 | | - {"name": _xml_safe(str(Path(entry.path).with_suffix(""))), "classname": "skipped"}, |
| 263 | + {"name": _xml_safe(_skipped_name(entry.path)), "classname": "skipped"}, |
252 | 264 | ) |
253 | 265 | ET.SubElement(case, "skipped", {"message": _xml_safe(entry.reason)}) |
254 | 266 | cases.append(case) |
|
0 commit comments