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)