mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
* test: add interactive Rust Python parity harness * test: simplify Rust Python parity harness structure * test: show parity confidence by SDK section --------- Co-authored-by: yuneng-jiang <yuneng@berri.ai>
160 lines
5.4 KiB
Python
160 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from collections.abc import Callable, Sequence
|
|
from pathlib import Path
|
|
from time import monotonic
|
|
|
|
import pytest
|
|
|
|
from .models import CaseResult, HarnessCase, HarnessRun, RunStatus
|
|
|
|
UpdateCallback = Callable[[HarnessRun], None]
|
|
|
|
|
|
def selector_matches_node(selector: str, nodeid: str) -> bool:
|
|
normalized_selector = selector.replace("\\", "/")
|
|
normalized_nodeid = nodeid.replace("\\", "/")
|
|
if "::" in normalized_selector:
|
|
return normalized_nodeid == normalized_selector or normalized_nodeid.startswith(
|
|
f"{normalized_selector}["
|
|
)
|
|
return normalized_nodeid == normalized_selector or normalized_nodeid.startswith(
|
|
f"{normalized_selector}::"
|
|
)
|
|
|
|
|
|
def selector_path(selector: str) -> Path:
|
|
return Path(selector.split("::", 1)[0])
|
|
|
|
|
|
def runnable_selectors(
|
|
cases: Sequence[HarnessCase], repo_root: Path
|
|
) -> tuple[str, ...]:
|
|
selectors = {
|
|
selector
|
|
for case in cases
|
|
for selector in case.selectors
|
|
if (repo_root / selector_path(selector)).exists()
|
|
}
|
|
return tuple(sorted(selectors))
|
|
|
|
|
|
class HarnessPytestPlugin:
|
|
def __init__(self, run: HarnessRun, on_update: UpdateCallback) -> None:
|
|
self.run = run
|
|
self.on_update = on_update
|
|
self.node_to_results: dict[str, list[CaseResult]] = {}
|
|
|
|
def _notify(self) -> None:
|
|
self.on_update(self.run)
|
|
|
|
def pytest_collection_modifyitems(self, items: list[pytest.Item]) -> None:
|
|
for item in items:
|
|
matched_results: list[CaseResult] = []
|
|
for result in self.run.results.values():
|
|
if any(
|
|
selector_matches_node(selector, item.nodeid)
|
|
for selector in result.case.selectors
|
|
):
|
|
result.collected.add(item.nodeid)
|
|
matched_results.append(result)
|
|
if matched_results:
|
|
self.node_to_results[item.nodeid] = matched_results
|
|
for result in self.run.results.values():
|
|
if result.status is RunStatus.QUEUED and not result.collected:
|
|
result.status = RunStatus.MISSING
|
|
self._notify()
|
|
|
|
def pytest_runtest_logstart(
|
|
self, nodeid: str, location: tuple[str, int | None, str]
|
|
) -> None:
|
|
del location
|
|
self.run.current_nodeid = nodeid
|
|
for result in self.node_to_results.get(nodeid, []):
|
|
if result.status not in {RunStatus.FAILED, RunStatus.ERROR}:
|
|
result.status = RunStatus.RUNNING
|
|
self._notify()
|
|
|
|
def pytest_runtest_logreport(self, report: pytest.TestReport) -> None:
|
|
if report.when not in {"setup", "call", "teardown"}:
|
|
return
|
|
results = self.node_to_results.get(report.nodeid, [])
|
|
if not results:
|
|
return
|
|
|
|
terminal = report.when == "call" or report.failed or report.skipped
|
|
if not terminal:
|
|
for result in results:
|
|
result.durations[report.nodeid] = (
|
|
result.durations.get(report.nodeid, 0.0) + report.duration
|
|
)
|
|
return
|
|
for result in results:
|
|
if report.when == "teardown" and not report.failed:
|
|
result.durations[report.nodeid] = (
|
|
result.durations.get(report.nodeid, 0.0) + report.duration
|
|
)
|
|
continue
|
|
if report.skipped:
|
|
status = RunStatus.SKIPPED
|
|
elif report.failed and report.when in {"setup", "teardown"}:
|
|
status = RunStatus.ERROR
|
|
elif report.failed:
|
|
status = RunStatus.FAILED
|
|
else:
|
|
status = RunStatus.PASSED
|
|
result.record(report.nodeid, status, report.duration)
|
|
if report.failed:
|
|
failure = (report.nodeid, str(report.longrepr))
|
|
if failure not in self.run.failures:
|
|
self.run.failures.append(failure)
|
|
self._notify()
|
|
|
|
def pytest_sessionfinish(
|
|
self, session: pytest.Session, exitstatus: int | pytest.ExitCode
|
|
) -> None:
|
|
del session, exitstatus
|
|
self.run.current_nodeid = None
|
|
self.run.finished_at = monotonic()
|
|
for result in self.run.results.values():
|
|
result.finalize()
|
|
self._notify()
|
|
|
|
|
|
def run_pytest(
|
|
cases: Sequence[HarnessCase],
|
|
repo_root: Path,
|
|
on_update: UpdateCallback,
|
|
pytest_args: Sequence[str] = (),
|
|
) -> tuple[int, HarnessRun]:
|
|
run = HarnessRun.from_cases(cases)
|
|
selectors = runnable_selectors(cases, repo_root)
|
|
if not selectors:
|
|
for result in run.results.values():
|
|
result.finalize()
|
|
run.finished_at = monotonic()
|
|
on_update(run)
|
|
has_missing_test = any(
|
|
result.status is RunStatus.MISSING for result in run.results.values()
|
|
)
|
|
exit_code = (
|
|
int(pytest.ExitCode.TESTS_FAILED)
|
|
if has_missing_test
|
|
else int(pytest.ExitCode.OK)
|
|
)
|
|
return exit_code, run
|
|
|
|
plugin = HarnessPytestPlugin(run=run, on_update=on_update)
|
|
args = [*selectors, "-p", "no:terminal", *pytest_args]
|
|
previous_directory = Path.cwd()
|
|
try:
|
|
os.chdir(repo_root)
|
|
exit_code = int(pytest.main(args, plugins=[plugin]))
|
|
finally:
|
|
os.chdir(previous_directory)
|
|
if exit_code == 0 and any(
|
|
result.status is RunStatus.MISSING for result in run.results.values()
|
|
):
|
|
exit_code = int(pytest.ExitCode.TESTS_FAILED)
|
|
return exit_code, run
|