fix: Flaky $NOW tests (#6990)

This commit is contained in:
Michael Genson
2026-02-02 11:24:13 -06:00
committed by GitHub
parent 6255c71609
commit fb8e318739
3 changed files with 52 additions and 100 deletions

View File

@@ -77,6 +77,7 @@ dev = [
"types-requests==2.32.4.20260107",
"types-urllib3==1.26.25.14",
"pydantic-to-typescript2==1.0.6",
"freezegun>=1.5.5",
]
[build-system]

View File

@@ -3,11 +3,13 @@ import time
from collections import defaultdict
from datetime import UTC, datetime, timedelta
from random import randint
from unittest.mock import patch
from urllib.parse import parse_qsl, urlsplit
import pytest
from dateutil.relativedelta import relativedelta
from fastapi.testclient import TestClient
from freezegun import freeze_time
from humps import camelize
from pydantic import UUID4
@@ -1589,34 +1591,28 @@ def test_parse_now_passthrough_non_placeholder():
assert result == test_string
@freeze_time("2024-01-15 12:00:00")
def test_parse_now_with_int_amount():
before = datetime.now(UTC)
result = PlaceholderKeyword._parse_now("$NOW+30d")
after = datetime.now(UTC)
assert isinstance(result, str)
dt = datetime.fromisoformat(result)
assert isinstance(dt, datetime)
# Verify offset is exactly 30 days from when the function was called
if dt.tzinfo is None:
dt = dt.replace(tzinfo=UTC)
expected_min = before + timedelta(days=30)
expected_max = after + timedelta(days=30)
assert expected_min <= dt <= expected_max
# Verify offset is exactly 30 days from the frozen time
dt = dt.replace(tzinfo=UTC)
expected = datetime(2024, 2, 14, 12, 0, 0, tzinfo=UTC)
assert dt == expected
@freeze_time("2024-01-15 12:00:00")
def test_parse_now_with_single_digit_int():
before = datetime.now(UTC)
result = PlaceholderKeyword._parse_now("$NOW+1d")
after = datetime.now(UTC)
assert isinstance(result, str)
dt = datetime.fromisoformat(result)
assert isinstance(dt, datetime)
# Verify offset is exactly 1 day from when the function was called
if dt.tzinfo is None:
dt = dt.replace(tzinfo=UTC)
expected_min = before + timedelta(days=1)
expected_max = after + timedelta(days=1)
assert expected_min <= dt <= expected_max
# Verify offset is exactly 1 day from the frozen time
dt = dt.replace(tzinfo=UTC)
expected = datetime(2024, 1, 16, 12, 0, 0, tzinfo=UTC)
assert dt == expected
@pytest.mark.parametrize("invalid_amount", ["apple", "abc", "!@#"])
@@ -1636,19 +1632,17 @@ def test_parse_now_with_invalid_amount(invalid_amount):
("S", timedelta(seconds=5)),
],
)
@freeze_time("2024-01-15 12:00:00")
def test_parse_now_with_valid_units(unit, offset_delta):
before = datetime.now(UTC)
result = PlaceholderKeyword._parse_now(f"$NOW+5{unit}")
after = datetime.now(UTC)
assert isinstance(result, str)
dt = datetime.fromisoformat(result)
assert isinstance(dt, datetime)
# Verify offset is correct from when the function was called
if dt.tzinfo is None:
dt = dt.replace(tzinfo=UTC)
expected_min = before + offset_delta
expected_max = after + offset_delta
assert expected_min <= dt <= expected_max
# Verify offset is correct from the frozen time
dt = dt.replace(tzinfo=UTC)
frozen_time = datetime(2024, 1, 15, 12, 0, 0, tzinfo=UTC)
expected = frozen_time + offset_delta
assert dt == expected
def test_parse_now_with_invalid_unit():
@@ -1662,19 +1656,17 @@ def test_parse_now_with_missing_unit():
@pytest.mark.parametrize("operation,expected_sign", [("+", 1), ("-", -1)])
@freeze_time("2024-01-15 12:00:00")
def test_parse_now_with_valid_operations(operation, expected_sign):
before = datetime.now(UTC)
result = PlaceholderKeyword._parse_now(f"$NOW{operation}5d")
after = datetime.now(UTC)
assert isinstance(result, str)
dt = datetime.fromisoformat(result)
assert isinstance(dt, datetime)
# Verify offset direction is correct from when the function was called
if dt.tzinfo is None:
dt = dt.replace(tzinfo=UTC)
expected_min = before + (timedelta(days=5) * expected_sign)
expected_max = after + (timedelta(days=5) * expected_sign)
assert expected_min <= dt <= expected_max
# Verify offset direction is correct from the frozen time
dt = dt.replace(tzinfo=UTC)
frozen_time = datetime(2024, 1, 15, 12, 0, 0, tzinfo=UTC)
expected = frozen_time + (timedelta(days=5) * expected_sign)
assert dt == expected
@pytest.mark.parametrize("invalid_operation", ["*", "/", "=", "x"])
@@ -1684,80 +1676,25 @@ def test_parse_now_with_invalid_operations(invalid_operation):
@pytest.mark.parametrize(
"placeholder,included_dates,excluded_dates",
"placeholder",
[
pytest.param(
"$NOW",
["today", "tomorrow"],
["yesterday"],
id="now_current_day",
),
pytest.param(
"$NOW+1d",
["tomorrow", "day_after_tomorrow"],
["yesterday", "today"],
id="now_plus_one_day",
),
pytest.param("$NOW", id="now_current_day"),
pytest.param("$NOW+1d", id="now_plus_one_day"),
],
)
def test_e2e_parse_now_placeholder(
api_client: TestClient,
unique_user: TestUser,
placeholder: str,
included_dates: list[str],
excluded_dates: list[str],
):
"""E2E test for parsing $NOW and $NOW+Xd placeholders in datetime filters"""
# Create recipes for testing
recipe_yesterday = unique_user.repos.recipes.create(
Recipe(user_id=unique_user.user_id, group_id=unique_user.group_id, name=random_string())
)
recipe_today = unique_user.repos.recipes.create(
Recipe(user_id=unique_user.user_id, group_id=unique_user.group_id, name=random_string())
)
recipe_tomorrow = unique_user.repos.recipes.create(
Recipe(user_id=unique_user.user_id, group_id=unique_user.group_id, name=random_string())
)
recipe_day_after = unique_user.repos.recipes.create(
Recipe(user_id=unique_user.user_id, group_id=unique_user.group_id, name=random_string())
)
with patch.object(PlaceholderKeyword, "parse_value", wraps=PlaceholderKeyword.parse_value) as mock_parse:
params = {
"page": 1,
"perPage": -1,
"queryFilter": f'id="{unique_user.user_id}" AND lastMade >= "{placeholder}"',
}
response = api_client.get(api_routes.recipes, params=params, headers=unique_user.token)
assert response.status_code == 200
date_map = {
"yesterday": (datetime.now(UTC) - timedelta(days=1)).strftime("%Y-%m-%d"),
"today": datetime.now(UTC).strftime("%Y-%m-%d"),
"tomorrow": (datetime.now(UTC) + timedelta(days=1)).strftime("%Y-%m-%d"),
"day_after_tomorrow": (datetime.now(UTC) + timedelta(days=2)).strftime("%Y-%m-%d"),
}
recipe_map = {
"yesterday": recipe_yesterday,
"today": recipe_today,
"tomorrow": recipe_tomorrow,
"day_after_tomorrow": recipe_day_after,
}
for date_name, recipe in recipe_map.items():
date_str = date_map[date_name]
datetime_str = f"{date_str}T12:00:00Z"
r = api_client.patch(
api_routes.recipes_slug_last_made(recipe.slug),
json={"timestamp": datetime_str},
headers=unique_user.token,
)
assert r.status_code == 200
# Query using placeholder
params = {"page": 1, "perPage": -1, "queryFilter": f'lastMade >= "{placeholder}"'}
response = api_client.get(api_routes.recipes, params=params, headers=unique_user.token)
assert response.status_code == 200
recipes_data = response.json()["items"]
result_ids = {recipe["id"] for recipe in recipes_data}
# Verify included and excluded recipes
for date_name in included_dates:
recipe_id = str(recipe_map[date_name].id)
assert recipe_id in result_ids, f"{date_name} should be included with {placeholder}"
for date_name in excluded_dates:
recipe_id = str(recipe_map[date_name].id)
assert recipe_id not in result_ids, f"{date_name} should be excluded with {placeholder}"
# Verify that the placeholder parsing was called
assert mock_parse.call_count

