mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-06 08:16:43 +00:00
`path:line` cannot represent a path that itself contains a colon, and the one way pytest produces one is a Windows absolute location: separator normalization turns `C:\app\e2e\a2a\test_x.py` into `C:/app/...`, which slipped past the leading-slash check and composed the nonsense repo path `tests/e2e/C:/app/e2e/a2a/test_x.py`. Reject the colon itself rather than special-casing a drive letter: it is the character the format reserves, so no path containing one was ever linkable. Claude-Session: https://claude.ai/code/session_017dTKXwJkzhtVLzDhePHsKG
106 lines
4.5 KiB
Python
106 lines
4.5 KiB
Python
"""Custom per-test signals for the standard JUnit reporter.
|
|
|
|
The e2e suite ships results to Loki/Grafana from a standard pytest JUnit report
|
|
(`--junitxml=e2e-report.xml`), not a bespoke log line. JUnit already records
|
|
outcome, duration, and node id for every `<testcase>`; the signals it cannot
|
|
derive on its own are the normalized suite package, the coverage-registry cell
|
|
ids a test covers, and where the test's source lives. Those ride along as JUnit
|
|
`<property>` entries via each item's `user_properties`, attached in
|
|
`conftest.py::pytest_collection_modifyitems`.
|
|
|
|
`source` is a property rather than the `file=` / `line=` attributes pytest used
|
|
to write, because the `xunit2` family this suite runs on drops those, and
|
|
switching families would change the XML for every consumer of it -- the
|
|
Buildkite Test Engine upload and the Loki pipeline included.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterable
|
|
|
|
import pytest
|
|
|
|
# Hardcoded because the runner image copies tests/e2e/ to /app/e2e, so nothing
|
|
# at runtime names this suite's place in the repo. test_junit_properties.py
|
|
# fails from a checkout if it moves.
|
|
SUITE_ROOT = "tests/e2e"
|
|
|
|
|
|
def suite_parts(path_part: str) -> tuple[str, ...]:
|
|
"""Path components of a suite file relative to tests/e2e, however it ran.
|
|
|
|
Pytest paths are rootdir-relative, and rootdir moves with the invocation: a
|
|
repo-root run gives `tests/e2e/logging/test_x.py`, a suite-cwd run (the
|
|
runner image) gives `logging/test_x.py`. Both collapse to the same tuple.
|
|
"""
|
|
raw = tuple(p for p in path_part.replace("\\", "/").split("/") if p and p != ".")
|
|
return raw[2:] if len(raw) >= 3 and raw[0] == "tests" and raw[1] == "e2e" else raw
|
|
|
|
|
|
def package_from_nodeid(nodeid: str) -> str:
|
|
"""Top-level suite package under tests/e2e/, or 'root' for top-level files."""
|
|
parts = suite_parts(nodeid.split("::", 1)[0])
|
|
if len(parts) <= 1:
|
|
return "root"
|
|
return parts[0]
|
|
|
|
|
|
def source_from_location(path: str, lineno: int | None) -> str:
|
|
"""Repo-relative `path:line` for a test, or '' when nothing is linkable.
|
|
|
|
`pytest.Item.location` gives a rootdir-relative path and a ZERO-based line.
|
|
The path is re-rooted at SUITE_ROOT so consumers need not know how pytest was
|
|
started, and the line is emitted ONE-based to match editors, tracebacks and
|
|
code hosts. A decorated test anchors at its first decorator, which is where
|
|
pytest reports it.
|
|
|
|
Empty rather than a guess for anything unlinkable: no line, a path reaching
|
|
upward, or a path carrying a colon, which is both how an absolute Windows
|
|
path arrives and a character `path:line` has no way to represent.
|
|
"""
|
|
if lineno is None:
|
|
return ""
|
|
normalized = path.replace("\\", "/")
|
|
if normalized.startswith("/") or ":" in normalized or ".." in normalized.split("/"):
|
|
return ""
|
|
parts = suite_parts(normalized)
|
|
if not parts:
|
|
return ""
|
|
return f"{'/'.join((SUITE_ROOT, *parts))}:{lineno + 1}"
|
|
|
|
|
|
def source_from_item(item: pytest.Item) -> str:
|
|
"""Read the repo-relative `path:line` off a pytest Item's reported location."""
|
|
path, lineno, _ = item.location
|
|
return source_from_location(path, lineno)
|
|
|
|
|
|
def dedupe_covers(marker_args: Iterable[tuple[object, ...]]) -> tuple[str, ...]:
|
|
"""Flatten @pytest.mark.covers arg lists into unique, order-preserving cell
|
|
ids, dropping anything that is not a non-empty string."""
|
|
return tuple(dict.fromkeys(arg for args in marker_args for arg in args if isinstance(arg, str) and arg))
|
|
|
|
|
|
def covers_from_item(item: pytest.Item) -> tuple[str, ...]:
|
|
"""Read @pytest.mark.covers cell ids off a pytest Item, order-preserving."""
|
|
return dedupe_covers(marker.args for marker in item.iter_markers(name="covers"))
|
|
|
|
|
|
def result_properties(item: pytest.Item) -> tuple[tuple[str, str], ...]:
|
|
"""The custom signals a standard reporter cannot derive: the normalized suite
|
|
package, the comma-joined coverage-registry cell ids this test covers, and the
|
|
repo-relative `path:line` its source sits at."""
|
|
return (
|
|
("package", package_from_nodeid(item.nodeid)),
|
|
("covers", ",".join(covers_from_item(item))),
|
|
("source", source_from_item(item)),
|
|
)
|
|
|
|
|
|
def attach_result_properties(item: pytest.Item) -> None:
|
|
"""Attach result_properties to an item's user_properties, idempotently: a
|
|
second call is a no-op, so a collection that runs the hook more than once
|
|
never emits duplicate <property> entries."""
|
|
if any(name == "package" for name, _ in item.user_properties):
|
|
return
|
|
item.user_properties.extend(result_properties(item))
|