Skip to content

Commit c83ecf2

Browse files
authored
⬆ Bump time-machine to 3.2.0 (#257)
1 parent 44ee72e commit c83ecf2

4 files changed

Lines changed: 121 additions & 130 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ dev = [
5252
"mypy==2.1.0",
5353
"ruff>=0.15.16",
5454
"respx==0.23.1",
55-
"time-machine>=2.19.0,<3.0.0",
55+
"time-machine>=2.19.0",
5656
"ty>=0.0.25",
5757
"zizmor>=1.24.1",
5858
]

tests/test_api_client.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import pytest
66
import respx
77
from httpx import Response
8-
from time_machine import TimeMachineFixture
98

109
from fastapi_cloud_cli.utils.api import (
1110
STREAM_LOGS_MAX_RETRIES,
@@ -365,13 +364,12 @@ def test_stream_build_logs_connection_closed_without_complete_failed_or_timeout(
365364
def test_stream_build_logs_retry_timeout(
366365
logs_route: respx.Route,
367366
client: APIClient,
368-
time_machine: TimeMachineFixture,
369367
deployment_id: str,
370368
) -> None:
371-
time_machine.move_to("2025-11-01 13:00:00", tick=False)
369+
clock = [0.0] # Container with a value to use as a result of time.monotonic() mock
372370

373371
def responses(request: httpx.Request, route: respx.Route) -> Response:
374-
time_machine.shift(timedelta(hours=1))
372+
clock[0] += timedelta(hours=1).total_seconds() # Simulate time passing
375373

376374
return Response(
377375
200,
@@ -382,7 +380,11 @@ def responses(request: httpx.Request, route: respx.Route) -> Response:
382380

383381
logs_route.mock(side_effect=responses)
384382

385-
with patch("time.sleep"), pytest.raises(TimeoutError, match="timed out"):
383+
with (
384+
patch("time.monotonic", side_effect=lambda: clock[0]),
385+
patch("time.sleep"),
386+
pytest.raises(TimeoutError, match="timed out"),
387+
):
386388
list(client.stream_build_logs(deployment_id))
387389

388390

tests/test_cli_deploy.py

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
1+
import itertools
12
import json
23
import random
34
import re
45
import string
5-
from datetime import timedelta
66
from pathlib import Path
77
from typing import TypedDict
8-
from unittest.mock import patch
8+
from unittest.mock import call, patch
99

1010
import httpx
1111
import pytest
1212
import respx
1313
import typer
1414
from httpx import Response
1515
from rich_toolkit.progress import Progress
16-
from time_machine import TimeMachineFixture
1716
from typer.testing import CliRunner, Result
1817

1918
from fastapi_cloud_cli.cli import app
19+
from fastapi_cloud_cli.commands.deploy import wait
2020
from fastapi_cloud_cli.config import Settings
2121
from fastapi_cloud_cli.utils.api import StreamLogError, TooManyRetriesError
2222
from tests.conftest import ConfiguredApp
@@ -1434,17 +1434,11 @@ def test_shows_error_message_on_build_log_http_error(
14341434

14351435

14361436
@pytest.mark.respx
1437-
@patch(
1438-
"fastapi_cloud_cli.commands.deploy.wait.WAITING_MESSAGES",
1439-
[("⏳", "short wait message")],
1440-
)
14411437
def test_short_wait_messages(
14421438
logged_in_cli: None,
14431439
tmp_path: Path,
14441440
respx_mock: respx.MockRouter,
1445-
time_machine: TimeMachineFixture,
14461441
) -> None:
1447-
time_machine.move_to("2025-11-01 13:00:00", tick=False)
14481442
app_data = _get_random_app()
14491443
team_data = _get_random_team()
14501444
app_id = app_data["id"]
@@ -1473,9 +1467,13 @@ def test_short_wait_messages(
14731467
)
14741468
)
14751469

1470+
# Each build-log request advances the fake monotonic clock so the elapsed
1471+
# time determines which message pool is used.
1472+
clock = [0.0]
1473+
14761474
def build_logs_handler(request: httpx.Request, route: respx.Route) -> Response:
14771475
if route.call_count <= 2:
1478-
time_machine.shift(timedelta(seconds=3))
1476+
clock[0] += 3
14791477
return Response(
14801478
200,
14811479
content=build_logs_response(
@@ -1503,26 +1501,28 @@ def build_logs_handler(request: httpx.Request, route: respx.Route) -> Response:
15031501
return_value=Response(200, json={**deployment_data, "status": "success"})
15041502
)
15051503

1506-
with changing_dir(tmp_path), patch("time.sleep"):
1504+
with (
1505+
changing_dir(tmp_path),
1506+
patch("time.sleep"),
1507+
patch("time.monotonic", side_effect=lambda: clock[0]),
1508+
patch.object(wait, "cycle", wraps=itertools.cycle) as cycle_spy,
1509+
):
15071510
result = runner.invoke(app, ["deploy"])
15081511

15091512
assert result.exit_code == 0
15101513
assert "Ready the chicken!" in result.output
15111514

1515+
# This is a short wait, so LONG_WAIT_MESSAGES should not be accessed by the
1516+
# `cycle` function.
1517+
assert call(wait.LONG_WAIT_MESSAGES) not in cycle_spy.call_args_list
1518+
15121519

15131520
@pytest.mark.respx
1514-
@patch(
1515-
"fastapi_cloud_cli.commands.deploy.wait.LONG_WAIT_MESSAGES",
1516-
[("⏳", "long wait message")],
1517-
)
15181521
def test_long_wait_messages(
15191522
logged_in_cli: None,
15201523
tmp_path: Path,
15211524
respx_mock: respx.MockRouter,
1522-
time_machine: TimeMachineFixture,
15231525
) -> None:
1524-
time_machine.move_to("2025-11-01 13:00:00", tick=False)
1525-
15261526
app_data = _get_random_app()
15271527
team_data = _get_random_team()
15281528
app_id = app_data["id"]
@@ -1551,9 +1551,13 @@ def test_long_wait_messages(
15511551
)
15521552
)
15531553

1554+
# Each build-log request advances the fake monotonic clock so the elapsed
1555+
# time determines which message pool is used.
1556+
clock = [0.0]
1557+
15541558
def build_logs_handler(request: httpx.Request, route: respx.Route) -> Response:
15551559
if route.call_count <= 2:
1556-
time_machine.shift(timedelta(seconds=35))
1560+
clock[0] += 35
15571561
return Response(
15581562
200,
15591563
content=build_logs_response(
@@ -1581,12 +1585,21 @@ def build_logs_handler(request: httpx.Request, route: respx.Route) -> Response:
15811585
return_value=Response(200, json={**deployment_data, "status": "success"})
15821586
)
15831587

1584-
with changing_dir(tmp_path), patch("time.sleep"):
1588+
with (
1589+
changing_dir(tmp_path),
1590+
patch("time.sleep"),
1591+
patch("time.monotonic", side_effect=lambda: clock[0]),
1592+
patch.object(wait, "cycle", wraps=itertools.cycle) as cycle_spy,
1593+
):
15851594
result = runner.invoke(app, ["deploy"])
15861595

15871596
assert result.exit_code == 0
15881597
assert "Ready the chicken!" in result.output
15891598

1599+
# This is a long wait, so LONG_WAIT_MESSAGES should be accessed by the `cycle`
1600+
# function.
1601+
assert call(wait.LONG_WAIT_MESSAGES) in cycle_spy.call_args_list
1602+
15901603

15911604
@pytest.mark.respx
15921605
def test_calls_upload_cancelled_when_user_interrupts(

0 commit comments

Comments
 (0)