The user_with_custom_thresholds fixture used UserProfile.objects.get_or_create() which returns a separate Python instance from the one cached in user._state.fields_cache['profile']. When the fixture modified and saved the get_or_create instance, the cached instance on the user object still held default values (periodic=16, full=24). compute_fasting() accessed u.profile which returned the stale cached object. Fixed by modifying through u.profile directly, which operates on the same cached instance. Also applied black formatting to fasting.py.
261 lines
10 KiB
Python
261 lines
10 KiB
Python
from datetime import timedelta
|
|
|
|
import time_machine
|
|
from django.utils import timezone
|
|
from drinks.models import Drink
|
|
from lifeevents.models import LifeEvent
|
|
from scrobbles.models import Scrobble
|
|
from trends.trends.fasting import _classify_gap, compute_fasting
|
|
|
|
|
|
def _dt(days_ago, hour=12, minute=0):
|
|
now = timezone.now()
|
|
return (now - timedelta(days=days_ago)).replace(
|
|
hour=hour, minute=minute, second=0, microsecond=0
|
|
)
|
|
|
|
|
|
def _make_food(user, food, ts):
|
|
return Scrobble.objects.create(
|
|
user=user,
|
|
food=food,
|
|
media_type=Scrobble.MediaType.FOOD,
|
|
timestamp=ts,
|
|
played_to_completion=True,
|
|
)
|
|
|
|
|
|
def _make_drink(user, obj, ts, media_type=Scrobble.MediaType.DRINK):
|
|
kwargs = {}
|
|
if media_type == Scrobble.MediaType.DRINK:
|
|
kwargs["drink"] = obj
|
|
elif media_type == Scrobble.MediaType.BEER:
|
|
kwargs["beer"] = obj
|
|
return Scrobble.objects.create(
|
|
user=user,
|
|
media_type=media_type,
|
|
timestamp=ts,
|
|
played_to_completion=True,
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
class TestClassifyGap:
|
|
def test_below_periodic_is_normal(self):
|
|
assert _classify_gap(10, 16, 24) == "normal"
|
|
|
|
def test_at_periodic_threshold(self):
|
|
assert _classify_gap(16, 16, 24) == "periodic"
|
|
|
|
def test_between_periodic_and_full(self):
|
|
assert _classify_gap(19.3, 16, 24) == "periodic"
|
|
|
|
def test_at_full_threshold(self):
|
|
assert _classify_gap(24, 16, 24) == "full"
|
|
|
|
def test_above_full_threshold(self):
|
|
assert _classify_gap(30, 16, 24) == "full"
|
|
|
|
def test_just_below_periodic(self):
|
|
assert _classify_gap(15.9, 16, 24) == "normal"
|
|
|
|
|
|
class TestFastingComputation:
|
|
def test_no_food_scrobbles(self, user, food):
|
|
result = compute_fasting(user, period="last_30")
|
|
assert result["fasting_days_count"] == 0
|
|
assert result["fasting_days"] == []
|
|
|
|
def test_single_food_scrobble(self, user, food):
|
|
_make_food(user, food, _dt(10))
|
|
result = compute_fasting(user, period="last_30")
|
|
assert result["fasting_days_count"] == 0
|
|
|
|
def test_short_gap_not_fasting(self, user, food):
|
|
_make_food(user, food, _dt(5, 8, 0))
|
|
_make_food(user, food, _dt(5, 20, 0))
|
|
result = compute_fasting(user, period="last_30")
|
|
assert result["fasting_days_count"] == 0
|
|
assert result["periodic_fasting_count"] == 0
|
|
|
|
def test_periodic_fast_consecutive_days(self, user, food):
|
|
_make_food(user, food, _dt(3, 18, 46))
|
|
_make_food(user, food, _dt(2, 14, 5))
|
|
result = compute_fasting(user, period="last_30")
|
|
assert result["periodic_fasting_count"] == 1
|
|
assert result["full_fasting_count"] == 0
|
|
assert result["fasting_days"][0]["type"] == "periodic"
|
|
|
|
def test_periodic_fast_with_gap_day_boundary(
|
|
self, user_with_custom_thresholds, food
|
|
):
|
|
u = user_with_custom_thresholds
|
|
_make_food(u, food, _dt(3, 18, 0))
|
|
_make_food(u, food, _dt(2, 8, 0))
|
|
result = compute_fasting(u, period="last_30")
|
|
fasting_dates = [d["date"] for d in result["fasting_days"]]
|
|
assert _dt(2).strftime("%Y-%m-%d") in fasting_dates
|
|
assert result["periodic_fasting_count"] >= 1
|
|
|
|
def test_full_fast_one_day_gap(self, user, food):
|
|
_make_food(user, food, _dt(5, 12, 0))
|
|
_make_food(user, food, _dt(3, 12, 0))
|
|
result = compute_fasting(user, period="last_30")
|
|
assert result["full_fasting_count"] >= 1
|
|
fasting_dates = [d["date"] for d in result["fasting_days"]]
|
|
assert _dt(4).strftime("%Y-%m-%d") in fasting_dates
|
|
|
|
def test_full_fast_multi_day_gap(self, user, food):
|
|
_make_food(user, food, _dt(7, 12, 0))
|
|
_make_food(user, food, _dt(4, 12, 0))
|
|
result = compute_fasting(user, period="last_30")
|
|
assert result["full_fasting_count"] == 2
|
|
fasting_dates = [d["date"] for d in result["fasting_days"]]
|
|
for days_ago in [6, 5]:
|
|
assert _dt(days_ago).strftime("%Y-%m-%d") in fasting_dates
|
|
|
|
def test_liquid_fast_with_drink(self, user, food, drink):
|
|
_make_food(user, food, _dt(5, 12, 0))
|
|
_make_food(user, food, _dt(3, 12, 0))
|
|
_make_drink(user, drink, _dt(4, 15, 0))
|
|
result = compute_fasting(user, period="last_30")
|
|
assert result["liquid_fasting_count"] == 1
|
|
day4 = next(
|
|
d
|
|
for d in result["fasting_days"]
|
|
if d["date"] == _dt(4).strftime("%Y-%m-%d")
|
|
)
|
|
assert day4["had_drink"] is True
|
|
|
|
def test_full_fast_without_drink(self, user, food):
|
|
_make_food(user, food, _dt(5, 12, 0))
|
|
_make_food(user, food, _dt(3, 12, 0))
|
|
result = compute_fasting(user, period="last_30")
|
|
assert result["liquid_fasting_count"] == 0
|
|
day4 = next(
|
|
d
|
|
for d in result["fasting_days"]
|
|
if d["date"] == _dt(4).strftime("%Y-%m-%d")
|
|
)
|
|
assert day4["had_drink"] is False
|
|
|
|
def test_liquid_fast_with_beer(self, user, food, beer):
|
|
_make_food(user, food, _dt(5, 12, 0))
|
|
_make_food(user, food, _dt(3, 12, 0))
|
|
_make_drink(user, beer, _dt(4, 20, 0), Scrobble.MediaType.BEER)
|
|
result = compute_fasting(user, period="last_30")
|
|
assert result["liquid_fasting_count"] == 1
|
|
|
|
def test_liquid_fast_only_in_full_fast_window(self, user, food, drink):
|
|
_make_food(user, food, _dt(5, 18, 0))
|
|
_make_food(user, food, _dt(4, 10, 0))
|
|
_make_food(user, food, _dt(3, 12, 0))
|
|
_make_drink(user, drink, _dt(4, 15, 0))
|
|
result = compute_fasting(user, period="last_30")
|
|
for d in result["fasting_days"]:
|
|
if d["type"] == "periodic":
|
|
assert d["had_drink"] is False
|
|
|
|
def test_full_fast_overrides_periodic(self, user, food):
|
|
_make_food(user, food, _dt(7, 12, 0))
|
|
_make_food(user, food, _dt(5, 12, 0))
|
|
_make_food(user, food, _dt(4, 12, 0))
|
|
result = compute_fasting(user, period="last_30")
|
|
fasting_dates = {d["date"]: d["type"] for d in result["fasting_days"]}
|
|
day6 = _dt(6).strftime("%Y-%m-%d")
|
|
assert fasting_dates.get(day6) == "full"
|
|
|
|
def test_vacation_exemption(self, user, food):
|
|
_make_food(user, food, _dt(10, 12, 0))
|
|
_make_food(user, food, _dt(8, 12, 0))
|
|
life_event = LifeEvent.objects.create(title="Summer Vacation")
|
|
Scrobble.objects.create(
|
|
user=user,
|
|
life_event=life_event,
|
|
media_type=Scrobble.MediaType.LIFE_EVENT,
|
|
timestamp=_dt(10),
|
|
stop_timestamp=_dt(8),
|
|
)
|
|
result = compute_fasting(user, period="last_30")
|
|
fasting_dates = [d["date"] for d in result["fasting_days"]]
|
|
assert _dt(9).strftime("%Y-%m-%d") not in fasting_dates
|
|
|
|
def test_vacation_exemption_disabled(self, user, food):
|
|
user.profile.fasting_vacation_exempt = False
|
|
user.profile.save()
|
|
_make_food(user, food, _dt(10, 12, 0))
|
|
_make_food(user, food, _dt(8, 12, 0))
|
|
life_event = LifeEvent.objects.create(title="Summer Vacation")
|
|
Scrobble.objects.create(
|
|
user=user,
|
|
life_event=life_event,
|
|
media_type=Scrobble.MediaType.LIFE_EVENT,
|
|
timestamp=_dt(10),
|
|
stop_timestamp=_dt(8),
|
|
)
|
|
result = compute_fasting(user, period="last_30")
|
|
fasting_dates = [d["date"] for d in result["fasting_days"]]
|
|
assert _dt(9).strftime("%Y-%m-%d") in fasting_dates
|
|
|
|
def test_custom_thresholds(self, user_with_custom_thresholds, food):
|
|
u = user_with_custom_thresholds
|
|
_make_food(u, food, _dt(3, 12, 0))
|
|
_make_food(u, food, _dt(2, 1, 0))
|
|
result = compute_fasting(u, period="last_30")
|
|
assert result["periodic_threshold_hours"] == 12
|
|
assert result["full_threshold_hours"] == 20
|
|
assert result["periodic_fasting_count"] == 1
|
|
|
|
def test_fasting_rate(self, user, food):
|
|
_make_food(user, food, _dt(10, 12, 0))
|
|
_make_food(user, food, _dt(8, 12, 0))
|
|
result = compute_fasting(user, period="last_30")
|
|
assert result["total_days"] == 30
|
|
assert result["fasting_days_count"] == 1
|
|
assert result["fasting_rate_pct"] > 0
|
|
|
|
def test_streaks(self, user, food):
|
|
_make_food(user, food, _dt(10, 12, 0))
|
|
_make_food(user, food, _dt(7, 12, 0))
|
|
_make_food(user, food, _dt(4, 12, 0))
|
|
result = compute_fasting(user, period="last_30")
|
|
fasting_dates = [d["date"] for d in result["fasting_days"]]
|
|
assert _dt(9).strftime("%Y-%m-%d") in fasting_dates
|
|
assert _dt(8).strftime("%Y-%m-%d") in fasting_dates
|
|
assert _dt(6).strftime("%Y-%m-%d") in fasting_dates
|
|
assert _dt(5).strftime("%Y-%m-%d") in fasting_dates
|
|
|
|
def test_mixed_fasting_types(self, user, food, drink):
|
|
_make_food(user, food, _dt(10, 12, 0))
|
|
_make_food(user, food, _dt(9, 6, 0))
|
|
_make_food(user, food, _dt(7, 12, 0))
|
|
_make_food(user, food, _dt(4, 12, 0))
|
|
_make_drink(user, drink, _dt(5, 15, 0))
|
|
result = compute_fasting(user, period="last_30")
|
|
types = {d["date"]: d["type"] for d in result["fasting_days"]}
|
|
assert types.get(_dt(9).strftime("%Y-%m-%d")) == "periodic"
|
|
assert types.get(_dt(8).strftime("%Y-%m-%d")) == "full"
|
|
assert types.get(_dt(6).strftime("%Y-%m-%d")) == "full"
|
|
assert types.get(_dt(5).strftime("%Y-%m-%d")) == "full"
|
|
day5 = next(
|
|
d
|
|
for d in result["fasting_days"]
|
|
if d["date"] == _dt(5).strftime("%Y-%m-%d")
|
|
)
|
|
assert day5["had_drink"] is True
|
|
|
|
@time_machine.travel(_dt(0, 12, 0))
|
|
def test_yesterday_fasting(self, user, food):
|
|
_make_food(user, food, _dt(2, 20, 0))
|
|
_make_food(user, food, _dt(0, 10, 0))
|
|
result = compute_fasting(user, period="last_30")
|
|
assert result["fasting_days_count"] >= 1
|
|
fasting_dates = [d["date"] for d in result["fasting_days"]]
|
|
assert _dt(1).strftime("%Y-%m-%d") in fasting_dates
|
|
|
|
def test_food_on_every_day_no_fasting(self, user, food):
|
|
for i in range(30):
|
|
_make_food(user, food, _dt(i, 12, 0))
|
|
result = compute_fasting(user, period="last_30")
|
|
assert result["fasting_days_count"] == 0
|