mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 20:27:44 +02:00
* [haik]: ckpt, added in the test runner skelton i made in another repo -> still gotta make tweaks to fold it onto the current repo * [haik]: restructure backend test layout: move 37 test files from backend/tests/ into backend/tests/test_cases/, add backend/tests/run.sh one-command launcher that auto-provisions both runner and test venvs with stamp-based caching, update runner config.json to point test_paths at test_cases/ and fix venv_python path, revise runner README with run.sh usage and clearer setup instructions, and gitignore the .runner-venv directory * [haik]: refactor: reorganize backend/tests/test_cases from flat structure into domain subdirectories — moved 36 test files into auth/, browser/, labeling/, service/, settings/, and web_search/ for better discoverability and grouping * [haik]: overhaul test picker UI and add post-run rerun loop: replace checkbox glyphs with fzf-style row recolouring (coral=full, lighter coral=partial) and a right-pinned selection dot, add config-driven icon tiers (nerd/emoji/unicode/ascii) with per-glyph graceful degradation, replace the modal -k keyword screen with an inline live-filtering search bar that prunes the tree on every keystroke, add warm Anthropic-dark coral theme, toolbar flag chips replacing the old status line, and a floating help badge overlay; add rerun_prompt.py with inline Textual pill prompt (rerun all/failed/passed/exit) shown after each TTY run, wire it into main.py as a post-run loop; change run_tests to return (exit_code, RunSummary) tracking collected/passed/failed node IDs via new Dashboard methods; add icons field to config.py and config.json (set to nerd), document icon tiers and Nerd Font setup in README, set Hack Nerd Font in .vscode/settings.json, add .runner-venv to linter excludes
172 lines
5.3 KiB
Python
172 lines
5.3 KiB
Python
"""Subprocess entry point that runs pytest in the *test* venv.
|
|
|
|
Spawned by the parent runner as::
|
|
|
|
<venv_python> -m tests.runner._worker <event_fd> <options_json>
|
|
|
|
with ``cwd`` set to the repo root (PEP 420 makes ``tests.runner`` importable
|
|
without an ``__init__.py``, the same trick discovery uses). It registers a thin
|
|
pytest plugin that frames every collection/run event as JSON onto ``event_fd``,
|
|
optionally streams ``-s`` output through a gutter shim onto the same FD, and (for
|
|
``--cov``) measures coverage and emits the computed rows. The parent owns all
|
|
Rich rendering — this process imports neither rich nor textual.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import json
|
|
import os
|
|
import sys
|
|
from io import StringIO
|
|
|
|
import pytest
|
|
|
|
from tests.runner import events
|
|
|
|
|
|
class _GutterStream:
|
|
"""stdout/stderr shim used in ``-s`` mode.
|
|
|
|
Buffers writes and forwards each completed line as an OUTPUT event so the
|
|
parent can rail it under the current test's header. Mirrors the old
|
|
in-process shim, but emits framed events instead of printing.
|
|
"""
|
|
|
|
def __init__(self, fd: int) -> None:
|
|
self._fd = fd
|
|
self._buf = ""
|
|
|
|
def write(self, s) -> int:
|
|
if isinstance(s, bytes):
|
|
s = s.decode("utf-8", "replace")
|
|
self._buf += s
|
|
while "\n" in self._buf:
|
|
line, self._buf = self._buf.split("\n", 1)
|
|
events.emit(self._fd, {"type": events.OUTPUT, "line": line})
|
|
return len(s)
|
|
|
|
def flush(self) -> None:
|
|
if self._buf:
|
|
events.emit(self._fd, {"type": events.OUTPUT, "line": self._buf})
|
|
self._buf = ""
|
|
|
|
def isatty(self) -> bool:
|
|
return False
|
|
|
|
def writable(self) -> bool:
|
|
return True
|
|
|
|
def fileno(self): # some libraries probe this; signal "not a real fd"
|
|
raise io.UnsupportedOperation("fileno")
|
|
|
|
|
|
class _EventEmitter:
|
|
"""pytest plugin: forward collection/run hooks as framed events."""
|
|
|
|
def __init__(self, fd: int) -> None:
|
|
self._fd = fd
|
|
|
|
def pytest_collection_finish(self, session) -> None:
|
|
events.emit(
|
|
self._fd,
|
|
{"type": events.COLLECTION, "items": [item.nodeid for item in session.items]},
|
|
)
|
|
|
|
def pytest_runtest_logstart(self, nodeid, location) -> None:
|
|
events.emit(self._fd, {"type": events.LOGSTART, "nodeid": nodeid})
|
|
|
|
def pytest_runtest_logreport(self, report) -> None:
|
|
events.emit(
|
|
self._fd,
|
|
{
|
|
"type": events.LOGREPORT,
|
|
"nodeid": report.nodeid,
|
|
"when": report.when,
|
|
"passed": bool(report.passed),
|
|
"failed": bool(report.failed),
|
|
"skipped": bool(report.skipped),
|
|
"duration": float(getattr(report, "duration", 0.0) or 0.0),
|
|
"capstdout": report.capstdout or "",
|
|
"capstderr": report.capstderr or "",
|
|
"longreprtext": report.longreprtext or "",
|
|
},
|
|
)
|
|
|
|
def pytest_runtest_logfinish(self, nodeid, location) -> None:
|
|
events.emit(self._fd, {"type": events.LOGFINISH, "nodeid": nodeid})
|
|
|
|
|
|
def _emit_coverage(fd: int, cov, coverage_source: list[str], repo_root: str) -> None:
|
|
"""Compute the per-file coverage table and emit it (rendering is the parent's job)."""
|
|
try:
|
|
cov.stop()
|
|
cov.save()
|
|
total = cov.report(file=StringIO())
|
|
except Exception as exc: # pragma: no cover - defensive
|
|
events.emit(fd, {"type": events.COVERAGE, "error": str(exc)})
|
|
return
|
|
|
|
needles = [f"{os.sep}{name}{os.sep}" for name in coverage_source]
|
|
data = cov.get_data()
|
|
rows = []
|
|
for path in data.measured_files():
|
|
if not any(n in path for n in needles):
|
|
continue
|
|
try:
|
|
_, statements, _, missing, _ = cov.analysis2(path)
|
|
except Exception:
|
|
continue
|
|
n = len(statements)
|
|
if n == 0:
|
|
continue
|
|
miss = len(missing)
|
|
pct = (n - miss) / n * 100
|
|
rel = os.path.relpath(path, repo_root)
|
|
rows.append([rel, n, miss, pct])
|
|
|
|
events.emit(fd, {"type": events.COVERAGE, "rows": rows, "total": float(total)})
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
fd = int(argv[0])
|
|
opts = json.loads(argv[1])
|
|
|
|
pytest_args = opts.get("pytest_args", [])
|
|
no_capture = bool(opts.get("no_capture"))
|
|
cov = bool(opts.get("cov"))
|
|
coverage_source = opts.get("coverage_source") or []
|
|
repo_root = opts.get("repo_root") or os.getcwd()
|
|
|
|
cov_obj = None
|
|
if cov:
|
|
import coverage
|
|
|
|
cov_obj = coverage.Coverage(source=coverage_source)
|
|
cov_obj.start()
|
|
|
|
emitter = _EventEmitter(fd)
|
|
args = ["-o", "addopts=", "-p", "no:terminal", *pytest_args]
|
|
|
|
saved_out, saved_err = sys.stdout, sys.stderr
|
|
if no_capture:
|
|
sys.stdout = _GutterStream(fd)
|
|
sys.stderr = _GutterStream(fd)
|
|
try:
|
|
code = int(pytest.main(args, plugins=[emitter]))
|
|
finally:
|
|
if no_capture:
|
|
sys.stdout.flush()
|
|
sys.stderr.flush()
|
|
sys.stdout, sys.stderr = saved_out, saved_err
|
|
|
|
if cov_obj is not None:
|
|
_emit_coverage(fd, cov_obj, coverage_source, repo_root)
|
|
|
|
events.emit(fd, {"type": events.DONE, "code": code})
|
|
return code
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(sys.argv[1:]))
|