diff --git a/src/countdown/__main__.py b/src/countdown/__main__.py index ccfd223..e1edbeb 100644 --- a/src/countdown/__main__.py +++ b/src/countdown/__main__.py @@ -1,6 +1,6 @@ """Command-line interface.""" -from time import sleep +from time import sleep, time import click @@ -41,7 +41,9 @@ def run_countdown(total_seconds): try: paused = False n = total_seconds - while n >= 0: + sleep_until = time() + total_seconds + pause_start = None + while n >= 0 or paused: lines = get_number_lines(n) print_full_screen(lines, paused=paused) @@ -53,6 +55,11 @@ def run_countdown(total_seconds): # Quit the timer break elif is_pause_key(key): + if paused: + sleep_until += time() - pause_start + pause_start = None + else: + pause_start = time() paused = not paused drain_keypresses() # Ignore any additional rapid keypresses lines = get_number_lines(n) @@ -60,15 +67,19 @@ def run_countdown(total_seconds): elif is_time_adjust_key(key): # Adjust the timer by +/- 30 seconds adjustment = get_time_adjustment(key) - n = max(0, n + adjustment) # Don't go below 0 + new_n = max(0, n + adjustment) # Don't go below 0 + sleep_until += new_n - n + n = new_n drain_keypresses() # Ignore any additional rapid keypresses lines = get_number_lines(n) print_full_screen(lines, paused=paused) # Only sleep and decrement if not paused if not paused: - # Sleep in small chunks to check for keypresses more frequently - for _ in range(20): # 20 x 0.05 = 1 second + # Wall-clock time at which to move from displaying n to n-1 + display_this_second_until = sleep_until - n + 1 + while time() < display_this_second_until: + # Sleep in small chunks to check for keypresses more frequently sleep(0.05) if check_for_keypress(): break # Exit sleep early if key is pressed diff --git a/tests/conftest.py b/tests/conftest.py index 8b93d40..0979c47 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,10 @@ """PyTest configuration.""" +import os + +import pytest from _pytest.assertion import truncate +from click.testing import CliRunner truncate.DEFAULT_MAX_LINES = 40 truncate.DEFAULT_MAX_CHARS = 40 * 80 @@ -27,3 +31,68 @@ def pytest_assertrepr_compare( f"Repr Comparison: {left!r} != {right!r}", ] return None + + +class FakeClock: + """Fake time.time() and time.sleep() that advance together. + + Since run_countdown uses time() for loop control and sleep() for pacing, + both must be faked in sync to avoid tests running in real time. + """ + + def __init__(self, *, raises={}, drift_per_sleep=0.0): # noqa: B006 + self.start = 1_000_000.0 + self.current = self.start + self.slept = 0 + self.raises = dict(raises) + self.drift_per_sleep = drift_per_sleep + + @property + def elapsed(self): + """Total wall clock time elapsed (including any drift).""" + return self.current - self.start + + def time(self): + return self.current + + def sleep(self, seconds): + self.current += seconds + self.drift_per_sleep + self.slept += seconds + # Check for exception with floating point tolerance + for trigger_time, exception in self.raises.items(): + if abs(self.slept - trigger_time) < 0.001: + raise exception + + +@pytest.fixture +def fake_clock(monkeypatch): + """Fixture that patches time/sleep with a FakeClock. + + Returns the clock instance. Set attributes like ``raises`` or + ``drift_per_sleep`` before invoking the CLI to customize behavior. + """ + clock = FakeClock() + monkeypatch.setattr("countdown.__main__.sleep", clock.sleep) + monkeypatch.setattr("countdown.__main__.time", clock.time) + return clock + + +@pytest.fixture +def fake_terminal_size(monkeypatch): + """Factory fixture: sets a fake terminal size for display calculations.""" + + def _set_size(columns, lines): + def get_terminal_size(fallback=(columns, lines)): + return os.terminal_size(fallback) + + monkeypatch.setattr( + "countdown.display.get_terminal_size", get_terminal_size + ) + + return _set_size + + +@pytest.fixture +def runner(): + """Fixture for invoking command-line interfaces.""" + return CliRunner() diff --git a/tests/test_drift.py b/tests/test_drift.py new file mode 100644 index 0000000..5952aab --- /dev/null +++ b/tests/test_drift.py @@ -0,0 +1,222 @@ +"""Tests for drift correction in the countdown timer.""" + +from countdown import __main__ + + +def test_countdown_displays_each_second( + runner, + fake_terminal_size, + fake_clock, + monkeypatch, +): + """Test that a 5-second countdown displays each second value.""" + fake_terminal_size(40, 20) + + displayed_times = [] + original_get_number_lines = __main__.get_number_lines + + def tracking_get_number_lines(seconds): + displayed_times.append(seconds) + return original_get_number_lines(seconds) + + monkeypatch.setattr(__main__, "get_number_lines", tracking_get_number_lines) + result = runner.invoke(__main__.main, ["5s"]) + assert result.exit_code == 0 + assert displayed_times == [5, 4, 3, 2, 1, 0] + + +def test_drift_correction_with_slow_sleeps( + runner, + fake_terminal_size, + fake_clock, + monkeypatch, +): + """With drift, the timer still counts down the right number of seconds. + + Each 0.05s sleep takes 0.06s (simulating OS scheduling overhead). + Without drift correction, this would make the countdown run 20% too long. + With drift correction, each second still advances based on wall clock time. + """ + fake_terminal_size(40, 20) + fake_clock.drift_per_sleep = 0.01 + + displayed_times = [] + original_get_number_lines = __main__.get_number_lines + + def tracking_get_number_lines(seconds): + displayed_times.append(seconds) + return original_get_number_lines(seconds) + + monkeypatch.setattr(__main__, "get_number_lines", tracking_get_number_lines) + result = runner.invoke(__main__.main, ["5s"]) + assert result.exit_code == 0 + + # Even with drift, we should still display 5 seconds counting down + assert displayed_times == [5, 4, 3, 2, 1, 0] + + +def test_drift_correction_skips_seconds_when_very_slow( + runner, + fake_terminal_size, + fake_clock, + monkeypatch, +): + """Simulate extreme drift. + + With extreme drift, individual seconds may be skipped but total time + is still bounded by wall clock time, not by sleep iteration count. + """ + fake_terminal_size(40, 20) + # Drift of 0.5s per 0.05s sleep call (each sleep takes 0.55s total) + fake_clock.drift_per_sleep = 0.5 + + displayed_times = [] + original_get_number_lines = __main__.get_number_lines + + def tracking_get_number_lines(seconds): + displayed_times.append(seconds) + return original_get_number_lines(seconds) + + monkeypatch.setattr(__main__, "get_number_lines", tracking_get_number_lines) + result = runner.invoke(__main__.main, ["60m"]) + assert result.exit_code == 0 + + # Timer should still start at 60m and count down + assert displayed_times[0] == 60 * 60 + # With extreme drift, seconds are skipped entirely, but the countdown + # still proceeds monotonically downward + for i in range(1, len(displayed_times)): + assert displayed_times[i] < displayed_times[i - 1] + assert displayed_times[-1] == 0 + + +def test_pause_preserves_remaining_time( + runner, + fake_terminal_size, + fake_clock, + monkeypatch, +): + """Pausing and resuming should not consume countdown time.""" + fake_terminal_size(40, 20) + + displayed_times = [] + original_get_number_lines = __main__.get_number_lines + + def tracking_get_number_lines(seconds): + displayed_times.append(seconds) + return original_get_number_lines(seconds) + + # Pause on first check (count=1), resume on fifth check (count=5) + keypress_count = [0] + + def fake_check_for_keypress(): + keypress_count[0] += 1 + return keypress_count[0] in [1, 5] # pause, then resume + + keys = iter([" ", " "]) # pause, resume + + def fake_read_key(): + return next(keys) + + def fake_drain(): + pass + + monkeypatch.setattr(__main__, "get_number_lines", tracking_get_number_lines) + monkeypatch.setattr(__main__, "check_for_keypress", fake_check_for_keypress) + monkeypatch.setattr(__main__, "read_key", fake_read_key) + monkeypatch.setattr(__main__, "drain_keypresses", fake_drain) + + result = runner.invoke(__main__.main, ["3s"]) + assert result.exit_code == 0 + + # Despite pausing, all countdown seconds should still be displayed + assert 3 in displayed_times + assert 2 in displayed_times + assert 1 in displayed_times + assert 0 in displayed_times + + +def test_add_time_extends_deadline( + runner, + fake_terminal_size, + fake_clock, + monkeypatch, +): + """Pressing + should extend the countdown deadline by 30 seconds.""" + fake_terminal_size(40, 20) + fake_clock.raises = {5: KeyboardInterrupt()} + + displayed_times = [] + original_get_number_lines = __main__.get_number_lines + + def tracking_get_number_lines(seconds): + displayed_times.append(seconds) + return original_get_number_lines(seconds) + + # Press + on first display + def fake_check_for_keypress(): + return len(displayed_times) == 1 + + def fake_read_key(): + return "+" + + def fake_drain(): + pass + + monkeypatch.setattr(__main__, "get_number_lines", tracking_get_number_lines) + monkeypatch.setattr(__main__, "check_for_keypress", fake_check_for_keypress) + monkeypatch.setattr(__main__, "read_key", fake_read_key) + monkeypatch.setattr(__main__, "drain_keypresses", fake_drain) + + result = runner.invoke(__main__.main, ["10s"]) + assert result.exit_code == 0 + + # After pressing + on display of 10, n jumps to 40 (10+30) + assert displayed_times[0] == 10 + assert 40 in displayed_times + # Timer should count down from 40 (not restart from 10) + idx_40 = displayed_times.index(40) + assert displayed_times[idx_40 + 1] == 39 + + +def test_subtract_time_shortens_deadline( + runner, + fake_terminal_size, + fake_clock, + monkeypatch, +): + """Pressing - should shorten the countdown deadline by 30 seconds.""" + fake_terminal_size(40, 20) + + displayed_times = [] + original_get_number_lines = __main__.get_number_lines + + def tracking_get_number_lines(seconds): + displayed_times.append(seconds) + return original_get_number_lines(seconds) + + # Press - on first display + def fake_check_for_keypress(): + return len(displayed_times) == 1 + + def fake_read_key(): + return "-" + + def fake_drain(): + pass + + monkeypatch.setattr(__main__, "get_number_lines", tracking_get_number_lines) + monkeypatch.setattr(__main__, "check_for_keypress", fake_check_for_keypress) + monkeypatch.setattr(__main__, "read_key", fake_read_key) + monkeypatch.setattr(__main__, "drain_keypresses", fake_drain) + + result = runner.invoke(__main__.main, ["1m"]) + assert result.exit_code == 0 + + # After pressing - on display of 60, n drops to 30 (60-30) + assert displayed_times[0] == 60 + assert 30 in displayed_times + # Timer should end at 0 after counting down ~30 seconds (not ~60) + assert displayed_times[-1] == 0 + # Total seconds displayed should be ~31 (30 down to 0, not 60 down to 0) + assert len([t for t in displayed_times if t <= 30]) < 35 diff --git a/tests/test_main.py b/tests/test_main.py index 9bbda2b..0aa8b67 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,36 +1,12 @@ """Integration test cases for the CLI.""" -import os import re import pytest -from click.testing import CliRunner from countdown import __main__ -class FakeSleep: - """Fake time.sleep.""" - - def __init__(self, *, raises={}): # noqa: B006 - self.slept = 0 - self.raises = dict(raises) - - def __call__(self, seconds): - self.slept += seconds - # Check for exception with floating point tolerance - for trigger_time, exception in self.raises.items(): - if abs(self.slept - trigger_time) < 0.001: - raise exception - - -def fake_size(columns, lines): - def get_terminal_size(fallback=(columns, lines)): - return os.terminal_size(fallback) - - return get_terminal_size - - def clean_main_output(output): """Remove ANSI escape codes and whitespace at ends of lines.""" output = re.sub(r"\033\[(\?\d+[hl]|[HJ])", "", output) @@ -38,12 +14,6 @@ def clean_main_output(output): return output -@pytest.fixture -def runner(): - """Fixture for invoking command-line interfaces.""" - return CliRunner() - - def test_main_with_no_arguments(runner): """It shows help when run without arguments.""" result = runner.invoke(__main__.main) @@ -61,14 +31,9 @@ def test_version_works(runner): assert result.exit_code == 0 -def test_main_3_seconds_sleeps_4_times(runner, monkeypatch): +def test_main_3_seconds(runner, fake_terminal_size, fake_clock): # Use 40x20 terminal to select size 5 digits (33w <= 40, 5h+2 <= 20) - monkeypatch.setattr( - "countdown.display.get_terminal_size", - fake_size(40, 20), - ) - fake_sleep = FakeSleep() - monkeypatch.setattr("countdown.__main__.sleep", fake_sleep) + fake_terminal_size(40, 20) result = runner.invoke(__main__.main, ["3s"]) assert result.exit_code == 0 assert clean_main_output(result.stdout) == ( @@ -97,21 +62,17 @@ def test_main_3_seconds_sleeps_4_times(runner, monkeypatch): " ██ ██ ██ ██ ██ ██ ██ ██ ██\n" " ██████ ██████ ██████ ██████ " ) - # 3 seconds countdown = 4 iterations (3,2,1,0), each sleeps 1 second = 4 seconds total - # Sleeping in chunks of 0.05, so total is ~4 seconds (floating point precision) - assert fake_sleep.slept == pytest.approx(4.0, abs=0.01) + # 3 seconds + 1 to display 00:00, each sleeping ~1 second + assert fake_clock.slept == pytest.approx(3 + 1, abs=0.01) + assert fake_clock.elapsed == pytest.approx(3 + 1, abs=0.01) -def test_main_1_minute(runner, monkeypatch): +def test_main_1_minute(runner, fake_terminal_size, fake_clock): # Use 40x10 terminal to select size 5 digits (33w <= 40, 5h+2 <= 10) - monkeypatch.setattr( - "countdown.display.get_terminal_size", - fake_size(40, 10), - ) + fake_terminal_size(40, 10) - # Raise exception after 11 sleeps - fake_sleep = FakeSleep(raises={11: SystemExit(0)}) - monkeypatch.setattr("countdown.__main__.sleep", fake_sleep) + # Raise exception after 11 seconds of fake sleep + fake_clock.raises = {11: SystemExit(0)} result = runner.invoke(__main__.main, ["1m"]) assert clean_main_output(result.stdout) == ( @@ -184,71 +145,68 @@ def test_main_1_minute(runner, monkeypatch): ) -def test_main_10_minutes_has_over_600_clear_screens(runner, monkeypatch): - monkeypatch.setattr( - "countdown.display.get_terminal_size", - fake_size(32, 10), - ) - fake_sleep = FakeSleep() - monkeypatch.setattr("countdown.__main__.sleep", fake_sleep) +def test_main_10_minutes_has_600_clear_screens( + runner, + fake_terminal_size, + fake_clock, +): + fake_terminal_size(32, 10) result = runner.invoke(__main__.main, ["10m"]) - # 10 minutes = 601 iterations, each sleeps 1 second (via 20×0.05 chunks) - # Floating point precision: 601 × 20 × 0.05 ≈ 601.0 - assert fake_sleep.slept == pytest.approx(601.0, abs=0.1) - assert result.stdout.count("\033[H\033[J") == 601 + # 10 minutes = 600 seconds + 1 to display 00:00 + assert fake_clock.slept == pytest.approx(10 * 60 + 1, abs=0.1) + assert fake_clock.elapsed == pytest.approx(10 * 60 + 1, abs=0.1) + assert result.stdout.count("\033[H\033[J") == 10 * 60 + 1 def test_main_enables_alt_buffer_and_hides_cursor_at_beginning( - runner, monkeypatch + runner, + fake_terminal_size, + fake_clock, ): - monkeypatch.setattr( - "countdown.display.get_terminal_size", - fake_size(32, 10), - ) - fake_sleep = FakeSleep() - monkeypatch.setattr("countdown.__main__.sleep", fake_sleep) + fake_terminal_size(32, 10) result = runner.invoke(__main__.main, ["5m"]) assert result.stdout.startswith("\033[?1049h\033[?25l") -def test_main_disable_alt_buffer_and_show_cursor_at_end(runner, monkeypatch): - monkeypatch.setattr( - "countdown.display.get_terminal_size", - fake_size(32, 10), - ) - fake_sleep = FakeSleep() - monkeypatch.setattr("countdown.__main__.sleep", fake_sleep) +def test_main_disable_alt_buffer_and_show_cursor_at_end( + runner, + fake_terminal_size, + fake_clock, +): + fake_terminal_size(32, 10) result = runner.invoke(__main__.main, ["5m"]) assert result.stdout.endswith("\033[?25h\033[?1049l") -def test_main_early_exit_still_shows_cursor_at_end(runner, monkeypatch): +def test_main_early_exit_still_shows_cursor_at_end( + runner, + fake_terminal_size, + fake_clock, +): # Use 40x10 terminal to select size 5 digits (33w <= 40, 5h+2 <= 10) - monkeypatch.setattr( - "countdown.display.get_terminal_size", - fake_size(40, 10), - ) + fake_terminal_size(40, 10) # Hit Ctrl+C after 4 seconds total sleep time (chunked sleep) - fake_sleep = FakeSleep(raises={4: KeyboardInterrupt()}) - monkeypatch.setattr("countdown.__main__.sleep", fake_sleep) + fake_clock.raises = {4: KeyboardInterrupt()} result = runner.invoke(__main__.main, ["15m"]) - # After 4 seconds of sleep, we've completed 4 iterations, each prints lines - assert len(result.stdout.splitlines()) == 25, "4 seconds of lines printed" + # 4 iterations x 6 newlines each (2 padding + 4 between 5 content lines) + # = 24 newlines, no trailing newline (end=""), so splitlines() gives 25 + assert len(result.stdout.splitlines()) == 25 assert result.stdout.endswith("\033[?25h\033[?1049l") -def test_pause_key_triggers_pause(runner, monkeypatch): +def test_pause_key_triggers_pause( + runner, + fake_terminal_size, + fake_clock, + monkeypatch, +): """Test that pressing a pause key triggers the pause logic.""" - monkeypatch.setattr( - "countdown.display.get_terminal_size", - fake_size(40, 20), - ) + fake_terminal_size(40, 20) # Exit after a short time - fake_sleep = FakeSleep(raises={1: KeyboardInterrupt()}) - monkeypatch.setattr("countdown.__main__.sleep", fake_sleep) + fake_clock.raises = {1: KeyboardInterrupt()} # Track whether pause key was detected pause_key_detected = [False] @@ -283,15 +241,15 @@ def fake_drain(): ) -def test_non_pause_key_ignored(runner, monkeypatch): +def test_non_pause_key_ignored( + runner, + fake_terminal_size, + fake_clock, + monkeypatch, +): """Test that non-pause keys are ignored during countdown.""" - monkeypatch.setattr( - "countdown.display.get_terminal_size", - fake_size(40, 20), - ) - - fake_sleep = FakeSleep(raises={1: KeyboardInterrupt()}) - monkeypatch.setattr("countdown.__main__.sleep", fake_sleep) + fake_terminal_size(40, 20) + fake_clock.raises = {1: KeyboardInterrupt()} # Track keypresses check_called = [False] @@ -321,29 +279,29 @@ def fake_read_key(): assert result.exit_code == 0 -def test_sleep_exits_early_on_keypress(runner, monkeypatch): +def test_sleep_exits_early_on_keypress( + runner, + fake_terminal_size, + fake_clock, + monkeypatch, +): """Test that sleep loop exits early when a key is pressed mid-sleep.""" - monkeypatch.setattr( - "countdown.display.get_terminal_size", - fake_size(40, 20), - ) + fake_terminal_size(40, 20) - # Track sleep calls + # Track sleep calls and use FakeClock for time control sleep_calls = [] + original_sleep = fake_clock.sleep - def fake_sleep(seconds): + def tracking_sleep(seconds): sleep_calls.append(seconds) - # Exit after we've done a few sleep chunks + original_sleep(seconds) if len(sleep_calls) >= 5: raise KeyboardInterrupt() - monkeypatch.setattr("countdown.__main__.sleep", fake_sleep) - - # Simulate keypress after 3rd sleep call (during chunked 1-second sleep) - check_count = [0] + monkeypatch.setattr("countdown.__main__.sleep", tracking_sleep) + # Simulate keypress after 3rd sleep call def fake_check_for_keypress(): - check_count[0] += 1 # Return True on the 3rd sleep chunk to simulate keypress mid-sleep return len(sleep_calls) == 3 @@ -360,34 +318,34 @@ def fake_drain(): result = runner.invoke(__main__.main, ["10s"]) assert result.exit_code == 0, result.output - # Should have broken out of sleep loop early (not all 20 chunks) - # We expect: 3 chunks of first iteration, then breaks, then starts paused sleep - # The key point is we don't see all 20 chunks of 0.05 before breaking + # Should have broken out of sleep loop early assert len(sleep_calls) >= 3, "Should have at least 3 sleep calls" - # If it didn't exit early, we'd see many more 0.05 sleep calls - # The presence of the break means we don't complete all 20 chunks first_iteration_sleeps = [s for s in sleep_calls[:3] if s == 0.05] assert len(first_iteration_sleeps) == 3, ( "Should have 3 chunks of 0.05s before breaking" ) -def test_resume_from_pause_exits_early(runner, monkeypatch): +def test_resume_from_pause_exits_early( + runner, + fake_terminal_size, + fake_clock, + monkeypatch, +): """Test that when paused, pressing a key to resume exits the 0.05s sleep loop.""" - monkeypatch.setattr( - "countdown.display.get_terminal_size", - fake_size(40, 20), - ) + fake_terminal_size(40, 20) sleep_calls = [] paused_state = [False] + original_sleep = fake_clock.sleep - def fake_sleep(seconds): + def tracking_sleep(seconds): sleep_calls.append((seconds, paused_state[0])) + original_sleep(seconds) if len(sleep_calls) >= 10: raise KeyboardInterrupt() - monkeypatch.setattr("countdown.__main__.sleep", fake_sleep) + monkeypatch.setattr("countdown.__main__.sleep", tracking_sleep) # Simulate: pause immediately, then resume after a few paused sleeps keypress_count = [0] @@ -432,15 +390,12 @@ def tracking_print(lines, paused=False): assert len(unpaused_sleeps) > 0, "Should have some unpaused sleep periods" -def test_add_time_with_plus_key(runner, monkeypatch): +def test_add_time_with_plus_key( + runner, fake_terminal_size, fake_clock, monkeypatch +): """Test that pressing + adds 30 seconds to the timer.""" - monkeypatch.setattr( - "countdown.display.get_terminal_size", - fake_size(40, 20), - ) - - fake_sleep = FakeSleep(raises={1: KeyboardInterrupt()}) - monkeypatch.setattr("countdown.__main__.sleep", fake_sleep) + fake_terminal_size(40, 20) + fake_clock.raises = {1: KeyboardInterrupt()} # Track the displayed times displayed_times = [] @@ -473,15 +428,15 @@ def fake_drain(): assert 90 in displayed_times, "Should display 90s after adding 30s" -def test_subtract_time_with_minus_key(runner, monkeypatch): +def test_subtract_time_with_minus_key( + runner, + fake_terminal_size, + fake_clock, + monkeypatch, +): """Test that pressing - subtracts 30 seconds from the timer.""" - monkeypatch.setattr( - "countdown.display.get_terminal_size", - fake_size(40, 20), - ) - - fake_sleep = FakeSleep(raises={1: KeyboardInterrupt()}) - monkeypatch.setattr("countdown.__main__.sleep", fake_sleep) + fake_terminal_size(40, 20) + fake_clock.raises = {1: KeyboardInterrupt()} # Track the displayed times displayed_times = [] @@ -514,15 +469,15 @@ def fake_drain(): assert 30 in displayed_times, "Should display 30s after subtracting 30s" -def test_subtract_time_cannot_go_negative(runner, monkeypatch): +def test_subtract_time_cannot_go_negative( + runner, + fake_terminal_size, + fake_clock, + monkeypatch, +): """Test that subtracting time stops at 0 (cannot go negative).""" - monkeypatch.setattr( - "countdown.display.get_terminal_size", - fake_size(40, 20), - ) - - fake_sleep = FakeSleep(raises={1: KeyboardInterrupt()}) - monkeypatch.setattr("countdown.__main__.sleep", fake_sleep) + fake_terminal_size(40, 20) + fake_clock.raises = {1: KeyboardInterrupt()} # Track the displayed times displayed_times = [] @@ -560,16 +515,9 @@ def fake_drain(): ) -def test_q_key_quits_timer(runner, monkeypatch): +def test_q_key_quits_timer(runner, fake_terminal_size, fake_clock, monkeypatch): """Test that pressing 'q' exits the timer.""" - monkeypatch.setattr( - "countdown.display.get_terminal_size", - fake_size(40, 20), - ) - - fake_sleep = FakeSleep() - monkeypatch.setattr("countdown.__main__.sleep", fake_sleep) - + fake_terminal_size(40, 20) keypress_count = [0] def fake_check_for_keypress():