mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-04 20:03:40 +00:00
217 lines
6.8 KiB
Python
217 lines
6.8 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Dependency-free test runner for hosts without pytest.
|
||
|
|
|
||
|
|
Discovers tests/test_*.py, runs every test_* function, and supports the
|
||
|
|
small pytest surface this suite uses: the tmp_path fixture, pytest.raises,
|
||
|
|
pytest.skip, and pytest.mark.* as no-ops. Prefer real pytest when it is
|
||
|
|
installed; this shim exists so `python3 scripts/mini_pytest.py` works on a
|
||
|
|
bare interpreter (the same portability posture as the rest of pacta).
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import importlib.util
|
||
|
|
import inspect
|
||
|
|
import sys
|
||
|
|
import tempfile
|
||
|
|
import traceback
|
||
|
|
import types
|
||
|
|
from contextlib import contextmanager
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
ROOT = Path(__file__).resolve().parent.parent
|
||
|
|
for entry in (ROOT / "src", ROOT / "provider" / "src", ROOT):
|
||
|
|
sys.path.insert(0, str(entry))
|
||
|
|
|
||
|
|
|
||
|
|
class _Skip(Exception):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
def _install_pytest_stub() -> None:
|
||
|
|
if "pytest" in sys.modules:
|
||
|
|
return
|
||
|
|
stub = types.ModuleType("pytest")
|
||
|
|
|
||
|
|
@contextmanager
|
||
|
|
def raises(exc_type, match=None):
|
||
|
|
class _Ctx:
|
||
|
|
value = None
|
||
|
|
|
||
|
|
ctx = _Ctx()
|
||
|
|
try:
|
||
|
|
yield ctx
|
||
|
|
except exc_type as exc: # noqa: PERF203
|
||
|
|
ctx.value = exc
|
||
|
|
if match is not None:
|
||
|
|
import re
|
||
|
|
|
||
|
|
if not re.search(match, str(exc)):
|
||
|
|
raise AssertionError(f"exception message {exc!r} does not match {match!r}") from exc
|
||
|
|
return
|
||
|
|
raise AssertionError(f"expected {exc_type.__name__} was not raised")
|
||
|
|
|
||
|
|
def skip(reason=""):
|
||
|
|
raise _Skip(reason)
|
||
|
|
|
||
|
|
class _Mark:
|
||
|
|
def __getattr__(self, _name):
|
||
|
|
def deco(fn=None, *a, **k):
|
||
|
|
return fn if fn is not None else deco
|
||
|
|
|
||
|
|
return deco
|
||
|
|
|
||
|
|
stub.raises = raises
|
||
|
|
stub.skip = skip
|
||
|
|
stub.mark = _Mark()
|
||
|
|
stub.approx = lambda v, **k: v
|
||
|
|
sys.modules["pytest"] = stub
|
||
|
|
|
||
|
|
|
||
|
|
class _MonkeyPatch:
|
||
|
|
def __init__(self):
|
||
|
|
self._attr: list[tuple[object, str, object, bool]] = []
|
||
|
|
self._env: list[tuple[str, str | None]] = []
|
||
|
|
|
||
|
|
def setattr(self, target, name, value=None):
|
||
|
|
import os
|
||
|
|
|
||
|
|
if value is None and isinstance(target, str):
|
||
|
|
# pytest-style dotted target: import the longest importable
|
||
|
|
# prefix, then walk attributes (handles module-as-attribute
|
||
|
|
# forms like "pkg.module.shutil.which").
|
||
|
|
parts = target.split(".")
|
||
|
|
obj = None
|
||
|
|
for split in range(len(parts) - 1, 0, -1):
|
||
|
|
try:
|
||
|
|
obj = importlib.import_module(".".join(parts[:split]))
|
||
|
|
except ModuleNotFoundError:
|
||
|
|
continue
|
||
|
|
for attr_name in parts[split:-1]:
|
||
|
|
obj = getattr(obj, attr_name)
|
||
|
|
break
|
||
|
|
if obj is None:
|
||
|
|
raise ModuleNotFoundError(target)
|
||
|
|
target, name, value = obj, parts[-1], name
|
||
|
|
had = hasattr(target, name)
|
||
|
|
self._attr.append((target, name, getattr(target, name, None), had))
|
||
|
|
setattr(target, name, value)
|
||
|
|
|
||
|
|
def setenv(self, name, value):
|
||
|
|
import os
|
||
|
|
|
||
|
|
self._env.append((name, os.environ.get(name)))
|
||
|
|
os.environ[name] = value
|
||
|
|
|
||
|
|
def delenv(self, name, raising=True):
|
||
|
|
import os
|
||
|
|
|
||
|
|
self._env.append((name, os.environ.get(name)))
|
||
|
|
os.environ.pop(name, None if not raising else os.environ[name] and None)
|
||
|
|
|
||
|
|
def undo(self):
|
||
|
|
import os
|
||
|
|
|
||
|
|
for target, name, old, had in reversed(self._attr):
|
||
|
|
if had:
|
||
|
|
setattr(target, name, old)
|
||
|
|
else:
|
||
|
|
delattr(target, name)
|
||
|
|
for name, old in reversed(self._env):
|
||
|
|
if old is None:
|
||
|
|
os.environ.pop(name, None)
|
||
|
|
else:
|
||
|
|
os.environ[name] = old
|
||
|
|
|
||
|
|
|
||
|
|
class _CapSys:
|
||
|
|
def __init__(self):
|
||
|
|
import io
|
||
|
|
|
||
|
|
self._out, self._err = io.StringIO(), io.StringIO()
|
||
|
|
self._old = (sys.stdout, sys.stderr)
|
||
|
|
sys.stdout, sys.stderr = self._out, self._err
|
||
|
|
|
||
|
|
def readouterr(self):
|
||
|
|
out, err = self._out.getvalue(), self._err.getvalue()
|
||
|
|
self._out.truncate(0), self._out.seek(0)
|
||
|
|
self._err.truncate(0), self._err.seek(0)
|
||
|
|
return types.SimpleNamespace(out=out, err=err)
|
||
|
|
|
||
|
|
def stop(self):
|
||
|
|
sys.stdout, sys.stderr = self._old
|
||
|
|
|
||
|
|
|
||
|
|
def _load(path: Path) -> types.ModuleType:
|
||
|
|
spec = importlib.util.spec_from_file_location(path.stem, path)
|
||
|
|
module = importlib.util.module_from_spec(spec)
|
||
|
|
assert spec.loader is not None
|
||
|
|
spec.loader.exec_module(module)
|
||
|
|
return module
|
||
|
|
|
||
|
|
|
||
|
|
def main(argv: list[str]) -> int:
|
||
|
|
_install_pytest_stub()
|
||
|
|
pattern = argv[1] if len(argv) > 1 else ""
|
||
|
|
passed = failed = skipped = 0
|
||
|
|
failures: list[str] = []
|
||
|
|
for path in sorted((ROOT / "tests").glob("test_*.py")):
|
||
|
|
if pattern and pattern not in path.name:
|
||
|
|
continue
|
||
|
|
try:
|
||
|
|
module = _load(path)
|
||
|
|
except Exception:
|
||
|
|
failed += 1
|
||
|
|
failures.append(f"{path.name}: import error\n{traceback.format_exc()}")
|
||
|
|
continue
|
||
|
|
for name in sorted(dir(module)):
|
||
|
|
if not name.startswith("test_"):
|
||
|
|
continue
|
||
|
|
fn = getattr(module, name)
|
||
|
|
if not callable(fn):
|
||
|
|
continue
|
||
|
|
kwargs = {}
|
||
|
|
params = inspect.signature(fn).parameters
|
||
|
|
tmp = None
|
||
|
|
cleanups = []
|
||
|
|
if "tmp_path" in params:
|
||
|
|
tmp = tempfile.TemporaryDirectory(prefix="mini-pytest-")
|
||
|
|
kwargs["tmp_path"] = Path(tmp.name)
|
||
|
|
if "monkeypatch" in params:
|
||
|
|
mp = _MonkeyPatch()
|
||
|
|
kwargs["monkeypatch"] = mp
|
||
|
|
cleanups.append(mp.undo)
|
||
|
|
if "capsys" in params:
|
||
|
|
cap = _CapSys()
|
||
|
|
kwargs["capsys"] = cap
|
||
|
|
cleanups.append(cap.stop)
|
||
|
|
unknown = [p for p in params if p not in kwargs]
|
||
|
|
if unknown:
|
||
|
|
skipped += 1
|
||
|
|
print(f"s {path.name}::{name} (unsupported fixtures: {unknown})")
|
||
|
|
continue
|
||
|
|
try:
|
||
|
|
fn(**kwargs)
|
||
|
|
passed += 1
|
||
|
|
print(f". {path.name}::{name}")
|
||
|
|
except _Skip as exc:
|
||
|
|
skipped += 1
|
||
|
|
print(f"s {path.name}::{name} ({exc})")
|
||
|
|
except Exception:
|
||
|
|
failed += 1
|
||
|
|
failures.append(f"{path.name}::{name}\n{traceback.format_exc()}")
|
||
|
|
print(f"F {path.name}::{name}")
|
||
|
|
finally:
|
||
|
|
for cleanup in cleanups:
|
||
|
|
cleanup()
|
||
|
|
if tmp is not None:
|
||
|
|
tmp.cleanup()
|
||
|
|
print(f"\n{passed} passed, {failed} failed, {skipped} skipped")
|
||
|
|
for failure in failures:
|
||
|
|
print("\n" + "=" * 70)
|
||
|
|
print(failure)
|
||
|
|
return 1 if failed else 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
raise SystemExit(main(sys.argv))
|