Skip to content

Commit 0ddcee1

Browse files
authored
Do not allow exceptions to propagate from backend (#40)
1 parent b482a05 commit 0ddcee1

File tree

4 files changed

+128
-2
lines changed

4 files changed

+128
-2
lines changed

src/pyproject_api/_backend.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,6 @@ def run(argv):
9797
result["exc_msg"] = str(exception)
9898
if not isinstance(exception, MissingCommand): # for missing command do not print stack
9999
traceback.print_exc()
100-
if not isinstance(exception, Exception): # allow SystemExit/KeyboardInterrupt to go through
101-
raise
102100
finally:
103101
try:
104102
with open(result_file, "w") as file_handler:

tests/test_backend.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from pathlib import Path
5+
from typing import Any
6+
7+
import pytest
8+
import pytest_mock
9+
10+
from pyproject_api._backend import BackendProxy, run
11+
12+
13+
def test_invalid_module(capsys: pytest.CaptureFixture[str]) -> None:
14+
with pytest.raises(ImportError):
15+
run([str(False), "an.invalid.module"])
16+
17+
captured = capsys.readouterr()
18+
assert "failed to start backend" in captured.err
19+
20+
21+
def test_invalid_request(mocker: pytest_mock.MockerFixture, capsys: pytest.CaptureFixture[str]) -> None:
22+
"""Validate behavior when an invalid request is issued."""
23+
command = "invalid json"
24+
25+
backend_proxy = mocker.MagicMock(spec=BackendProxy)
26+
backend_proxy.return_value = "dummy_result"
27+
backend_proxy.__str__.return_value = "FakeBackendProxy"
28+
mocker.patch("pyproject_api._backend.BackendProxy", return_value=backend_proxy)
29+
mocker.patch("pyproject_api._backend.read_line", return_value=bytearray(command, "utf-8"))
30+
31+
ret = run([str(False), "a.dummy.module"])
32+
33+
assert ret == 0
34+
captured = capsys.readouterr()
35+
assert "started backend " in captured.out
36+
assert "Backend: incorrect request to backend: " in captured.err
37+
38+
39+
def test_exception(mocker: pytest_mock.MockerFixture, capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None:
40+
"""Ensure an exception in the backend is not bubbled up."""
41+
result = str(tmp_path / "result")
42+
command = json.dumps({"cmd": "dummy_command", "kwargs": {"foo": "bar"}, "result": result})
43+
44+
backend_proxy = mocker.MagicMock(spec=BackendProxy)
45+
backend_proxy.side_effect = SystemExit(1)
46+
backend_proxy.__str__.return_value = "FakeBackendProxy"
47+
mocker.patch("pyproject_api._backend.BackendProxy", return_value=backend_proxy)
48+
mocker.patch("pyproject_api._backend.read_line", return_value=bytearray(command, "utf-8"))
49+
50+
ret = run([str(False), "a.dummy.module"])
51+
52+
# We still return 0 and write a result file. The exception should *not* bubble up
53+
assert ret == 0
54+
captured = capsys.readouterr()
55+
assert "started backend FakeBackendProxy" in captured.out
56+
assert "Backend: run command dummy_command with args {'foo': 'bar'}" in captured.out
57+
assert "Backend: Wrote response " in captured.out
58+
assert "SystemExit: 1" in captured.err
59+
60+
61+
def test_valid_request(mocker: pytest_mock.MockerFixture, capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None:
62+
"""Validate the "success" path."""
63+
result = str(tmp_path / "result")
64+
command = json.dumps({"cmd": "dummy_command", "kwargs": {"foo": "bar"}, "result": result})
65+
66+
backend_proxy = mocker.MagicMock(spec=BackendProxy)
67+
backend_proxy.return_value = "dummy-result"
68+
backend_proxy.__str__.return_value = "FakeBackendProxy"
69+
mocker.patch("pyproject_api._backend.BackendProxy", return_value=backend_proxy)
70+
mocker.patch("pyproject_api._backend.read_line", return_value=bytearray(command, "utf-8"))
71+
72+
ret = run([str(False), "a.dummy.module"])
73+
74+
assert ret == 0
75+
captured = capsys.readouterr()
76+
assert "started backend FakeBackendProxy" in captured.out
77+
assert "Backend: run command dummy_command with args {'foo': 'bar'}" in captured.out
78+
assert "Backend: Wrote response " in captured.out
79+
assert "" == captured.err
80+
81+
82+
def test_reuse_process(mocker: pytest_mock.MockerFixture, capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None:
83+
"""Validate behavior when reusing the backend proxy process.
84+
85+
There are a couple of things we'd like to check here:
86+
87+
- Ensure we can actually reuse the process.
88+
- Ensure an exception in a call to the backend does not affect subsequent calls.
89+
- Ensure we can exit safely by calling the '_exit' command.
90+
"""
91+
results = [
92+
str(tmp_path / "result_a"),
93+
str(tmp_path / "result_b"),
94+
str(tmp_path / "result_c"),
95+
str(tmp_path / "result_d"),
96+
]
97+
commands = [
98+
json.dumps({"cmd": "dummy_command_a", "kwargs": {"foo": "bar"}, "result": results[0]}),
99+
json.dumps({"cmd": "dummy_command_b", "kwargs": {"baz": "qux"}, "result": results[1]}),
100+
json.dumps({"cmd": "dummy_command_c", "kwargs": {"win": "wow"}, "result": results[2]}),
101+
json.dumps({"cmd": "_exit", "kwargs": {}, "result": results[3]}),
102+
]
103+
104+
def fake_backend(name: str, *args: Any, **kwargs: Any) -> Any: # noqa: U100
105+
if name == "dummy_command_b":
106+
raise SystemExit(2)
107+
108+
return "dummy-result"
109+
110+
backend_proxy = mocker.MagicMock(spec=BackendProxy)
111+
backend_proxy.side_effect = fake_backend
112+
backend_proxy.__str__.return_value = "FakeBackendProxy"
113+
mocker.patch("pyproject_api._backend.BackendProxy", return_value=backend_proxy)
114+
mocker.patch("pyproject_api._backend.read_line", side_effect=[bytearray(x, "utf-8") for x in commands])
115+
116+
ret = run([str(True), "a.dummy.module"])
117+
118+
# We still return 0 and write a result file. The exception should *not* bubble up and all commands should execute.
119+
# It is the responsibility of the caller to handle errors.
120+
assert ret == 0
121+
captured = capsys.readouterr()
122+
assert "started backend FakeBackendProxy" in captured.out
123+
assert "Backend: run command dummy_command_a with args {'foo': 'bar'}" in captured.out
124+
assert "Backend: run command dummy_command_b with args {'baz': 'qux'}" in captured.out
125+
assert "Backend: run command dummy_command_c with args {'win': 'wow'}" in captured.out
126+
assert "SystemExit: 2" in captured.err

whitelist.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
autoclass
22
autodoc
3+
capsys
34
cfg
45
delenv
56
exe
@@ -17,6 +18,7 @@ py311
1718
py38
1819
pygments
1920
pyproject
21+
readouterr
2022
sdist
2123
setenv
2224
tmpdir

0 commit comments

Comments
 (0)