Report the real cause when jrunner fails

--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 <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-08-11 22:28:25 -04:00
parent 002deb8ad6
commit 63e900466c

View File

@ -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)