From 23fbb1d2876f7fc288921654b3c0de38b9632407 Mon Sep 17 00:00:00 2001 From: Gadi Evron Date: Thu, 9 Jul 2026 13:25:08 +0300 Subject: [PATCH] fix(parsers): bound the JS and Go parse subprocesses with a timeout _parse_javascript and _parse_go called subprocess.run with no timeout, while _parse_c / _parse_ruby / _parse_php / _parse_zig all pass timeout=1800. A hung inner Node/Go parser therefore blocked the Python parent indefinitely for any direct caller of parse_repository (library API, tests, tooling); the four already-timed parsers fail cleanly instead. - Add timeout=1800 (30 min) to the subprocess.run in _parse_javascript and _parse_go, matching the placement and value of the other four parsers. Parity/robustness change only. Verified against origin/master (98f5504): analyzer output is byte-identical with vs without the timeout on a real repo (a non-firing timeout is a no-op), and a runaway subprocess is killed cleanly with TimeoutExpired rather than hanging. Note: PR #98 bounds the OUTER Go-CLI -> python subprocess; this bounds the INNER python -> node/go parse subprocess one level deeper -- complementary, not redundant (direct-Python callers get no protection from #98). Regression test (fails on master, passes here): $ python -m pytest tests/test_parser_adapter_timeout.py -q 3 passed Co-Authored-By: Claude Opus 4.8 (1M context) --- libs/openant-core/core/parser_adapter.py | 2 + .../tests/test_parser_adapter_timeout.py | 64 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 libs/openant-core/tests/test_parser_adapter_timeout.py diff --git a/libs/openant-core/core/parser_adapter.py b/libs/openant-core/core/parser_adapter.py index cf221add..5df17124 100644 --- a/libs/openant-core/core/parser_adapter.py +++ b/libs/openant-core/core/parser_adapter.py @@ -579,6 +579,7 @@ def _parse_javascript(repo_path: str, output_dir: str, processing_level: str, sk stdout=sys.stderr, stderr=sys.stderr, cwd=str(_CORE_ROOT), + timeout=1800, # 30 min — parity with the C/Ruby/PHP/Zig parse subprocesses ) if result.returncode != 0: @@ -637,6 +638,7 @@ def _parse_go(repo_path: str, output_dir: str, processing_level: str, skip_tests stdout=sys.stderr, stderr=sys.stderr, cwd=str(_CORE_ROOT), + timeout=1800, # 30 min — parity with the C/Ruby/PHP/Zig parse subprocesses ) if result.returncode != 0: diff --git a/libs/openant-core/tests/test_parser_adapter_timeout.py b/libs/openant-core/tests/test_parser_adapter_timeout.py new file mode 100644 index 00000000..1b72952b --- /dev/null +++ b/libs/openant-core/tests/test_parser_adapter_timeout.py @@ -0,0 +1,64 @@ +"""Regression lock: the JS and Go parse subprocesses must pass a wall-clock timeout, +so a hung inner Node/Go parser fails cleanly instead of blocking the Python parent forever. + +C/Ruby/PHP/Zig already pass timeout=1800; JS and Go did not, so a direct-Python caller of +parse_repository / _parse_javascript / _parse_go had no bound. These tests fail on master +(no timeout) and pass with the fix. +""" +import inspect +import tempfile + +from core import parser_adapter + + +class _FakeCompleted: + returncode = 0 + + +def _capture_run_calls(monkeypatch): + calls = [] + + def fake_run(cmd, **kwargs): + calls.append((cmd, kwargs)) + return _FakeCompleted() + + monkeypatch.setattr(parser_adapter.subprocess, "run", fake_run) + return calls + + +def _parse_call_timeout(calls, script_name): + """timeout kwarg of the subprocess.run whose cmd invokes (the parse step).""" + for cmd, kwargs in calls: + if any(script_name in str(part) for part in cmd): + return kwargs.get("timeout") + return "NO-PARSE-CALL" + + +def test_js_parse_subprocess_has_timeout(monkeypatch): + calls = _capture_run_calls(monkeypatch) + d = tempfile.mkdtemp() + try: + parser_adapter._parse_javascript(d, d, "reachable") + except Exception: + pass # we only care that the parse subprocess.run received a timeout + assert _parse_call_timeout(calls, "test_pipeline.py") == 1800 + + +def test_go_parse_subprocess_has_timeout(monkeypatch): + calls = _capture_run_calls(monkeypatch) + d = tempfile.mkdtemp() + try: + parser_adapter._parse_go(d, d, "reachable") + except Exception: + pass + assert _parse_call_timeout(calls, "test_pipeline.py") == 1800 + + +def test_all_subprocess_parsers_are_uniformly_timed(): + """Every language whose *parse step* runs as a subprocess must carry a timeout — not just + 4 of them. (Python parses in-process, so it is intentionally excluded; the npm-install + dependency bootstrap in _ensure_js_parser_dependencies is a separate concern, not a parse.)""" + for lang in ("_parse_javascript", "_parse_go", "_parse_c", + "_parse_ruby", "_parse_php", "_parse_zig"): + src = inspect.getsource(getattr(parser_adapter, lang)) + assert "timeout=" in src, f"{lang} subprocess.run is missing a timeout"