Result parsing and formatting, one stdout result line, elapsed time, handling of stale elements and of clicks blocked by the cookie banner (found in real container runs), the baseline result and the screenshot of lecture step 4. Code review RC-017. Closes #22 Closes #23 Closes #24 Closes #25 Task: MIL-005#1 Task: MIL-005#2 Task: MIL-005#3 Task: MIL-005#4
175 lines
4.7 KiB
Python
175 lines
4.7 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"], lambda _h: RecordingGame())
|
|
assert "bought Cursor" in caplog.text
|
|
|
|
caplog.clear()
|
|
game = RecordingGame(buy_succeeds=False)
|
|
main(["--duration", "0.05", "--interval", "0"], 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 == ""
|