Skip to content

Commit 8db7636

Browse files
epicgdogevanugarte
andauthored
Text Parsing Subprocess Command (#125)
* retrieve id from print_job * from loggin.warning to logging.info * retrieve id from print_job * from loggin.warning to logging.info * retrieve id from print_job * added args to popen * more fixes * added unit tests for text-parsing * added changes for server.py * added unit tests for returncode 1 and 0 * added better logging * fixed test typos * tests are fixed * lint * rebase * return early in if * better exception, comment for fun --------- Co-authored-by: evan <evanuxd@gmail.com>
1 parent a8c4955 commit 8db7636

2 files changed

Lines changed: 194 additions & 19 deletions

File tree

printer/server.py

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
allow_headers=["*"],
2727
)
2828
logging.basicConfig(
29-
format="%(asctime)s.%(msecs)03dZ %(processName)s %(threadName)s %(levelname)s:%(name)s:%(message)s",
29+
# in mondo we trust
30+
format="%(asctime)s.%(msecs)03dZ %(levelname)s:%(name)s:%(message)s",
3031
datefmt="%Y-%m-%dT%H:%M:%S",
3132
level=logging.INFO,
3233
)
@@ -60,9 +61,9 @@ def get_args() -> argparse.Namespace:
6061
"--dont-delete-pdfs",
6162
action="store_true",
6263
default=False,
63-
help="specify if server should delete pdfs after printing"
64+
help="specify if server should delete pdfs after printing",
6465
)
65-
66+
6667
return parser.parse_args()
6768

6869

