"""Absolute transaction deadlines on the real process boundary (pre-push 4.1/4.2).""" from __future__ import annotations import contextlib import os import subprocess import sys import time from pathlib import Path import pytest from openultrasast.cpg.backend import BEGIN, END, JoernBackend from openultrasast.model.contracts import ExecutionBudget def test_expired_budget_launches_nothing(tmp_path: Path) -> None: calls = [] def run(command, **kwargs): calls.append(command) return subprocess.CompletedProcess(command, 0, "ok", "") backend = JoernBackend(runner=run, execution_budget=ExecutionBudget(time.monotonic() - 1, 0.2)) assert backend._run(["unused"], timeout=300) is None assert not calls assert "deadline_exhausted" in backend.last_failure def test_remaining_time_is_not_reset_between_commands() -> None: remaining = [] def run(command, **kwargs): remaining.append(kwargs["timeout"]) time.sleep(0.04) return subprocess.CompletedProcess(command, 0, "", "") backend = JoernBackend(runner=run, execution_budget=ExecutionBudget(time.monotonic() + 0.5, 0.2)) assert backend._run(["first"], timeout=300) is not None assert backend._run(["second"], timeout=300) is not None assert 0 < remaining[1] < remaining[0] <= 0.5 @pytest.mark.parametrize("leader_exits", [False, True]) def test_hanging_process_group_killed_inside_shared_allowance(tmp_path: Path, leader_exits: bool) -> None: source = tmp_path / "source.js" source.write_bytes(b"const input = req.query.input;\n") assert len(source.read_bytes()) == 31 pids = tmp_path / "pids" script = """import os,signal,time,sys signal.signal(signal.SIGTERM,signal.SIG_IGN) child=os.fork() if child==0: while True: time.sleep(1) open(sys.argv[1], 'w').write(str(os.getpid())+' '+str(child)) if sys.argv[2]=='yes': sys.exit(0) while True: time.sleep(1) """ started = time.monotonic() deadline = started + 0.25 backend = JoernBackend(execution_budget=ExecutionBudget(deadline, 0.3)) try: assert backend._run([sys.executable, "-c", script, str(pids), "yes" if leader_exits else "no"], timeout=1) is None assert time.monotonic() - started < 0.65 assert pids.is_file() for pid in map(int, pids.read_text().split()): status = Path(f"/proc/{pid}/stat") for _ in range(30): # Read, never check-then-read: a killed process can be reaped between the two, and its # disappearing is exactly the outcome this test wants, not an error. try: if status.read_text().split()[2] == "Z": break except FileNotFoundError: break time.sleep(0.005) else: pytest.fail(f"process {pid} survived cancellation") finally: if pids.exists(): for pid in map(int, pids.read_text().split()): with contextlib.suppress(ProcessLookupError): os.kill(pid, 9) @pytest.mark.parametrize("hang_stage", ["probe", "frontend", "overlay", "census", "taint"]) def test_real_hanging_engine_stages_share_deadline_and_cleanup(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, hang_stage: str) -> None: from openultrasast.cpg import backend as module root = tmp_path / "source" root.mkdir() source = root / "api.php" source.write_bytes(b" None: from openultrasast.cpg import backend as module calls = [] def run(command, **kwargs): calls.append(kwargs["timeout"]) if "--output" in command: Path(command[command.index("--output") + 1]).write_bytes(b"graph fixture") elif "-o" in command: Path(command[command.index("-o") + 1]).write_bytes(b"graph fixture") return subprocess.CompletedProcess(command, 0, f'{BEGIN}\n{{"r":[]}}\n{END}', "") monkeypatch.setattr(module.shutil, "which", lambda name: name) backend = JoernBackend(runner=run) budget = ExecutionBudget(time.monotonic() + 1, 0.2) graph = backend.build(tmp_path, language="javascript", execution_budget=budget) assert graph is not None and graph.run_batch is not None and graph.cleanup is not None assert backend.execution_budget is None time.sleep(0.02) assert graph.run_batch("taint", {"r": {}}) == {"r": []} assert 0 < calls[1] < calls[0] <= 1 graph.cleanup() assert not graph.cpg_path.parent.exists() def test_early_process_timeout_stops_fallback_and_retries(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: from openultrasast.cpg import backend as module calls = [] def run(command, **kwargs): calls.append(command) raise subprocess.TimeoutExpired(command, kwargs["timeout"]) monkeypatch.setattr(module.shutil, "which", lambda name: name) backend = JoernBackend(runner=run, execution_budget=ExecutionBudget(time.monotonic() + 100, 0.2)) assert backend.build(tmp_path, language="javascript") is None assert len(calls) == 1 assert "process_timeout" in backend._diagnostics def test_retry_timeout_consumes_same_absolute_budget(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: from openultrasast.cpg import backend as module calls = [] def run(command, **kwargs): calls.append(kwargs["timeout"]) Path(command[command.index("-o") + 1]).write_bytes(b"partial graph") time.sleep(0.04) return subprocess.CompletedProcess(command, 0, "WARN Failed to process 'x.php'", "") monkeypatch.setattr(module.shutil, "which", lambda name: name) backend = JoernBackend(runner=run, execution_budget=ExecutionBudget(time.monotonic() + 1, 0.2)) assert backend._build_with_retries_raw(tmp_path, tmp_path / "cpg.bin", tmp_path, "php") == ("x.php",) assert len(calls) == 4 assert all(a > b > 0 for a, b in zip(calls, calls[1:], strict=False)) assert calls[0] <= 1 def test_shard_failure_cannot_become_complete_empty_answer(tmp_path: Path) -> None: calls = [] def run(command, **kwargs): calls.append(command) if len(calls) == 2: raise subprocess.TimeoutExpired(command, kwargs["timeout"]) return subprocess.CompletedProcess(command, 0, f'{BEGIN}\n{{"r":[]}}\n{END}', "") backend = JoernBackend(runner=run, execution_budget=ExecutionBudget(time.monotonic() + 1, 0.2)) assert backend.query_batch_across([tmp_path / "a.bin", tmp_path / "b.bin", tmp_path / "c.bin"], "taint", {"r": {}}) is None assert len(calls) == 2 assert "shard_query_incomplete" in backend._diagnostics def test_cleanup_cannot_reset_expired_cancellation_allowance(tmp_path: Path) -> None: graph = tmp_path / "owned" graph.mkdir() (graph / "data").write_bytes(b"data") backend = JoernBackend(execution_budget=ExecutionBudget(time.monotonic() - 1, 0.1)) started = time.monotonic() backend._cleanup(graph, final=True) assert time.monotonic() - started < 0.05 assert graph.exists() assert "scratch_cleanup_incomplete" in backend._diagnostics def test_cleanup_does_not_follow_substituted_directory_links(tmp_path: Path) -> None: outside = tmp_path / "outside" outside.mkdir() (outside / "keep").write_bytes(b"keep") scratch = tmp_path / "scratch" scratch.mkdir() (scratch / "link").symlink_to(outside, target_is_directory=True) backend = JoernBackend(execution_budget=ExecutionBudget(time.monotonic() + 1, 0.2)) backend._cleanup(scratch, final=True) assert not scratch.exists() assert (outside / "keep").read_bytes() == b"keep" def test_driver_propagates_budget_and_rejects_legacy_without_launch(tmp_path: Path) -> None: (tmp_path / "input.py").write_bytes(b"print(1)\n") from openultrasast.cpg.backend import CpgResult from openultrasast.model.scan import scan_repository budget = ExecutionBudget(time.monotonic() + 1, 0.2) class Backend: seen = None cleaned = False def build(self, root, *, language, exclude, execution_budget): self.seen = execution_budget return CpgResult(tmp_path / "graph", lambda *args: [], cleanup=self.cleanup) def cleanup(self): self.cleaned = True backend = Backend() result = scan_repository(tmp_path, [], backend=backend, execution_budget=budget, client=object()) assert backend.seen is budget and backend.cleaned assert any(d["reason"] == "model_withheld_execution_budget" for d in result.degradations) class Legacy: called = False def build(self, root): self.called = True raise AssertionError("must not launch without a deadline") legacy = Legacy() result = scan_repository(tmp_path, [], backend=legacy, execution_budget=budget) assert not legacy.called assert any(d["reason"] == "cpg_build_failed" for d in result.degradations) def test_budgeted_driver_does_not_retry_internal_typeerror(tmp_path: Path) -> None: (tmp_path / "input.py").write_bytes(b"print(1)\n") from openultrasast.model.scan import scan_repository class Backend: calls = 0 def build(self, root, **kwargs): self.calls += 1 raise TypeError("internal backend bug") backend = Backend() with pytest.raises(TypeError, match="internal backend bug"): scan_repository(tmp_path, [], backend=backend, execution_budget=ExecutionBudget(time.monotonic() + 1, 0.2)) assert backend.calls == 1 def test_cancelled_graph_session_does_not_poison_next_build(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: from openultrasast.cpg import backend as module calls = [] def run(command, **kwargs): calls.append(command) if len(calls) == 1: raise subprocess.TimeoutExpired(command, kwargs["timeout"]) if "--output" in command: Path(command[command.index("--output") + 1]).write_bytes(b"graph") return subprocess.CompletedProcess(command, 0, f'{BEGIN}\n{{"r":[]}}\n{END}', "") monkeypatch.setattr(module.shutil, "which", lambda name: name) backend = JoernBackend(runner=run) budget = ExecutionBudget(time.monotonic() + 2, 0.2) assert backend.build(tmp_path, execution_budget=budget) is None assert backend._cancel_deadline is None graph = backend.build(tmp_path, execution_budget=budget) assert graph is not None and graph.cleanup is not None assert graph.execution_diagnostics is not None assert graph.execution_diagnostics() == () graph.cleanup() def test_driver_cleans_graph_on_unexpected_exception(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: (tmp_path / "input.py").write_bytes(b"print(1)\n") from openultrasast.cpg.backend import CpgResult from openultrasast.model import scan cleaned = [] class Backend: def build(self, root, **kwargs): return CpgResult(tmp_path / "graph", lambda *args: [], cleanup=lambda: cleaned.append(True)) def broken(*args, **kwargs): raise RuntimeError("broken context") monkeypatch.setattr(scan, "_hook_callbacks", broken) with pytest.raises(RuntimeError, match="broken context"): scan.scan_repository(tmp_path, [], backend=Backend(), execution_budget=ExecutionBudget(time.monotonic() + 1, 0.2)) assert cleaned == [True] def test_failed_nonbatch_query_is_unjudged_on_budgeted_path(tmp_path: Path) -> None: from openultrasast.cpg.backend import CpgResult from openultrasast.model.regions import ScanRegion from openultrasast.model.scan import scan_repository source = tmp_path / "api.py" source.write_bytes(b"def run(x): return x\n") assert len(source.read_bytes()) > 0 class Backend: def build(self, root, **kwargs): return CpgResult(tmp_path / "graph", lambda *args: None) region = ScanRegion(path="api.py", function="run", language="python", families=("injection",), rank=1.0, source="entry_point") result = scan_repository(tmp_path, [region], backend=Backend(), execution_budget=ExecutionBudget(time.monotonic() + 1, 0.2)) assert result.regions_scanned == 0 and result.regions_unjudged == 1 assert any(d["reason"] == "query_failed" for d in result.degradations)