14
uv.lock generated
View File

@@ -443,6 +443,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl", hash = "sha256:3738b456192dcb3e15620f324c447721023c0293f6af9955b481e91d00179846", size = 27625, upload-time = "2024-11-07T02:00:54.523Z" },
]
[[package]]
name = "freezegun"
version = "1.5.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "python-dateutil" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/dd/23e2f4e357f8fd3bdff613c1fe4466d21bfb00a6177f238079b17f7b1c84/freezegun-1.5.5.tar.gz", hash = "sha256:ac7742a6cc6c25a2c35e9292dfd554b897b517d2dec26891a2e8debf205cb94a", size = 35914, upload-time = "2025-08-09T10:39:08.338Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/2e/b41d8a1a917d6581fc27a35d05561037b048e47df50f27f8ac9c7e27a710/freezegun-1.5.5-py3-none-any.whl", hash = "sha256:cd557f4a75cf074e84bc374249b9dd491eaeacd61376b9eb3c423282211619d2", size = 19266, upload-time = "2025-08-09T10:39:06.636Z" },
]
[[package]]
name = "ghp-import"
version = "2.1.0"
@@ -864,6 +876,7 @@ pgsql = [
dev = [
{ name = "coverage" },
{ name = "coveragepy-lcov" },
{ name = "freezegun" },
{ name = "mkdocs-material" },
{ name = "mypy" },
{ name = "pre-commit" },
@@ -933,6 +946,7 @@ provides-extras = ["pgsql"]
dev = [
{ name = "coverage", specifier = "==7.13.2" },
{ name = "coveragepy-lcov", specifier = "==0.1.2" },
{ name = "freezegun", specifier = ">=1.5.5" },
{ name = "mkdocs-material", specifier = "==9.7.1" },
{ name = "mypy", specifier = "==1.19.1" },
{ name = "pre-commit", specifier = "==4.5.1" },