From 63e900466cc1370287060c23d616d26af84a758c Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Tue, 11 Aug 2026 22:28:25 -0400 Subject: [PATCH] Report the real cause when jrunner fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --strict means a failure now exits non-zero, so the returncode branch raises instead of _detect_silent_failure. That branch used raw stderr, whose first line is always the JVM's "Picked up JAVA_TOOL_OPTIONS: ..." banner (_subprocess_env sets JAVA_TOOL_OPTIONS to keep jt400 headless). Every failed run would therefore have recorded that banner in run_log instead of the actual error — a regression against the old path, which extracted the exception header. _error_message strips the banner and prefers the Java exception header: org.postgresql.util.PSQLException: ERROR: relation "..." does not exist org.postgresql.util.PSQLException: ERROR: syntax error at or near "SELEKT" Co-Authored-By: Claude Opus 5 --- pipekit/jrunner.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/pipekit/jrunner.py b/pipekit/jrunner.py index dc36229..c9a9866 100644 --- a/pipekit/jrunner.py +++ b/pipekit/jrunner.py @@ -158,7 +158,7 @@ def query( _unlink_quietly(sql_path, pass_path) if r.returncode != 0: - raise JrunnerError(r.stderr.strip() or r.stdout.strip(), + raise JrunnerError(_error_message(r.stdout, r.stderr), stdout=r.stdout, stderr=r.stderr) silent = _detect_silent_failure(r.stdout, r.stderr) if silent: @@ -250,7 +250,7 @@ def migrate( _unlink_quietly(sql_path, pass_path) if proc.returncode != 0: - raise JrunnerError(stderr.strip() or stdout.strip(), + raise JrunnerError(_error_message(stdout, stderr), stdout=stdout, stderr=stderr) silent = _detect_silent_failure(stdout, stderr) if silent: @@ -317,6 +317,26 @@ def _parse_row_count(text: str) -> int | None: # nearly every failure site (see jrunner.java). Detect those by scanning # for a Java stack-trace signature so callers don't treat silent failures # as success. +_JVM_NOISE_RE = re.compile(r"^Picked up JAVA_TOOL_OPTIONS:.*$", re.M) + + +def _error_message(stdout: str, stderr: str) -> str: + """Best single-line cause for a failed jrunner call. + + The JVM prints "Picked up JAVA_TOOL_OPTIONS: ..." as the first stderr line + whenever JAVA_TOOL_OPTIONS is set (which _subprocess_env always does, to + keep jt400 headless), so raising raw stderr surfaces that banner instead of + the actual error. Prefer the Java exception header, which names the real + failure, and fall back to whatever output remains once the banner is gone. + """ + cleaned_err = _JVM_NOISE_RE.sub("", stderr or "").strip() + cleaned_out = _JVM_NOISE_RE.sub("", stdout or "").strip() + m = _EXCEPTION_HEADER_RE.search(cleaned_err) or _EXCEPTION_HEADER_RE.search(cleaned_out) + if m: + return m.group(0).strip() + return cleaned_err or cleaned_out or "jrunner failed with no output" + + _STACK_FRAME_RE = re.compile(r"^\s*at [\w.$<>]+\([^)\n]*\.java:\d+\)", re.M) _EXCEPTION_HEADER_RE = re.compile( r"^(?:[\w.$]+\.)*[\w$]+(?:Exception|Error)(?::[^\n]*)?$", re.M)