"""Endpoint-diff request/response recorder (pytest plugin form). The extract step copies this file to ``tests/integration/conftest.py`` so pytest auto-discovers it (xdist-safe -- every worker loads it), runs the endpoints suite once, then deletes the copy. It wraps ``requests.Session.request`` so every HTTP call the suite makes is logged together with the response it got. ``requests.get`` / ``requests.post`` and the shared session in ``tests/integration/request.py`` all funnel through ``Session.request``, so this single seam catches every request. Each pytest process (xdist worker) writes its own ``raw_.jsonl`` into ``$ENDPOINT_DIFF_OUT``; ``build_manifest.py`` merges and dedupes them. """ import base64 import json import os import threading from urllib.parse import urlsplit import requests.sessions _OUT = os.environ.get("ENDPOINT_DIFF_OUT", ".") # Record only calls to the dev server under test. The recorder patches requests # globally, so it also sees the test process's own SplitIO SDK / telemetry # traffic -- those go to other hosts (sdk.split.io, ...) and must be ignored. _TARGET_HOSTS = { h.strip() for h in os.environ.get("ENDPOINT_DIFF_TARGET_HOSTS", "localhost,127.0.0.1").split( "," ) if h.strip() } _orig_request = requests.sessions.Session.request _lock = threading.Lock() _records = [] def _jsonable(value): """Return value unchanged if JSON-serializable, else its repr.""" try: json.dumps(value) return value except (TypeError, ValueError): return repr(value) def _record_errored_request(method, url, kwargs, exc): """Record a request whose ``Session.request`` raised before returning. The retrying session in ``tests/integration/request.py`` raises a ``RetryError`` once it exhausts retries on a 5xx (``status_forcelist``). That 5xx never reaches the recorder as a response, so an endpoint that is *broken on the ref under test* makes no recordable round trip -- it silently drops out of the manifest and the before/after comparison never diffs it. Recording the attempt (with no response) keeps it in the manifest; ``replay.py`` re-issues it with a plain, non-retrying client, observes the real 5xx, and the regression surfaces as a STATUS diff. """ try: if urlsplit(str(url)).hostname not in _TARGET_HOSTS: return # SplitIO / telemetry / other non-server traffic record = { "method": str(method).upper(), "url": url, "params": _jsonable(kwargs.get("params")), "headers": _jsonable(dict(kwargs.get("headers") or {})), "json": _jsonable(kwargs.get("json")), "data": _jsonable(kwargs.get("data")) if kwargs.get("data") is not None else None, "status": None, "content_type": None, "resp_len": 0, "resp_body": None, "resp_body_b64": None, "request_error": repr(exc), } except Exception as rec_exc: # recording must never mask the real error record = { "method": str(method).upper(), "url": url, "record_error": repr(rec_exc), } with _lock: _records.append(record) def _recording_request(self, method, url, **kwargs): try: response = _orig_request(self, method, url, **kwargs) except Exception as exc: # Session.request raised before producing a response (e.g. the # retrying session raises RetryError after retrying a 5xx). Record # the attempt so the endpoint still enters the manifest, then # re-raise so the test fails exactly as it otherwise would. _record_errored_request(method, url, kwargs, exc) raise try: if urlsplit(str(url)).hostname not in _TARGET_HOSTS: return response # SplitIO / telemetry / other non-server traffic raw = response.content try: body, body_b64 = raw.decode("utf-8"), None except UnicodeDecodeError: body, body_b64 = None, base64.b64encode(raw).decode("ascii") record = { "method": str(method).upper(), "url": url, "params": _jsonable(kwargs.get("params")), "headers": _jsonable(dict(kwargs.get("headers") or {})), "json": _jsonable(kwargs.get("json")), "data": _jsonable(kwargs.get("data")) if kwargs.get("data") is not None else None, "status": response.status_code, "content_type": response.headers.get("Content-Type"), "resp_len": len(raw), "resp_body": body, "resp_body_b64": body_b64, } except Exception as exc: # recording must never break a test record = {"method": str(method).upper(), "url": url, "record_error": repr(exc)} with _lock: _records.append(record) return response def pytest_configure(config): requests.sessions.Session.request = _recording_request def pytest_unconfigure(config): worker = os.environ.get("PYTEST_XDIST_WORKER", "main") os.makedirs(_OUT, exist_ok=True) path = os.path.join(_OUT, f"raw_{worker}.jsonl") with _lock: with open(path, "w") as fh: for record in _records: fh.write(json.dumps(record) + "\n")