MIL-003 Higher or Lower game: logic, routes, server.py, tests #25
No files matched your search
@@ -15,7 +15,7 @@ document of a type. `Primary File` may contain a glob (e.g.
|
|||||||
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
|
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
|
||||||
| PP | Project Plan | docs/project-plan.md | 002 |
|
| PP | Project Plan | docs/project-plan.md | 002 |
|
||||||
| MIL | Milestone / Gateway | docs/milestones/*.md | 005 |
|
| MIL | Milestone / Gateway | docs/milestones/*.md | 005 |
|
||||||
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 010 |
|
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 011 |
|
||||||
|
|
||||||
## Languages
|
## Languages
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# RC-010 Code review of MIL-003
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-010 |
|
||||||
|
| CrossReference | [MIL-003], [QC-PY-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-11-06 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [cc95105] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [MIL-003]
|
||||||
|
- Checklist used: [QC-PY-001]
|
||||||
|
- Scope: Code of MIL-003: domain, application, infrastructure and presentation/web packages, `server.py`, and the unit and integration tests; written by the python-developer agent
|
||||||
|
- Language and domain: n/a (technical type)
|
||||||
|
- Language reviewer: none
|
||||||
|
- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing (`snake_case`, `PascalCase`, `UPPER_SNAKE`) | Pass | Modules and functions use snake_case, classes PascalCase, constants UPPER_SNAKE |
|
||||||
|
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose (`compare_guess`, `GuessResult`, `HOME_HEADING`); no unexplained abbreviations |
|
||||||
|
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff check` and `ruff format --check` report no finding; no suppressions |
|
||||||
|
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every signature is annotated; `mypy --strict` reports no issue |
|
||||||
|
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | No bare `except`; no exception is swallowed |
|
||||||
|
| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable default arguments; no builtin is shadowed |
|
||||||
|
| 7 | Files, locks and connections are managed with context managers | N-A | No file, lock or connection is opened |
|
||||||
|
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Public modules, classes and functions have Doxygen-style docstrings |
|
||||||
|
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | No `print`; no secret in the code; `.env` is neither read nor imported |
|
||||||
|
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | N-A | No DCD exists (plain technical tasks) |
|
||||||
|
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | pytest tests are named for behaviour, run in any order and use no network |
|
||||||
|
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `mypy --strict` passes; no `Any` |
|
||||||
|
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `pyproject.toml` declares Flask (runtime) and pytest, ruff, mypy (extras); none unused |
|
||||||
|
| 14 | Each piece of knowledge (a business rule, constant, format, validation or query) is defined in one place; code is merged only where it expresses the same knowledge, not where it merely looks alike | Pass | The comparison rule is only in `domain/rules.py`; headings, colours and GIFs only in `constants.py` |
|
||||||
|
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | domain imports only the standard library; application imports domain and constants; infrastructure imports only the standard library; presentation imports application, infrastructure and Flask; no cycle |
|
||||||
|
| 16 | SOLID holds: a class or module has one reason to change; new behaviour is added by extension, not by editing a type test in several places; subclasses honour the contract of their base; protocols are narrow; high-level code depends on protocols or abstract types and receives its implementations | Pass | Each module has one reason to change; behaviour is added by extension |
|
||||||
|
| 17 | Source is laid out in the layers and folders of the clean architecture folder structure (`domain`, `application`, `infrastructure`, `presentation` packages under `src/`, and `unit`, `integration` and `e2e` under `tests/`), in `snake_case`, and imports between the layers point inward | Pass | Domain rule, application use case, infrastructure random source and presentation web app sit in their layer packages; tests in `tests/unit` and `tests/integration` |
|
||||||
|
| 18 | File and folder names are English, contain no space and no character outside letters, digits, `-`, `_` and `.`, and follow the casing of the language for files | Pass | File and folder names are English with letters, digits and `_` only |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — All criteria pass or are not applicable; no blocking finding.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| None | S01 | 2026-11-06 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[MIL-003]: ../../docs/milestones/mil-003-higher-lower-game.md
|
||||||
|
[QC-PY-001]: ../../framework/qc/code/qc-programming-python.md
|
||||||
|
[cc95105]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/055-higher-lower/commit/cc95105cf4fb9915e4c392fa58eec5d9a1f7c454
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
"""Entry point of the Higher or Lower URLs project.
|
||||||
|
|
||||||
|
@brief Composition root: builds the web application and runs it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from higher_lower.constants import HOST, PORT
|
||||||
|
from higher_lower.presentation.web.app import create_app
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host=HOST, port=PORT)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""Use case of the Higher or Lower game.
|
||||||
|
|
||||||
|
@brief Holds the secret number and judges guesses against it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from higher_lower.constants import MAX_NUMBER, MIN_NUMBER
|
||||||
|
from higher_lower.domain.rules import GuessResult, compare_guess
|
||||||
|
|
||||||
|
|
||||||
|
class GuessGame:
|
||||||
|
"""@brief One game with a fixed secret number."""
|
||||||
|
|
||||||
|
def __init__(self, secret_number: int) -> None:
|
||||||
|
"""Start a game.
|
||||||
|
|
||||||
|
@param secret_number The number the player has to find.
|
||||||
|
"""
|
||||||
|
self._secret_number = secret_number
|
||||||
|
|
||||||
|
def check(self, guess: int) -> GuessResult:
|
||||||
|
"""Judge a guess.
|
||||||
|
|
||||||
|
@param guess The number the player guessed.
|
||||||
|
@return Whether the guess is low, high or correct.
|
||||||
|
"""
|
||||||
|
return compare_guess(guess, self._secret_number)
|
||||||
|
|
||||||
|
def is_in_range(self, guess: int) -> bool:
|
||||||
|
"""Tell whether a guess is inside the allowed range.
|
||||||
|
|
||||||
|
@param guess The number the player guessed.
|
||||||
|
@return True when the guess is between the minimum and maximum number.
|
||||||
|
"""
|
||||||
|
return MIN_NUMBER <= guess <= MAX_NUMBER
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""Rules of the Higher or Lower game.
|
||||||
|
|
||||||
|
@brief Pure comparison of a guess with the secret number.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class GuessResult(Enum):
|
||||||
|
"""@brief Outcome of comparing a guess with the secret number."""
|
||||||
|
|
||||||
|
LOW = "low"
|
||||||
|
HIGH = "high"
|
||||||
|
CORRECT = "correct"
|
||||||
|
|
||||||
|
|
||||||
|
def compare_guess(guess: int, secret: int) -> GuessResult:
|
||||||
|
"""Compare a guess with the secret number.
|
||||||
|
|
||||||
|
@param guess The number the player guessed.
|
||||||
|
@param secret The number to find.
|
||||||
|
@return LOW if the guess is below the secret, HIGH if above, else CORRECT.
|
||||||
|
"""
|
||||||
|
if guess < secret:
|
||||||
|
return GuessResult.LOW
|
||||||
|
if guess > secret:
|
||||||
|
return GuessResult.HIGH
|
||||||
|
return GuessResult.CORRECT
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"""Source of the secret number.
|
||||||
|
|
||||||
|
@brief The only place in the project that uses randomness.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
def random_secret_number(minimum: int, maximum: int) -> int:
|
||||||
|
"""Draw a secret number.
|
||||||
|
|
||||||
|
@param minimum Lowest possible number.
|
||||||
|
@param maximum Highest possible number.
|
||||||
|
@return A random integer from minimum to maximum, both included.
|
||||||
|
"""
|
||||||
|
return random.randint(minimum, maximum)
|
||||||
Whitespace-only changes.
@@ -0,0 +1,68 @@
|
|||||||
|
"""Flask application of the Higher or Lower game.
|
||||||
|
|
||||||
|
@brief Routes that map a URL path to a guess and show the matching page.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Blueprint, Flask, current_app
|
||||||
|
|
||||||
|
from higher_lower.application.guess_game import GuessGame
|
||||||
|
from higher_lower.constants import MAX_NUMBER, MIN_NUMBER
|
||||||
|
from higher_lower.infrastructure.secret_number import random_secret_number
|
||||||
|
from higher_lower.presentation.web.page import (
|
||||||
|
HOME_PAGE,
|
||||||
|
OUT_OF_RANGE_PAGE,
|
||||||
|
page_for_result,
|
||||||
|
)
|
||||||
|
from higher_lower.presentation.web.view import render_page
|
||||||
|
|
||||||
|
## Key under which the game is stored in the Flask extensions registry.
|
||||||
|
_GAME_KEY = "guess_game"
|
||||||
|
|
||||||
|
## Blueprint that holds the views of the game.
|
||||||
|
_blueprint = Blueprint("higher_lower", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _current_game() -> GuessGame:
|
||||||
|
"""Find the game stored in the running application.
|
||||||
|
|
||||||
|
@return The game created by create_app.
|
||||||
|
"""
|
||||||
|
game: GuessGame = current_app.extensions[_GAME_KEY]
|
||||||
|
return game
|
||||||
|
|
||||||
|
|
||||||
|
@_blueprint.route("/")
|
||||||
|
def home() -> str:
|
||||||
|
"""Show the home page.
|
||||||
|
|
||||||
|
@return The home page HTML.
|
||||||
|
"""
|
||||||
|
return render_page(HOME_PAGE)
|
||||||
|
|
||||||
|
|
||||||
|
@_blueprint.route("/<int:guess>")
|
||||||
|
def guess_number(guess: int) -> str:
|
||||||
|
"""Judge the guess in the URL path.
|
||||||
|
|
||||||
|
@param guess The number the player guessed.
|
||||||
|
@return The page for a low, high, correct or out of range guess.
|
||||||
|
"""
|
||||||
|
game = _current_game()
|
||||||
|
if not game.is_in_range(guess):
|
||||||
|
return render_page(OUT_OF_RANGE_PAGE)
|
||||||
|
return render_page(page_for_result(game.check(guess)))
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(secret_number: int | None = None) -> Flask:
|
||||||
|
"""Build the Flask application.
|
||||||
|
|
||||||
|
@param secret_number The number to find; None draws a random one that
|
||||||
|
stays the same for the lifetime of the application.
|
||||||
|
@return The configured Flask application.
|
||||||
|
"""
|
||||||
|
if secret_number is None:
|
||||||
|
secret_number = random_secret_number(MIN_NUMBER, MAX_NUMBER)
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.extensions[_GAME_KEY] = GuessGame(secret_number)
|
||||||
|
app.register_blueprint(_blueprint)
|
||||||
|
return app
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"""Page view model of the web presentation.
|
||||||
|
|
||||||
|
@brief Plain data for one page, built from a guess result.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from higher_lower.constants import (
|
||||||
|
CORRECT_COLOUR,
|
||||||
|
CORRECT_GIF,
|
||||||
|
CORRECT_HEADING,
|
||||||
|
HIGH_COLOUR,
|
||||||
|
HIGH_GIF,
|
||||||
|
HIGH_HEADING,
|
||||||
|
HOME_COLOUR,
|
||||||
|
HOME_GIF,
|
||||||
|
HOME_HEADING,
|
||||||
|
LOW_COLOUR,
|
||||||
|
LOW_GIF,
|
||||||
|
LOW_HEADING,
|
||||||
|
OUT_OF_RANGE_COLOUR,
|
||||||
|
OUT_OF_RANGE_HEADING,
|
||||||
|
)
|
||||||
|
from higher_lower.domain.rules import GuessResult
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class PageViewModel:
|
||||||
|
"""@brief What a page shows: a coloured heading and a picture."""
|
||||||
|
|
||||||
|
heading: str
|
||||||
|
colour: str
|
||||||
|
gif: str
|
||||||
|
|
||||||
|
|
||||||
|
HOME_PAGE = PageViewModel(HOME_HEADING, HOME_COLOUR, HOME_GIF)
|
||||||
|
OUT_OF_RANGE_PAGE = PageViewModel(OUT_OF_RANGE_HEADING, OUT_OF_RANGE_COLOUR, HOME_GIF)
|
||||||
|
|
||||||
|
_PAGES_BY_RESULT: dict[GuessResult, PageViewModel] = {
|
||||||
|
GuessResult.LOW: PageViewModel(LOW_HEADING, LOW_COLOUR, LOW_GIF),
|
||||||
|
GuessResult.HIGH: PageViewModel(HIGH_HEADING, HIGH_COLOUR, HIGH_GIF),
|
||||||
|
GuessResult.CORRECT: PageViewModel(CORRECT_HEADING, CORRECT_COLOUR, CORRECT_GIF),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def page_for_result(result: GuessResult) -> PageViewModel:
|
||||||
|
"""Build the page that tells the player how the guess went.
|
||||||
|
|
||||||
|
@param result The outcome of the guess.
|
||||||
|
@return The page view model for that outcome.
|
||||||
|
"""
|
||||||
|
return _PAGES_BY_RESULT[result]
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
"""HTML view of the web presentation.
|
||||||
|
|
||||||
|
@brief Renders a page view model as HTML.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from html import escape
|
||||||
|
|
||||||
|
from higher_lower.presentation.web.page import PageViewModel
|
||||||
|
|
||||||
|
|
||||||
|
def render_page(page: PageViewModel) -> str:
|
||||||
|
"""Render a page as HTML, escaping every value.
|
||||||
|
|
||||||
|
@param page The page to show.
|
||||||
|
@return A heading in the page colour followed by the picture.
|
||||||
|
"""
|
||||||
|
colour = escape(page.colour)
|
||||||
|
heading = escape(page.heading)
|
||||||
|
gif = escape(page.gif)
|
||||||
|
return f'<h1 style="color: {colour}">{heading}</h1><img src="{gif}">'
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
"""Tests of the web application through the Flask test client."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from flask.testing import FlaskClient
|
||||||
|
|
||||||
|
from higher_lower import constants
|
||||||
|
from higher_lower.presentation.web.app import create_app
|
||||||
|
|
||||||
|
SECRET = 5
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client() -> FlaskClient:
|
||||||
|
return create_app(SECRET).test_client()
|
||||||
|
|
||||||
|
|
||||||
|
def _body(client: FlaskClient, path: str) -> str:
|
||||||
|
return client.get(path).get_data(as_text=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _expected(heading: str, colour: str, gif: str) -> str:
|
||||||
|
return f'<h1 style="color: {colour}">{heading}</h1><img src="{gif}">'
|
||||||
|
|
||||||
|
|
||||||
|
def test_home_page_shows_home_heading_colour_and_gif(client: FlaskClient) -> None:
|
||||||
|
expected = _expected(
|
||||||
|
constants.HOME_HEADING, constants.HOME_COLOUR, constants.HOME_GIF
|
||||||
|
)
|
||||||
|
assert _body(client, "/") == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_guess_below_secret_shows_too_low_page(client: FlaskClient) -> None:
|
||||||
|
expected = _expected(constants.LOW_HEADING, constants.LOW_COLOUR, constants.LOW_GIF)
|
||||||
|
assert _body(client, "/3") == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_guess_above_secret_shows_too_high_page(client: FlaskClient) -> None:
|
||||||
|
expected = _expected(
|
||||||
|
constants.HIGH_HEADING, constants.HIGH_COLOUR, constants.HIGH_GIF
|
||||||
|
)
|
||||||
|
assert _body(client, "/7") == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_guess_equal_to_secret_shows_correct_page(client: FlaskClient) -> None:
|
||||||
|
expected = _expected(
|
||||||
|
constants.CORRECT_HEADING, constants.CORRECT_COLOUR, constants.CORRECT_GIF
|
||||||
|
)
|
||||||
|
assert _body(client, "/5") == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_pages_use_distinct_colours() -> None:
|
||||||
|
colours = {
|
||||||
|
constants.HOME_COLOUR,
|
||||||
|
constants.LOW_COLOUR,
|
||||||
|
constants.HIGH_COLOUR,
|
||||||
|
constants.CORRECT_COLOUR,
|
||||||
|
constants.OUT_OF_RANGE_COLOUR,
|
||||||
|
}
|
||||||
|
assert len(colours) == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_guess_above_range_shows_out_of_range_page_with_home_gif(
|
||||||
|
client: FlaskClient,
|
||||||
|
) -> None:
|
||||||
|
response = client.get("/10")
|
||||||
|
expected = _expected(
|
||||||
|
constants.OUT_OF_RANGE_HEADING,
|
||||||
|
constants.OUT_OF_RANGE_COLOUR,
|
||||||
|
constants.HOME_GIF,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.get_data(as_text=True) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("path", ["/-1", "/abc"])
|
||||||
|
def test_negative_or_text_path_is_not_found(client: FlaskClient, path: str) -> None:
|
||||||
|
assert client.get(path).status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_without_secret_keeps_one_random_secret() -> None:
|
||||||
|
client = create_app().test_client()
|
||||||
|
numbers = range(constants.MIN_NUMBER, constants.MAX_NUMBER + 1)
|
||||||
|
first = [_body(client, f"/{n}") for n in numbers]
|
||||||
|
second = [_body(client, f"/{n}") for n in numbers]
|
||||||
|
correct = _expected(
|
||||||
|
constants.CORRECT_HEADING, constants.CORRECT_COLOUR, constants.CORRECT_GIF
|
||||||
|
)
|
||||||
|
assert first == second
|
||||||
|
assert first.count(correct) == 1
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""Tests of the guess game use case."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from higher_lower.application.guess_game import GuessGame
|
||||||
|
from higher_lower.constants import MAX_NUMBER, MIN_NUMBER
|
||||||
|
from higher_lower.domain.rules import GuessResult
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_reports_low_high_and_correct() -> None:
|
||||||
|
game = GuessGame(5)
|
||||||
|
assert game.check(2) is GuessResult.LOW
|
||||||
|
assert game.check(8) is GuessResult.HIGH
|
||||||
|
assert game.check(5) is GuessResult.CORRECT
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("guess", [MIN_NUMBER, MAX_NUMBER])
|
||||||
|
def test_is_in_range_accepts_limits(guess: int) -> None:
|
||||||
|
assert GuessGame(5).is_in_range(guess)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("guess", [MIN_NUMBER - 1, MAX_NUMBER + 1])
|
||||||
|
def test_is_in_range_rejects_values_just_outside(guess: int) -> None:
|
||||||
|
assert not GuessGame(5).is_in_range(guess)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
"""Tests of the game rules."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from higher_lower.domain.rules import GuessResult, compare_guess
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("guess", "expected"),
|
||||||
|
[(3, GuessResult.LOW), (7, GuessResult.HIGH), (5, GuessResult.CORRECT)],
|
||||||
|
)
|
||||||
|
def test_compare_guess_classifies_guess(guess: int, expected: GuessResult) -> None:
|
||||||
|
assert compare_guess(guess, 5) is expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_compare_guess_is_correct_at_lower_boundary() -> None:
|
||||||
|
assert compare_guess(0, 0) is GuessResult.CORRECT
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
"""Tests of the secret number source."""
|
||||||
|
|
||||||
|
from higher_lower.infrastructure.secret_number import random_secret_number
|
||||||
|
|
||||||
|
DRAWS = 500
|
||||||
|
|
||||||
|
|
||||||
|
def test_random_secret_number_stays_in_range() -> None:
|
||||||
|
draws = [random_secret_number(0, 9) for _ in range(DRAWS)]
|
||||||
|
assert all(0 <= value <= 9 for value in draws)
|
||||||
|
|
||||||
|
|
||||||
|
def test_random_secret_number_returns_the_only_possible_value() -> None:
|
||||||
|
assert random_secret_number(4, 4) == 4
|
||||||
Reference in new issue
Block a user