154 lines
6.2 KiB
Python
154 lines
6.2 KiB
Python
"""Unit tests for runner.py error-handling behaviour.
|
|
|
|
Regression: when the JAR fails, the streaming branch already wrote the
|
|
JAR's stdout to the console live, so the except handler must not re-emit
|
|
the captured copy. The quiet branch, conversely, has not surfaced anything
|
|
yet and is allowed to print the captured streams — but only once
|
|
(``CalledProcessError.output`` and ``.stdout`` are the same attribute).
|
|
"""
|
|
|
|
import io
|
|
import subprocess
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from opendataloader_pdf import runner
|
|
|
|
|
|
class _FakeAsFile:
|
|
def __init__(self, path):
|
|
self._path = path
|
|
|
|
def __enter__(self):
|
|
return self._path
|
|
|
|
def __exit__(self, *_args):
|
|
return False
|
|
|
|
|
|
@pytest.fixture
|
|
def patched_jar(monkeypatch, tmp_path):
|
|
"""Bypass the real resources lookup so run_jar reaches subprocess."""
|
|
fake_jar = tmp_path / "opendataloader-pdf-cli.jar"
|
|
fake_jar.write_bytes(b"")
|
|
fake_traversable = MagicMock()
|
|
fake_traversable.joinpath = lambda *_a, **_kw: fake_jar
|
|
monkeypatch.setattr(runner.resources, "files", lambda _pkg: fake_traversable)
|
|
monkeypatch.setattr(runner.resources, "as_file", lambda p: _FakeAsFile(p))
|
|
|
|
|
|
def test_streaming_failure_does_not_duplicate_output(monkeypatch, capsys, patched_jar):
|
|
"""Streaming mode prints JAR output live; the except handler must not
|
|
re-emit the captured copy on stderr."""
|
|
jar_output = "Invalid page range format: '-10'\nusage: [options] ...\n"
|
|
|
|
fake_process = MagicMock()
|
|
fake_process.stdout = iter([jar_output])
|
|
fake_process.wait.return_value = 2
|
|
fake_process.__enter__ = lambda self: self
|
|
fake_process.__exit__ = lambda self, *_a: False
|
|
|
|
monkeypatch.setattr(runner.subprocess, "Popen", lambda *_a, **_kw: fake_process)
|
|
|
|
with pytest.raises(subprocess.CalledProcessError):
|
|
runner.run_jar(["--bogus"], quiet=False)
|
|
|
|
captured = capsys.readouterr()
|
|
# JAR text appears exactly once: the live streaming write.
|
|
assert "Invalid page range format" in captured.out
|
|
assert captured.out.count("usage: [options]") == 1
|
|
# The except handler did NOT re-emit the captured copy on stderr.
|
|
assert "Invalid page range format" not in captured.err
|
|
assert "usage: [options]" not in captured.err
|
|
# Meta info is still surfaced.
|
|
assert "Error running opendataloader-pdf CLI." in captured.err
|
|
assert "Return code: 2" in captured.err
|
|
|
|
|
|
def test_quiet_success_relays_stdout_but_not_stderr(monkeypatch, capsys, patched_jar):
|
|
"""Quiet mode must relay the JAR's stdout (--to-stdout payload, folder
|
|
summary line) to the caller while still suppressing the JAR's log
|
|
stream (stderr). Regression: --quiet + --to-stdout produced no output,
|
|
breaking pipe consumers."""
|
|
result = subprocess.CompletedProcess(
|
|
args=["java", "-jar", "fake.jar"],
|
|
returncode=0,
|
|
stdout="extracted text payload\n",
|
|
stderr="[INFO] java log noise\n",
|
|
)
|
|
monkeypatch.setattr(runner.subprocess, "run", MagicMock(return_value=result))
|
|
|
|
returned = runner.run_jar(["doc.pdf", "--quiet", "--to-stdout"], quiet=True)
|
|
|
|
captured = capsys.readouterr()
|
|
# Payload reaches the caller's stdout exactly once.
|
|
assert captured.out.count("extracted text payload") == 1
|
|
# The JAR's log stream stays suppressed (that is what quiet means).
|
|
assert "java log noise" not in captured.out
|
|
assert "java log noise" not in captured.err
|
|
# Return value is unchanged for library callers.
|
|
assert returned == "extracted text payload\n"
|
|
|
|
|
|
def test_quiet_relays_through_stdout_buffer_byte_path(monkeypatch, patched_jar):
|
|
"""The production CLI writes the relayed payload through
|
|
``sys.stdout.buffer`` (bytes), not the ``sys.stdout.write`` (str)
|
|
fallback. ``capsys`` replaces ``sys.stdout`` with an object whose
|
|
``.buffer`` semantics differ, so the sibling test only exercises the
|
|
str branch. This drives the real byte-relay path and asserts the
|
|
payload is encoded to UTF-8 and written exactly once, intact — covering
|
|
the ``encode("utf-8", "replace")`` step that the str branch skips.
|
|
"""
|
|
payload = "héllo 한글 payload\n" # non-ASCII: exercises the utf-8 encode
|
|
result = subprocess.CompletedProcess(
|
|
args=["java", "-jar", "fake.jar"],
|
|
returncode=0,
|
|
stdout=payload,
|
|
stderr="[INFO] java log noise\n",
|
|
)
|
|
monkeypatch.setattr(runner.subprocess, "run", MagicMock(return_value=result))
|
|
|
|
# Fake stdout with a real binary buffer, mirroring a genuine TextIOWrapper
|
|
# (hasattr(sys.stdout, "buffer") is True), so run_jar takes the byte path.
|
|
fake_stdout = MagicMock()
|
|
fake_stdout.buffer = io.BytesIO()
|
|
monkeypatch.setattr(runner.sys, "stdout", fake_stdout)
|
|
|
|
returned = runner.run_jar(["doc.pdf", "--quiet", "--to-stdout"], quiet=True)
|
|
|
|
written = fake_stdout.buffer.getvalue()
|
|
# Byte path was taken: payload written as UTF-8 exactly once, intact.
|
|
assert written == payload.encode("utf-8")
|
|
assert written.decode("utf-8") == payload
|
|
# The str fallback branch was NOT used.
|
|
fake_stdout.write.assert_not_called()
|
|
fake_stdout.buffer.flush() # buffer is flushed by run_jar; no error
|
|
# Return value is unchanged for library callers.
|
|
assert returned == payload
|
|
|
|
|
|
def test_quiet_failure_prints_captured_streams_once(monkeypatch, capsys, patched_jar):
|
|
"""Quiet mode captures output, so the except handler surfaces it — but
|
|
must avoid the old bug where Output and Stdout (aliases) both printed."""
|
|
error = subprocess.CalledProcessError(
|
|
returncode=2,
|
|
cmd=["java", "-jar", "fake.jar"],
|
|
output="captured stdout text",
|
|
stderr="captured stderr text",
|
|
)
|
|
monkeypatch.setattr(runner.subprocess, "run", MagicMock(side_effect=error))
|
|
|
|
with pytest.raises(subprocess.CalledProcessError):
|
|
runner.run_jar(["--bogus"], quiet=True)
|
|
|
|
err = capsys.readouterr().err
|
|
assert err.count("captured stdout text") == 1
|
|
assert err.count("captured stderr text") == 1
|
|
# The pre-fix code printed both "Output:" and "Stdout:" with the same text.
|
|
assert "Output:" not in err
|
|
assert "Stdout: captured stdout text" in err
|
|
assert "Stderr: captured stderr text" in err
|
|
assert "Error running opendataloader-pdf CLI." in err
|
|
assert "Return code: 2" in err
|