lint / ruff (push) Failing after 15s
tests / desktop (push) Successful in 21s
tests / web (push) Successful in 46s
docker / image (push) Failing after 1m31s
tests / android-core (push) Successful in 1m5s
tests / backend (push) Successful in 2m33s
android / apk (push) Successful in 4m34s
136 lines
5.3 KiB
Python
136 lines
5.3 KiB
Python
"""The timer's state machine."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from app.models import PomodoroSession
|
|
from app.services import pomodoro, settings_service
|
|
|
|
|
|
def test_start_sets_duration_from_settings(session, settings):
|
|
row = pomodoro.start(session, settings, kind="work")
|
|
assert row.planned_seconds == settings.pomodoro.work_minutes * 60
|
|
assert row.status == "running"
|
|
assert pomodoro.remaining_seconds(row) <= row.planned_seconds
|
|
|
|
|
|
def test_minutes_override_wins(session, settings):
|
|
row = pomodoro.start(session, settings, kind="work", minutes=3)
|
|
assert row.planned_seconds == 180
|
|
|
|
|
|
def test_starting_again_abandons_the_previous_interval(session, settings):
|
|
first = pomodoro.start(session, settings, kind="work")
|
|
pomodoro.start(session, settings, kind="work")
|
|
session.refresh(first)
|
|
assert first.status == "abandoned"
|
|
assert len(session.query(PomodoroSession).all()) == 2
|
|
|
|
|
|
def test_pause_counts_an_interruption_and_freezes_the_clock(session, settings):
|
|
pomodoro.start(session, settings, kind="work")
|
|
row = pomodoro.pause(session, settings)
|
|
assert row.status == "paused"
|
|
assert row.interruptions == 1
|
|
frozen = pomodoro.elapsed_seconds(row)
|
|
assert pomodoro.elapsed_seconds(row) == frozen
|
|
|
|
|
|
def test_pause_can_be_switched_off(session, settings):
|
|
settings.pomodoro.allow_pause = False
|
|
settings_service.save_settings(session, settings)
|
|
pomodoro.start(session, settings, kind="work")
|
|
with pytest.raises(pomodoro.TimerError):
|
|
pomodoro.pause(session, settings)
|
|
|
|
|
|
def test_long_break_arrives_on_the_configured_cycle(session, settings):
|
|
settings.pomodoro.cycles_before_long_break = 2
|
|
settings.pomodoro.auto_start_breaks = False
|
|
settings_service.save_settings(session, settings)
|
|
|
|
pomodoro.start(session, settings, kind="work")
|
|
pomodoro.complete(session, settings, auto_next=False)
|
|
assert pomodoro.next_kind(session, settings) == "short_break"
|
|
|
|
pomodoro.start(session, settings, kind="short_break")
|
|
pomodoro.complete(session, settings, auto_next=False)
|
|
pomodoro.start(session, settings, kind="work")
|
|
pomodoro.complete(session, settings, auto_next=False)
|
|
assert pomodoro.next_kind(session, settings) == "long_break"
|
|
|
|
|
|
def test_auto_start_breaks_rolls_straight_on(session, settings):
|
|
settings.pomodoro.auto_start_breaks = True
|
|
settings_service.save_settings(session, settings)
|
|
pomodoro.start(session, settings, kind="work")
|
|
finished, follow_on = pomodoro.complete(session, settings)
|
|
assert finished.status == "completed"
|
|
assert follow_on is not None and follow_on.kind == "short_break"
|
|
|
|
|
|
def test_a_complete_naming_a_finished_interval_does_nothing(session, settings):
|
|
"""Two clients reaching zero at once must not cost you the break.
|
|
|
|
Every client ticks its own countdown and calls `complete` when it runs out.
|
|
The first call finishes the work interval and auto-starts the break; the
|
|
second — sent a moment earlier, from a client that has not polled since —
|
|
used to finish *that* break, so a break ended the instant it began.
|
|
"""
|
|
settings.pomodoro.auto_start_breaks = True
|
|
settings.pomodoro.auto_start_work = True
|
|
settings_service.save_settings(session, settings)
|
|
|
|
work = pomodoro.start(session, settings, kind="work")
|
|
finished, follow_on = pomodoro.complete(session, settings, expect_id=work.id)
|
|
assert finished is not None and follow_on is not None
|
|
assert follow_on.kind == "short_break"
|
|
|
|
# The second client's call, naming the work interval it was still showing.
|
|
stale_finished, stale_follow_on = pomodoro.complete(session, settings, expect_id=work.id)
|
|
assert stale_finished is None and stale_follow_on is None
|
|
|
|
still_running = pomodoro.current_session(session)
|
|
assert still_running is not None
|
|
assert still_running.id == follow_on.id and still_running.status == "running"
|
|
|
|
|
|
def test_complete_without_a_session_id_stays_unconditional(session, settings):
|
|
settings.pomodoro.auto_start_breaks = False
|
|
settings_service.save_settings(session, settings)
|
|
pomodoro.start(session, settings, kind="work")
|
|
finished, _ = pomodoro.complete(session, settings)
|
|
assert finished is not None and finished.status == "completed"
|
|
|
|
|
|
def test_strict_mode_demands_a_reason(session, settings):
|
|
settings.pomodoro.strict_mode = True
|
|
settings_service.save_settings(session, settings)
|
|
pomodoro.start(session, settings, kind="work")
|
|
with pytest.raises(pomodoro.TimerError):
|
|
pomodoro.abandon(session, settings)
|
|
row = pomodoro.abandon(session, settings, reason="fire alarm")
|
|
assert row.status == "abandoned" and row.abandon_reason == "fire alarm"
|
|
|
|
|
|
def test_reap_closes_intervals_left_running(session, settings):
|
|
from datetime import timedelta
|
|
|
|
from app.models import utcnow
|
|
|
|
row = pomodoro.start(session, settings, kind="work")
|
|
row.started_at = utcnow() - timedelta(hours=9)
|
|
session.commit()
|
|
assert pomodoro.reap_stale(session, settings) == 1
|
|
session.refresh(row)
|
|
assert row.status == "abandoned"
|
|
|
|
|
|
def test_timer_disabled_prevents_start(session, settings):
|
|
settings.pomodoro.timer_enabled = False
|
|
settings_service.save_settings(session, settings)
|
|
with pytest.raises(pomodoro.TimerError, match="view-only"):
|
|
pomodoro.start(session, settings, kind="work")
|
|
st = pomodoro.state(session, settings)
|
|
assert st["timer_enabled"] is False
|