|
5 | 5 | import signal |
6 | 6 | import sys |
7 | 7 | import tempfile |
| 8 | +import threading |
8 | 9 | from code import InteractiveConsole |
| 10 | +from concurrent.futures import ThreadPoolExecutor |
9 | 11 | from typing import ( |
10 | 12 | NoReturn, |
11 | 13 | cast, |
@@ -1272,6 +1274,37 @@ def test_ctrl_d_at_prompt(say_app, monkeypatch) -> None: |
1272 | 1274 | assert out == "hello\n\n" |
1273 | 1275 |
|
1274 | 1276 |
|
| 1277 | +def _run_until_alerts_processed(app: cmd2.Cmd, pipe_input) -> None: |
| 1278 | + """Quit only once the alert worker has finished the queued batch. |
| 1279 | +
|
| 1280 | + Sending quit before starting the prompt races command input against the alert |
| 1281 | + worker. Observe the worker returning to its condition wait after draining the |
| 1282 | + queue, so even a stale prompt-only alert (which emits nothing) is accounted for. |
| 1283 | + """ |
| 1284 | + assert app._alert_queue |
| 1285 | + processed = threading.Event() |
| 1286 | + wait_for = app._alert_condition.wait_for |
| 1287 | + |
| 1288 | + def observe_wait(predicate, timeout=None): |
| 1289 | + # The worker still holds the condition here; all effects of the previous |
| 1290 | + # batch, including printing or invalidation, have completed. |
| 1291 | + if not app._alert_queue: |
| 1292 | + processed.set() |
| 1293 | + return wait_for(predicate, timeout) |
| 1294 | + |
| 1295 | + def quit_after_processing() -> None: |
| 1296 | + try: |
| 1297 | + assert processed.wait(timeout=5), "the alert worker never finished the queued batch" |
| 1298 | + finally: |
| 1299 | + # Also release the prompt on failure, so the assertion cannot hang it. |
| 1300 | + pipe_input.send_text("quit\n") |
| 1301 | + |
| 1302 | + with mock.patch.object(app._alert_condition, "wait_for", side_effect=observe_wait), ThreadPoolExecutor() as executor: |
| 1303 | + interaction = executor.submit(quit_after_processing) |
| 1304 | + app._cmdloop() |
| 1305 | + interaction.result(timeout=5) |
| 1306 | + |
| 1307 | + |
1275 | 1308 | @pytest.mark.skipif( |
1276 | 1309 | sys.platform.startswith("win"), |
1277 | 1310 | reason="Don't have a real Windows console with how we are currently running tests in GitHub Actions", |
@@ -1318,9 +1351,7 @@ def test_async_alert(base_app: cmd2.Cmd, msg: str, prompt: str, is_stale: bool) |
1318 | 1351 | history=base_app.main_session.history, |
1319 | 1352 | completer=base_app.main_session.completer, |
1320 | 1353 | ) |
1321 | | - pipe_input.send_text("quit\n") |
1322 | | - |
1323 | | - base_app._cmdloop() |
| 1354 | + _run_until_alerts_processed(base_app, pipe_input) |
1324 | 1355 |
|
1325 | 1356 | # If there was a message, patch_stdout handles the redraw (no invalidate) |
1326 | 1357 | if msg: |
@@ -1370,8 +1401,7 @@ def test_async_alert_rich(base_app: cmd2.Cmd) -> None: |
1370 | 1401 | history=base_app.main_session.history, |
1371 | 1402 | completer=base_app.main_session.completer, |
1372 | 1403 | ) |
1373 | | - pipe_input.send_text("quit\n") |
1374 | | - base_app._cmdloop() |
| 1404 | + _run_until_alerts_processed(base_app, pipe_input) |
1375 | 1405 |
|
1376 | 1406 | # Verify that print_formatted_text was called with a formatted ANSI object |
1377 | 1407 | assert mock_print.called |
|
0 commit comments