Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not allow exceptions to propagate from backend #40

Merged
merged 3 commits into from
Jan 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src/pyproject_api/_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,6 @@ def run(argv):
result["exc_msg"] = str(exception)
if not isinstance(exception, MissingCommand): # for missing command do not print stack
traceback.print_exc()
if not isinstance(exception, Exception): # allow SystemExit/KeyboardInterrupt to go through
raise
finally:
try:
with open(result_file, "w") as file_handler:
Expand Down
126 changes: 126 additions & 0 deletions tests/test_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from __future__ import annotations

import json
from pathlib import Path
from typing import Any

import pytest
import pytest_mock

from pyproject_api._backend import BackendProxy, run


def test_invalid_module(capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(ImportError):
run([str(False), "an.invalid.module"])

captured = capsys.readouterr()
assert "failed to start backend" in captured.err


def test_invalid_request(mocker: pytest_mock.MockerFixture, capsys: pytest.CaptureFixture[str]) -> None:
"""Validate behavior when an invalid request is issued."""
command = "invalid json"

backend_proxy = mocker.MagicMock(spec=BackendProxy)
backend_proxy.return_value = "dummy_result"
backend_proxy.__str__.return_value = "FakeBackendProxy"
mocker.patch("pyproject_api._backend.BackendProxy", return_value=backend_proxy)
mocker.patch("pyproject_api._backend.read_line", return_value=bytearray(command, "utf-8"))

ret = run([str(False), "a.dummy.module"])

assert ret == 0
captured = capsys.readouterr()
assert "started backend " in captured.out
assert "Backend: incorrect request to backend: " in captured.err


def test_exception(mocker: pytest_mock.MockerFixture, capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None:
"""Ensure an exception in the backend is not bubbled up."""
result = str(tmp_path / "result")
command = json.dumps({"cmd": "dummy_command", "kwargs": {"foo": "bar"}, "result": result})

backend_proxy = mocker.MagicMock(spec=BackendProxy)
backend_proxy.side_effect = SystemExit(1)
backend_proxy.__str__.return_value = "FakeBackendProxy"
mocker.patch("pyproject_api._backend.BackendProxy", return_value=backend_proxy)
mocker.patch("pyproject_api._backend.read_line", return_value=bytearray(command, "utf-8"))

ret = run([str(False), "a.dummy.module"])

# We still return 0 and write a result file. The exception should *not* bubble up
assert ret == 0
captured = capsys.readouterr()
assert "started backend FakeBackendProxy" in captured.out
assert "Backend: run command dummy_command with args {'foo': 'bar'}" in captured.out
assert "Backend: Wrote response " in captured.out
assert "SystemExit: 1" in captured.err


def test_valid_request(mocker: pytest_mock.MockerFixture, capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None:
"""Validate the "success" path."""
result = str(tmp_path / "result")
command = json.dumps({"cmd": "dummy_command", "kwargs": {"foo": "bar"}, "result": result})

backend_proxy = mocker.MagicMock(spec=BackendProxy)
backend_proxy.return_value = "dummy-result"
backend_proxy.__str__.return_value = "FakeBackendProxy"
mocker.patch("pyproject_api._backend.BackendProxy", return_value=backend_proxy)
mocker.patch("pyproject_api._backend.read_line", return_value=bytearray(command, "utf-8"))

ret = run([str(False), "a.dummy.module"])

assert ret == 0
captured = capsys.readouterr()
assert "started backend FakeBackendProxy" in captured.out
assert "Backend: run command dummy_command with args {'foo': 'bar'}" in captured.out
assert "Backend: Wrote response " in captured.out
assert "" == captured.err


def test_reuse_process(mocker: pytest_mock.MockerFixture, capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None:
"""Validate behavior when reusing the backend proxy process.

There are a couple of things we'd like to check here:

- Ensure we can actually reuse the process.
- Ensure an exception in a call to the backend does not affect subsequent calls.
- Ensure we can exit safely by calling the '_exit' command.
"""
results = [
str(tmp_path / "result_a"),
str(tmp_path / "result_b"),
str(tmp_path / "result_c"),
str(tmp_path / "result_d"),
]
commands = [
json.dumps({"cmd": "dummy_command_a", "kwargs": {"foo": "bar"}, "result": results[0]}),
json.dumps({"cmd": "dummy_command_b", "kwargs": {"baz": "qux"}, "result": results[1]}),
json.dumps({"cmd": "dummy_command_c", "kwargs": {"win": "wow"}, "result": results[2]}),
json.dumps({"cmd": "_exit", "kwargs": {}, "result": results[3]}),
]

def fake_backend(name: str, *args: Any, **kwargs: Any) -> Any: # noqa: U100
if name == "dummy_command_b":
raise SystemExit(2)

return "dummy-result"

backend_proxy = mocker.MagicMock(spec=BackendProxy)
backend_proxy.side_effect = fake_backend
backend_proxy.__str__.return_value = "FakeBackendProxy"
mocker.patch("pyproject_api._backend.BackendProxy", return_value=backend_proxy)
mocker.patch("pyproject_api._backend.read_line", side_effect=[bytearray(x, "utf-8") for x in commands])

ret = run([str(True), "a.dummy.module"])

# We still return 0 and write a result file. The exception should *not* bubble up and all commands should execute.
# It is the responsibility of the caller to handle errors.
assert ret == 0
captured = capsys.readouterr()
assert "started backend FakeBackendProxy" in captured.out
assert "Backend: run command dummy_command_a with args {'foo': 'bar'}" in captured.out
assert "Backend: run command dummy_command_b with args {'baz': 'qux'}" in captured.out
assert "Backend: run command dummy_command_c with args {'win': 'wow'}" in captured.out
assert "SystemExit: 2" in captured.err
File renamed without changes.
2 changes: 2 additions & 0 deletions whitelist.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
autoclass
autodoc
capsys
cfg
delenv
exe
Expand All @@ -17,6 +18,7 @@ py311
py38
pygments
pyproject
readouterr
sdist
setenv
tmpdir
Expand Down