diff --git a/src/openultrasast/cpg/backend.py b/src/openultrasast/cpg/backend.py index b1cd311..89244b4 100644 --- a/src/openultrasast/cpg/backend.py +++ b/src/openultrasast/cpg/backend.py @@ -1129,7 +1129,7 @@ class JoernBackend: command += ["--param", f"{name}={value}"] completed = self._run(command, timeout=self.query_timeout, cwd=cpg_path.parent) if completed is None or completed.returncode != 0: - detail = (completed.stderr or "")[-400:] if completed is not None else "timeout" + detail = _engine_failure(completed) logger.warning("cpg batch %s failed: %s", query, detail) return None body = completed.stdout or "" @@ -1162,7 +1162,7 @@ class JoernBackend: # it can never land in the repository being analysed. completed = self._run(command, timeout=self.query_timeout, cwd=cpg_path.parent) if completed is None or completed.returncode != 0: - detail = (completed.stderr or "")[-400:] if completed is not None else "timeout" + detail = _engine_failure(completed) logger.warning("cpg query %s failed: %s", query, detail) return None body = completed.stdout or "" @@ -1541,6 +1541,23 @@ def _configured_timeout() -> int: return QUERY_TIMEOUT_SECONDS +def _engine_failure(completed: subprocess.CompletedProcess[str] | None) -> str: + """What the engine said, keeping the line that names the cause. + + A stack trace's exception is on its FIRST line and its frames follow, so logging only the tail + reports frames and discards the cause. Four PHP investigations were spent on `cpg batch taint + failed: dure1.java:10)`, which is the middle of a frame and says nothing at all. + """ + if completed is None: + return "timeout" + text = (completed.stderr or completed.stdout or "").strip() + if not text: + return f"exit {completed.returncode} with no output" + lines = [line for line in text.splitlines() if line.strip() and not line.strip().startswith("at ")] + head = " | ".join(lines[:4])[:600] + return head or text[:300] + + def _render(value: object) -> str: """CPGQL parameters are strings; a sequence becomes a comma-separated list the script splits.""" if isinstance(value, str): diff --git a/tests/test_cpg_backend.py b/tests/test_cpg_backend.py index 6a45803..386ca32 100644 --- a/tests/test_cpg_backend.py +++ b/tests/test_cpg_backend.py @@ -1217,3 +1217,16 @@ def test_a_batch_no_part_can_answer_is_still_unavailable(tmp_path, monkeypatch): graph.write_bytes(b"graph") # None means "could not ask", which must never become an empty answer. assert backend.query_batch(graph, "taint", {str(i): {"file": f"f{i}.php"} for i in range(8)}) is None + + +def test_an_engine_failure_reports_the_line_that_names_the_cause(tmp_path, monkeypatch): + """A stack trace's exception is on its first line; logging the tail reports frames only.""" + from openultrasast.cpg.backend import JoernBackend, _engine_failure + + trace = "java.lang.OutOfMemoryError: Java heap space\n\tat foo.Bar.baz(Bar.java:10)\n\tat x.Procedure1.java:10)" + named = _engine_failure(subprocess.CompletedProcess(["joern"], 1, "", trace)) + assert "OutOfMemoryError" in named and "Java heap space" in named + assert "Bar.java:10" not in named, "frames are noise; the cause is not" + assert _engine_failure(None) == "timeout" + assert "no output" in _engine_failure(subprocess.CompletedProcess(["joern"], 3, "", "")) + assert isinstance(JoernBackend(), JoernBackend)