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
217 lines
6.0 KiB
Python
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
|