Files
Tirsvad afcb7d35a8 Add strategy options: check interval, buy all affordable
BuyStrategy with a pure buy-all plan, validated --interval, --buy-all,
a five attempt retry for reads that go stale (found in a real run), the
comparison of two batches of 300 s container runs and the screenshot of
lecture step 5. Code review RC-018.

Closes #26
Closes #27
Closes #28

Task: MIL-006#1
Task: MIL-006#2
Task: MIL-006#3
2026-10-28 18:03:55 +08:00

217 lines
6.0 KiB
Python

"""@file test_cli.py
@brief Tests of the command line and the run sequence with a fake game.
"""
from pathlib import Path
import pytest
from cookie_clicker_bot import constants
from cookie_clicker_bot.application.ports import GamePort
from cookie_clicker_bot.cli import main, parse_args
from cookie_clicker_bot.domain.upgrades import Upgrade
class RecordingGame:
"""A game that records every call in order."""
def __init__(self, fail_on_open: bool = False, buy_succeeds: bool = True) -> None:
self._buy_succeeds = buy_succeeds
self.calls: list[str] = []
self.screenshots: list[Path] = []
self._fail_on_open = fail_on_open
def open_game(self) -> None:
self.calls.append("open_game")
if self._fail_on_open:
message = "boom"
raise RuntimeError(message)
def dismiss_language(self) -> None:
self.calls.append("dismiss_language")
def click_cookie(self) -> None:
self.calls.append("click_cookie")
def cookie_count(self) -> int:
return 1000
def cookies_per_second(self) -> float:
return 8.5
def upgrades(self) -> list[Upgrade]:
return [Upgrade("product0", "Cursor", 15)]
def buy(self, upgrade: Upgrade) -> bool:
self.calls.append("buy")
return self._buy_succeeds
def save_screenshot(self, path: Path) -> None:
self.calls.append("save_screenshot")
self.screenshots.append(path)
def close(self) -> None:
self.calls.append("close")
def test_parse_args_uses_defaults() -> None:
args = parse_args([])
assert args.duration == constants.RUN_DURATION_SECONDS == 300
assert args.interval == constants.UPGRADE_CHECK_INTERVAL_SECONDS == 5
assert args.headless is False
assert args.screenshot_start is None
assert args.screenshot is None
def test_parse_args_reads_every_option() -> None:
args = parse_args(
[
"--headless",
"--duration",
"10",
"--interval",
"2.5",
"--screenshot-start",
"a.png",
"--screenshot",
"b.png",
]
)
assert args.headless is True
assert args.duration == 10
assert args.interval == 2.5
assert args.screenshot_start == Path("a.png")
assert args.screenshot == Path("b.png")
def test_main_opens_dismisses_and_closes() -> None:
game = RecordingGame()
code = main(["--duration", "0"], lambda _headless: game)
assert code == 0
assert game.calls == ["open_game", "dismiss_language", "close"]
def test_main_passes_headless_to_the_factory() -> None:
seen: list[bool] = []
def factory(headless: bool) -> GamePort:
seen.append(headless)
return RecordingGame()
main(["--headless", "--duration", "0"], factory)
assert seen == [True]
def test_main_takes_both_screenshots_when_asked() -> None:
game = RecordingGame()
main(
["--duration", "0", "--screenshot-start", "s.png", "--screenshot", "e.png"],
lambda _headless: game,
)
assert game.screenshots == [Path("s.png"), Path("e.png")]
assert game.calls[-1] == "close"
def test_main_takes_no_screenshot_by_default() -> None:
game = RecordingGame()
main(["--duration", "0"], lambda _headless: game)
assert game.screenshots == []
def test_main_closes_the_game_when_an_error_is_raised() -> None:
game = RecordingGame(fail_on_open=True)
with pytest.raises(RuntimeError, match="boom"):
main(["--duration", "0"], lambda _headless: game)
assert game.calls == ["open_game", "close"]
def test_main_prints_exactly_one_result_line_on_stdout(
capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture
) -> None:
caplog.set_level("INFO")
main(["--duration", "0"], lambda _headless: RecordingGame())
out = capsys.readouterr().out
assert out == "cookies/second: 8.5" + chr(10)
assert "clicked the cookie" not in out
assert "clicked the cookie" in caplog.text
def test_main_logs_bought_only_for_real_purchases(
caplog: pytest.LogCaptureFixture,
) -> None:
caplog.set_level("INFO")
main(["--duration", "0.05", "--interval", "0.001"], lambda _h: RecordingGame())
assert "bought Cursor" in caplog.text
caplog.clear()
game = RecordingGame(buy_succeeds=False)
main(["--duration", "0.05", "--interval", "0.001"], lambda _h: game)
assert "buy" in game.calls
assert "bought Cursor" not in caplog.text
assert "bought 0 upgrades" in caplog.text
def test_main_prints_nothing_on_stdout_when_the_run_fails(
capsys: pytest.CaptureFixture[str],
) -> None:
with pytest.raises(RuntimeError, match="boom"):
main(["--duration", "0"], lambda _headless: RecordingGame(fail_on_open=True))
assert capsys.readouterr().out == ""
def test_parse_args_buys_one_upgrade_unless_buy_all_is_given() -> None:
assert parse_args([]).buy_all is False
assert parse_args(["--buy-all"]).buy_all is True
@pytest.mark.parametrize("value", ["0", "-1", "abc", "nan"])
def test_parse_args_rejects_an_interval_that_is_not_positive(
value: str, capsys: pytest.CaptureFixture[str]
) -> None:
with pytest.raises(SystemExit) as exit_info:
parse_args(["--interval", value])
assert exit_info.value.code == 2
assert "greater than 0" in capsys.readouterr().err
def test_main_logs_the_interval_and_the_strategy(
caplog: pytest.LogCaptureFixture,
) -> None:
caplog.set_level("INFO")
main(["--duration", "0"], lambda _h: RecordingGame())
assert "every 5 seconds, strategy buy-one" in caplog.text
caplog.clear()
main(
["--duration", "0", "--interval", "2", "--buy-all"], lambda _h: RecordingGame()
)
assert "every 2.0 seconds, strategy buy-all" in caplog.text
def test_main_buy_all_buys_every_affordable_upgrade_per_check(
caplog: pytest.LogCaptureFixture,
) -> None:
caplog.set_level("INFO")
game = RecordingGame()
main(["--duration", "0.05", "--interval", "0.001", "--buy-all"], lambda _h: game)
assert "bought Cursor" in caplog.text