Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,15 @@
logger = logging.getLogger(__name__)


class TimeoutError(Exception):
"""Raised when file parsing exceeds timeout."""
class TimeoutError(BaseException):
"""Raised when file parsing exceeds timeout.

A ``BaseException`` on purpose: the language analyzers wrap many small
steps in ``except Exception`` blocks. If the alarm fired inside one of
them, the error was swallowed, the alarm was already spent, and a slow
file (e.g. a multi-megabyte vendored JS bundle) ran forever. Only
``_analyze_code_file`` catches it.
"""


@contextmanager
Expand Down
41 changes: 41 additions & 0 deletions tests/test_parse_timeout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""The per-file parse timeout must not be swallowed by an analyzer's own
``except Exception`` blocks."""

import signal
import time

import pytest

from codewiki.src.be.dependency_analyzer.analysis import call_graph_analyzer
from codewiki.src.be.dependency_analyzer.analysis.call_graph_analyzer import (
CallGraphAnalyzer,
)

pytestmark = pytest.mark.skipif(
not hasattr(signal, "SIGALRM"), reason="the timeout relies on SIGALRM"
)


def _swallowing_analyzer(self, file_path, content, repo_dir):
# Mimics a language analyzer that guards each small step with
# ``except Exception`` and loops for a very long time.
deadline = time.monotonic() + 30
while time.monotonic() < deadline:
try:
time.sleep(0.01)
except Exception: # noqa: BLE001
pass


def test_timeout_escapes_except_exception(tmp_path, monkeypatch):
(tmp_path / "slow.js").write_text("f();\n", encoding="utf-8")
monkeypatch.setattr(CallGraphAnalyzer, "_analyze_javascript_file", _swallowing_analyzer)

real_timeout = call_graph_analyzer.timeout
monkeypatch.setattr(call_graph_analyzer, "timeout", lambda seconds: real_timeout(1))

started = time.monotonic()
CallGraphAnalyzer()._analyze_code_file(
str(tmp_path), {"path": "slow.js", "language": "javascript"}
)
assert time.monotonic() - started < 10
Loading