|
44 | 44 | ) |
45 | 45 | _EXISTING_LOGS_BEFORE_REDIRECT_ATTACH = 3 |
46 | 46 |
|
| 47 | +# Large enough that a real sleep is clearly detectable against `_FAST_EXIT_THRESHOLD_S`. |
| 48 | +_PATCHED_FINAL_SLEEP_S = 5 |
| 49 | +_FAST_EXIT_THRESHOLD_S = 1.0 |
| 50 | + |
47 | 51 | _EXPECTED_MESSAGES_AND_LEVELS = ( |
48 | 52 | ('2025-05-13T07:24:12.588Z ACTOR: Pulling Docker image of build.', logging.INFO), |
49 | 53 | ('2025-05-13T07:24:12.686Z ACTOR: Creating Docker container.', logging.INFO), |
@@ -652,3 +656,64 @@ async def test_status_message_watcher_async_restart_after_normal_completion(http |
652 | 656 | assert task2 is not task # New task created |
653 | 657 | await task2 # Let it complete (will hit terminal status again) |
654 | 658 | assert task2.done() |
| 659 | + |
| 660 | + |
| 661 | +@pytest.mark.usefixtures('mock_api') |
| 662 | +def test_sync_watcher_manual_stop_skips_final_sleep( |
| 663 | + httpserver: HTTPServer, |
| 664 | + monkeypatch: pytest.MonkeyPatch, |
| 665 | +) -> None: |
| 666 | + """Manual `stop()` on the sync watcher must not pay the final sleep — only `__exit__` should.""" |
| 667 | + monkeypatch.setattr(StatusMessageWatcherBase, '_final_sleep_time_s', _PATCHED_FINAL_SLEEP_S) |
| 668 | + |
| 669 | + api_url = httpserver.url_for('/').removesuffix('/') |
| 670 | + run_client = ApifyClient(token='mocked_token', api_url=api_url).run(run_id=_MOCKED_RUN_ID) |
| 671 | + watcher = run_client.get_status_message_watcher(check_period=timedelta(seconds=0)) |
| 672 | + |
| 673 | + watcher.start() |
| 674 | + start = time.monotonic() |
| 675 | + watcher.stop() |
| 676 | + elapsed = time.monotonic() - start |
| 677 | + |
| 678 | + assert elapsed < _FAST_EXIT_THRESHOLD_S, f'stop() should not sleep, took {elapsed:.2f}s' |
| 679 | + |
| 680 | + |
| 681 | +@pytest.mark.usefixtures('mock_api') |
| 682 | +def test_sync_watcher_exit_skips_final_sleep_on_exception( |
| 683 | + httpserver: HTTPServer, |
| 684 | + monkeypatch: pytest.MonkeyPatch, |
| 685 | +) -> None: |
| 686 | + """Exceptional `with`-exit must not pay the final sleep so exceptions propagate immediately.""" |
| 687 | + monkeypatch.setattr(StatusMessageWatcherBase, '_final_sleep_time_s', _PATCHED_FINAL_SLEEP_S) |
| 688 | + |
| 689 | + api_url = httpserver.url_for('/').removesuffix('/') |
| 690 | + run_client = ApifyClient(token='mocked_token', api_url=api_url).run(run_id=_MOCKED_RUN_ID) |
| 691 | + watcher = run_client.get_status_message_watcher(check_period=timedelta(seconds=0)) |
| 692 | + |
| 693 | + start = time.monotonic() |
| 694 | + with pytest.raises(RuntimeError, match='boom'), watcher: |
| 695 | + raise RuntimeError('boom') |
| 696 | + elapsed = time.monotonic() - start |
| 697 | + |
| 698 | + assert elapsed < _FAST_EXIT_THRESHOLD_S, f'__exit__ should skip final sleep on exception, took {elapsed:.2f}s' |
| 699 | + |
| 700 | + |
| 701 | +@pytest.mark.usefixtures('mock_api') |
| 702 | +async def test_async_watcher_aexit_skips_final_sleep_on_exception( |
| 703 | + httpserver: HTTPServer, |
| 704 | + monkeypatch: pytest.MonkeyPatch, |
| 705 | +) -> None: |
| 706 | + """Exceptional `async with`-exit must not pay the final sleep so exceptions propagate immediately.""" |
| 707 | + monkeypatch.setattr(StatusMessageWatcherBase, '_final_sleep_time_s', _PATCHED_FINAL_SLEEP_S) |
| 708 | + |
| 709 | + api_url = httpserver.url_for('/').removesuffix('/') |
| 710 | + run_client = ApifyClientAsync(token='mocked_token', api_url=api_url).run(run_id=_MOCKED_RUN_ID) |
| 711 | + watcher = await run_client.get_status_message_watcher(check_period=timedelta(seconds=0)) |
| 712 | + |
| 713 | + start = time.monotonic() |
| 714 | + with pytest.raises(RuntimeError, match='boom'): |
| 715 | + async with watcher: |
| 716 | + raise RuntimeError('boom') |
| 717 | + elapsed = time.monotonic() - start |
| 718 | + |
| 719 | + assert elapsed < _FAST_EXIT_THRESHOLD_S, f'__aexit__ should skip final sleep on exception, took {elapsed:.2f}s' |
0 commit comments