@@ -105,27 +106,62 @@ def send_file_to_printer(
105106
command = f"lp -n {num_copies} {maybe_page_range} -o sides={sides} -o media=na_letter_8.5x11in -d {PRINTER_NAME} {file_path}"
106107
metrics_handler.print_jobs_recieved.inc()
107108
if args.development:
108-
logging.warning(f"server is in development mode, command would've been `{command}`")
109-
else:
110-
print_job = subprocess.Popen(
111-
command,
112-
shell=True,
109+
logging.warning(
110+
f"server is in development mode, command would've been `{command}`"
111+
)
112+
return None
113+
114+
print_job = subprocess.Popen(
115+
command,
116+
shell=True,
117+
stdout=subprocess.PIPE,
118+
stderr=subprocess.PIPE,
119+
text=True,
120+
)
121+
print_job.wait()
122+
123+
if print_job.returncode != 0:
124+
logging.error(
125+
f"command returned code {print_job.returncode} stderr: {print_job.stderr.read()} stdout: {print_job.stdout.read()}"
126+
)
127+
return None
128+
try:
129+
print_id = print_job.stdout.read().strip().split(" ")[3]
130+
logging.info(f"extracted print id is {print_id}")
131+
return print_id
132+
except Exception:
133+
logging.exception(
134+
f"failed to extract print id from stdout: {print_job.stdout.read()}"
113135
)
114-
print_job.wait()
136+
# need to find a better value to return when the command exited
137+
# with code 0 but the output could not be parsed for a job id.
138+
return ''
139+
140+
141+
def maybe_delete_pdf(file_path):
142+
if args.dont_delete_pdfs:
143+
logging.info(
144+
f"--dont-delete-pdfs is set, skipping deletion of file {file_path}"
145+
)
146+
return
147+
pathlib.Path(file_path).unlink()
115148

116149

117150
@app.get("/healthcheck/printer")
118151
def api():
119152
metrics_handler.last_health_check_request.set(int(time.time()))
120153
return "printer is up!"
121154

155+
122156
@app.get("/metrics", response_class=PlainTextResponse)
123157
def metrics():
124158
return prometheus_client.generate_latest()
125159

126160

127161
@app.post("/print")
128-
async def read_item(file: UploadFile = File(...), copies: str = Form(...), sides: str = Form(...)):
162+
async def read_item(
163+
file: UploadFile = File(...), copies: str = Form(...), sides: str = Form(...)
164+
):
129165
"""
130166
incoming request to print looks like
131167
{
@@ -140,16 +176,17 @@ async def read_item(file: UploadFile = File(...), copies: str = Form(...), sides
140176
file_path = str(base / file_id)
141177
with open(file_path, "wb") as f:
142178
f.write(await file.read())
143-
send_file_to_printer(
179+
print_id = send_file_to_printer(
144180
str(file_path),
145181
copies,
146182
sides=sides,
147183
)
148-
if args.dont_delete_pdfs:
149-
logging.info(f'--dont-delete-pdfs is set, skipping deletion of file {file_path}')
150-
return "worked!"
151-
pathlib.Path(file_path).unlink()
152-
return "worked!"
184+
185+
maybe_delete_pdf(file_path)
186+
187+
if not args.development and print_id is None:
188+
raise Exception("unable to extract print id from print request")
189+
return {"print_id": print_id}
153190
except Exception:
154191
logging.exception("printing failed!")
155192
return HTTPException(

printer/test_server.py

Lines changed: 141 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import importlib
22
import io
33
import os
4+
import subprocess
45
import unittest
56
from unittest import mock
67

8+
79
from fastapi.testclient import TestClient
810

911
import server
@@ -34,14 +36,29 @@ def test_health_check(self):
3436
def test_print_endpoint(self, mock_pathlib_unlink, mock_open_func, mock_popen, _):
3537
client = self.load_server_with_args()
3638
test_file = io.BytesIO(b"dummy file content")
39+
40+
mock_popen_result = mock.MagicMock()
41+
mock_popen_result.returncode = 0
42+
mock_popen_result.stdout.read.return_value = (
43+
"request id is HP_LaserJet_p2015dn_Right-53 (1 file(s))"
44+
)
45+
46+
mock_popen.return_value = mock_popen_result
47+
3748
response = client.post(
3849
"/print",
3950
files={"file": ("test.txt", test_file, "text/plain")},
4051
data={"copies": "1", "sides": "one-sided"},
4152
)
4253

4354
self.assertEqual(response.status_code, 200)
44-
self.assertEqual(response.text, '"worked!"')
55+
json_response = response.json()
56+
self.assertEqual(
57+
json_response,
58+
{
59+
"print_id": "HP_LaserJet_p2015dn_Right-53",
60+
},
61+
)
4562

4663
mock_open_func.assert_called_once_with("/tmp/test-id", "wb")
4764

@@ -57,6 +74,9 @@ def test_print_endpoint(self, mock_pathlib_unlink, mock_open_func, mock_popen, _
5774
mock.call(
5875
"lp -n 1 -o sides=one-sided -o media=na_letter_8.5x11in -d HP_P2015_DN /tmp/test-id",
5976
shell=True,
77+
stdout=subprocess.PIPE,
78+
stderr=subprocess.PIPE,
79+
text=True,
6080
),
6181
)
6282

@@ -69,6 +89,14 @@ def test_print_endpoint(self, mock_pathlib_unlink, mock_open_func, mock_popen, _
6989
def test_print_endpoint_dont_delete_pdf(
7090
self, mock_pathlib_unlink, mock_open_func, mock_popen, _
7191
):
92+
93+
mock_popen_result = mock.MagicMock()
94+
mock_popen_result.returncode = 0
95+
mock_popen_result.stdout.read.return_value = (
96+
"request id is HP_LaserJet_p2015dn_Right-53 (1 file(s))"
97+
)
98+
99+
mock_popen.return_value = mock_popen_result
72100
client = self.load_server_with_args(["--dont-delete-pdfs"])
73101
test_file = io.BytesIO(b"dummy file content")
74102
response = client.post(
@@ -78,7 +106,13 @@ def test_print_endpoint_dont_delete_pdf(
78106
)
79107

80108
self.assertEqual(response.status_code, 200)
81-
self.assertEqual(response.text, '"worked!"')
109+
json_response = response.json()
110+
self.assertEqual(
111+
json_response,
112+
{
113+
"print_id": "HP_LaserJet_p2015dn_Right-53",
114+
},
115+
)
82116

83117
mock_open_func.assert_called_once_with("/tmp/test-id", "wb")
84118

@@ -94,6 +128,9 @@ def test_print_endpoint_dont_delete_pdf(
94128
mock.call(
95129
"lp -n 1 -o sides=one-sided -o media=na_letter_8.5x11in -d HP_P2015_DN /tmp/test-id",
96130
shell=True,
131+
stdout=subprocess.PIPE,
132+
stderr=subprocess.PIPE,
133+
text=True,
97134
),
98135
)
99136

@@ -102,7 +139,7 @@ def test_print_endpoint_dont_delete_pdf(
102139
@mock.patch("server.subprocess.Popen")
103140
@mock.patch("builtins.open", side_effect=FileNotFoundError("sorry!"))
104141
@mock.patch("pathlib.Path.unlink")
105-
def test_print_endpoint_error(self, mock_pathlib_unlink, _, mock_popen):
142+
def test_print_endpoint_file_not_found(self, mock_pathlib_unlink, _, mock_popen):
106143
client = self.load_server_with_args()
107144
test_file = io.BytesIO(b"dummy file content")
108145
response = client.post(
@@ -124,6 +161,107 @@ def test_print_endpoint_error(self, mock_pathlib_unlink, _, mock_popen):
124161
mock_popen.assert_not_called()
125162
mock_pathlib_unlink.assert_not_called()
126163

164+
@mock.patch("server.uuid.uuid4", return_value="test-id")
165+
@mock.patch("server.subprocess.Popen")
166+
@mock.patch("builtins.open", new_callable=mock.mock_open)
167+
@mock.patch("pathlib.Path.unlink")
168+
def test_print_endpoint_nonzero_returncode(
169+
self, mock_pathlib_unlink, mock_open_func, mock_popen, _
170+
):
171+
client = self.load_server_with_args()
172+
test_file = io.BytesIO(b"dummy file content")
173+
174+
mock_popen_result = mock.MagicMock()
175+
mock_popen_result.returncode = 1
176+
mock_popen.return_value = mock_popen_result
177+
178+
response = client.post(
179+
"/print",
180+
files={"file": ("test.txt", test_file, "text/plain")},
181+
data={"copies": "1", "sides": "dark-side"},
182+
)
183+
184+
self.assertEqual(response.status_code, 200)
185+
self.assertEqual(
186+
response.json(),
187+
{
188+
"status_code": 500,
189+
"detail": "printing failed, check logs",
190+
"headers": None,
191+
},
192+
)
193+
194+
mock_open_func.assert_called_once_with("/tmp/test-id", "wb")
195+
196+
mock_open_func().write.assert_called_once()
197+
self.assertEqual(
198+
mock_open_func().write.call_args_list[0], mock.call(b"dummy file content")
199+
)
200+
201+
mock_popen.assert_called_once()
202+
203+
self.assertEqual(
204+
mock_popen.call_args_list[0],
205+
mock.call(
206+
"lp -n 1 -o sides=dark-side -o media=na_letter_8.5x11in -d HP_P2015_DN /tmp/test-id",
207+
shell=True,
208+
stdout=subprocess.PIPE,
209+
stderr=subprocess.PIPE,
210+
text=True,
211+
),
212+
)
213+
214+
mock_pathlib_unlink.assert_called_once()
215+
216+
@mock.patch("server.uuid.uuid4", return_value="test-id")
217+
@mock.patch("server.subprocess.Popen")
218+
@mock.patch("builtins.open", new_callable=mock.mock_open)
219+
@mock.patch("pathlib.Path.unlink")
220+
def test_junk_print_id(self, mock_pathlib_unlink, mock_open_func, mock_popen, _):
221+
client = self.load_server_with_args()
222+
test_file = io.BytesIO(b"dummy file content")
223+
224+
mock_popen_result = mock.MagicMock()
225+
mock_popen_result.returncode = 0
226+
mock_popen_result.stdout.read.return_value = "junk output"
227+
mock_popen.return_value = mock_popen_result
228+
229+
response = client.post(
230+
"/print",
231+
files={"file": ("test.txt", test_file, "text/plain")},
232+
data={"copies": "1", "sides": "one-sided"},
233+
)
234+
235+
self.assertEqual(response.status_code, 200)
236+
self.assertEqual(
237+
response.json(),
238+
{
239+
"print_id": "",
240+
},
241+
)
242+
243+
mock_open_func.assert_called_once_with("/tmp/test-id", "wb")
244+
245+
mock_open_func().write.assert_called_once()
246+
self.assertEqual(
247+
mock_open_func().write.call_args_list[0], mock.call(b"dummy file content")
248+
)
249+
250+
mock_popen.assert_called_once()
251+
252+
self.assertEqual(
253+
mock_popen.call_args_list[0],
254+
mock.call(
255+
"lp -n 1 -o sides=one-sided -o media=na_letter_8.5x11in -d HP_P2015_DN /tmp/test-id",
256+
shell=True,
257+
stdout=subprocess.PIPE,
258+
stderr=subprocess.PIPE,
259+
text=True,
260+
),
261+
)
262+
263+
mock_pathlib_unlink.assert_called_once()
264+
127265

128266
if __name__ == "__main__":
129267
unittest.main()

0 commit comments

Comments
 (0)