|
| 1 | +import itertools |
1 | 2 | import json |
2 | 3 | import random |
3 | 4 | import re |
4 | 5 | import string |
5 | | -from datetime import timedelta |
6 | 6 | from pathlib import Path |
7 | 7 | from typing import TypedDict |
8 | | -from unittest.mock import patch |
| 8 | +from unittest.mock import call, patch |
9 | 9 |
|
10 | 10 | import httpx |
11 | 11 | import pytest |
12 | 12 | import respx |
13 | 13 | import typer |
14 | 14 | from httpx import Response |
15 | 15 | from rich_toolkit.progress import Progress |
16 | | -from time_machine import TimeMachineFixture |
17 | 16 | from typer.testing import CliRunner, Result |
18 | 17 |
|
19 | 18 | from fastapi_cloud_cli.cli import app |
| 19 | +from fastapi_cloud_cli.commands.deploy import wait |
20 | 20 | from fastapi_cloud_cli.config import Settings |
21 | 21 | from fastapi_cloud_cli.utils.api import StreamLogError, TooManyRetriesError |
22 | 22 | from tests.conftest import ConfiguredApp |
@@ -1434,17 +1434,11 @@ def test_shows_error_message_on_build_log_http_error( |
1434 | 1434 |
|
1435 | 1435 |
|
1436 | 1436 | @pytest.mark.respx |
1437 | | -@patch( |
1438 | | - "fastapi_cloud_cli.commands.deploy.wait.WAITING_MESSAGES", |
1439 | | - [("⏳", "short wait message")], |
1440 | | -) |
1441 | 1437 | def test_short_wait_messages( |
1442 | 1438 | logged_in_cli: None, |
1443 | 1439 | tmp_path: Path, |
1444 | 1440 | respx_mock: respx.MockRouter, |
1445 | | - time_machine: TimeMachineFixture, |
1446 | 1441 | ) -> None: |
1447 | | - time_machine.move_to("2025-11-01 13:00:00", tick=False) |
1448 | 1442 | app_data = _get_random_app() |
1449 | 1443 | team_data = _get_random_team() |
1450 | 1444 | app_id = app_data["id"] |
@@ -1473,9 +1467,13 @@ def test_short_wait_messages( |
1473 | 1467 | ) |
1474 | 1468 | ) |
1475 | 1469 |
|
| 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 | + |
1476 | 1474 | def build_logs_handler(request: httpx.Request, route: respx.Route) -> Response: |
1477 | 1475 | if route.call_count <= 2: |
1478 | | - time_machine.shift(timedelta(seconds=3)) |
| 1476 | + clock[0] += 3 |
1479 | 1477 | return Response( |
1480 | 1478 | 200, |
1481 | 1479 | content=build_logs_response( |
@@ -1503,26 +1501,28 @@ def build_logs_handler(request: httpx.Request, route: respx.Route) -> Response: |
1503 | 1501 | return_value=Response(200, json={**deployment_data, "status": "success"}) |
1504 | 1502 | ) |
1505 | 1503 |
|
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 | + ): |
1507 | 1510 | result = runner.invoke(app, ["deploy"]) |
1508 | 1511 |
|
1509 | 1512 | assert result.exit_code == 0 |
1510 | 1513 | assert "Ready the chicken!" in result.output |
1511 | 1514 |
|
| 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 | + |
1512 | 1519 |
|
1513 | 1520 | @pytest.mark.respx |
1514 | | -@patch( |
1515 | | - "fastapi_cloud_cli.commands.deploy.wait.LONG_WAIT_MESSAGES", |
1516 | | - [("⏳", "long wait message")], |
1517 | | -) |
1518 | 1521 | def test_long_wait_messages( |
1519 | 1522 | logged_in_cli: None, |
1520 | 1523 | tmp_path: Path, |
1521 | 1524 | respx_mock: respx.MockRouter, |
1522 | | - time_machine: TimeMachineFixture, |
1523 | 1525 | ) -> None: |
1524 | | - time_machine.move_to("2025-11-01 13:00:00", tick=False) |
1525 | | - |
1526 | 1526 | app_data = _get_random_app() |
1527 | 1527 | team_data = _get_random_team() |
1528 | 1528 | app_id = app_data["id"] |
@@ -1551,9 +1551,13 @@ def test_long_wait_messages( |
1551 | 1551 | ) |
1552 | 1552 | ) |
1553 | 1553 |
|
| 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 | + |
1554 | 1558 | def build_logs_handler(request: httpx.Request, route: respx.Route) -> Response: |
1555 | 1559 | if route.call_count <= 2: |
1556 | | - time_machine.shift(timedelta(seconds=35)) |
| 1560 | + clock[0] += 35 |
1557 | 1561 | return Response( |
1558 | 1562 | 200, |
1559 | 1563 | content=build_logs_response( |
@@ -1581,12 +1585,21 @@ def build_logs_handler(request: httpx.Request, route: respx.Route) -> Response: |
1581 | 1585 | return_value=Response(200, json={**deployment_data, "status": "success"}) |
1582 | 1586 | ) |
1583 | 1587 |
|
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 | + ): |
1585 | 1594 | result = runner.invoke(app, ["deploy"]) |
1586 | 1595 |
|
1587 | 1596 | assert result.exit_code == 0 |
1588 | 1597 | assert "Ready the chicken!" in result.output |
1589 | 1598 |
|
| 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 | + |
1590 | 1603 |
|
1591 | 1604 | @pytest.mark.respx |
1592 | 1605 | def test_calls_upload_cancelled_when_user_interrupts( |
|
0 commit comments