Skip to content

Commit

Permalink
test: improve world module test cov
Browse files Browse the repository at this point in the history
  • Loading branch information
u8slvn committed Aug 30, 2024
1 parent cbeb7bc commit cad9fc8
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 55 deletions.
16 changes: 2 additions & 14 deletions src/doggo/__main__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
from __future__ import annotations

import os

import pygame as pg

from doggo import ASSETS_PATH
from doggo import config
from doggo.config import COMPILED_ENV
Expand All @@ -22,26 +18,18 @@ def run() -> None:
Initialize the pygame and start the world.
"""
# Check if the app should run in fullscreen mode.
fullscreen = os.getenv("DOGGO_FULLSCREEN", "False").lower() in ("1", "true")

pg.init()

world = World(
title=config.WORLD_TITLE,
size=(config.WORLD_WIDTH, config.WORLD_HEIGHT),
icon=ASSETS_PATH.joinpath("icon.png"),
fps=config.WORLD_FPS,
fullscreen=fullscreen,
fullscreen=config.WORLD_FULLSCREEN,
)

if COMPILED_ENV and WIN:
pyi_splash.close()

try:
world.start()
except KeyboardInterrupt:
world.stop()
world.start()


if __name__ == "__main__":
Expand Down
2 changes: 2 additions & 0 deletions src/doggo/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import os
import sys

from typing import TYPE_CHECKING
Expand All @@ -26,6 +27,7 @@
WORLD_GROUND = (
WORLD_HEIGHT - WORLD_GROUND_HEIGHT
) # The ground level in the world, where the dog can walk on.
WORLD_FULLSCREEN = os.getenv("DOGGO_FULLSCREEN", "False").lower() in ("1", "true")

# --- Dog sprite sheet configuration ---

Expand Down
47 changes: 32 additions & 15 deletions src/doggo/world.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,31 @@ def __init__(
fps: int = 60,
fullscreen: bool = False,
) -> None:
pg.init()
self.window: pg.window.Window = pg.window.Window(
title=title,
size=size,
borderless=True,
always_on_top=True,
fullscreen=fullscreen,
)
self._running: bool = False
self.window_surf: pg.Surface = self.window.get_surface()
self.screen: pg.Surface = pg.Surface(size=size)
self.window.set_icon(pg.image.load(icon).convert_alpha())
self.draggable: DraggableWindow = DraggableWindow(window=self.window)
self.fullscreen: bool = fullscreen
self.fps: int = fps
self.clock: pg.time.Clock = pg.time.Clock()
self.running: bool = False
self.dt: float = 0.0
self.prev_time: float = time.time()
self.landscape = build_landscape()
self.dog: Dog = build_dog()

def is_running(self) -> bool:
"""Check if the world is running."""
return self._running

def get_screen(self) -> pg.Surface:
"""Adapt the screen to the window size if fullscreen.
Expand All @@ -70,7 +75,7 @@ def process_inputs(self) -> None:
if event.type == pg.QUIT or (
event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE
):
self.running = False
self.stop()

if not self.fullscreen:
self.draggable.process_event(event=event)
Expand Down Expand Up @@ -102,20 +107,32 @@ def start(self) -> None:
f"for {self.dog.brain.current_state.countdown}s."
)

self.running = True

while self.running:
self.get_dt()
self.process_inputs()
self.update()
self.render()
self.clock.tick(self.fps)

self.stop()
self._running = True
self.run()

def run(self) -> None:
"""World game loop."""
try:
while self.is_running():
self.get_dt()
self.process_inputs()
self.update()
self.render()
self.clock.tick(self.fps)
except KeyboardInterrupt:
logger.info("A mysterious force stopped the world.")
except Exception as error:
logger.error(f"World crashed: {error}.")
finally:
self.destroy()

def stop(self) -> None:
"""Stop the world."""
self._running = False
logger.info("World stopped. Dog is going to sleep.")

@staticmethod
def stop() -> None:
"""Stop the world."""
def destroy() -> None:
"""Destroy the world."""
pg.quit()
logger.info("World stopped. Dog is going to sleep.")
sys.exit()
186 changes: 160 additions & 26 deletions tests/test_world.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,37 @@

import time

import pygame as pg
import pytest

from doggo import ASSETS_PATH
from doggo.world import World


def test_world_initializes_correctly():
world = World(
title="Doggo Test",
size=(340, 106),
icon=ASSETS_PATH.joinpath("icon.png"),
fps=30,
fullscreen=False,
)
@pytest.fixture
def create_world():
world = None

def _(fullscreen=False):
nonlocal world
world = World(
title="Doggo Test",
size=(340, 106),
icon=ASSETS_PATH.joinpath("icon.png"),
fps=30,
fullscreen=fullscreen,
)

return world

yield _

if world is not None:
world.window.destroy()


def test_world_initializes_correctly(create_world):
world = create_world()

assert world.window.title == "Doggo Test"
assert world.window.size == (340, 106)
Expand All @@ -25,20 +42,13 @@ def test_world_initializes_correctly():
assert world.screen.get_size() == (340, 106)
assert world.fullscreen is False
assert world.fps == 30
assert world.running is False
assert world._running is False
assert world.dt == 0.0


@pytest.mark.parametrize("fullscreen", [True, False])
def test_world_screen_is_scaled_regarding_fullscreen(fullscreen):
world = World(
title="Doggo Test",
size=(340, 106),
icon=ASSETS_PATH.joinpath("icon.png"),
fps=30,
fullscreen=fullscreen,
)

def test_world_screen_is_scaled_regarding_fullscreen(create_world, fullscreen):
world = create_world(fullscreen)
screen = world.get_screen()

if fullscreen:
Expand All @@ -47,16 +57,140 @@ def test_world_screen_is_scaled_regarding_fullscreen(fullscreen):
assert screen.get_size() == (340, 106)


def test_world_get_dt():
world = World(
title="Doggo Test",
size=(340, 106),
icon=ASSETS_PATH.joinpath("icon.png"),
fps=30,
fullscreen=False,
)
def test_world_get_dt(create_world):
world = create_world()
time.sleep(0.1)

world.get_dt()

assert world.dt > 0.0


@pytest.mark.parametrize(
"event",
[
pg.event.Event(pg.QUIT),
pg.event.Event(pg.KEYDOWN, {"key": pg.K_ESCAPE}),
],
)
def test_world_stop_at_some_events(mocker, create_world, event):
mocker.patch("pygame.event.get", return_value=[event])
world = create_world()
world._running = True

world.process_inputs()

assert world._running is False


@pytest.mark.parametrize(
"fullscreen",
[True, False],
)
def test_world_draggable_window_is_processed_correctly(
mocker, create_world, fullscreen
):
mocker.patch("pygame.event.get", return_value=[pg.event.Event(pg.MOUSEBUTTONDOWN)])
world = create_world(fullscreen)
mocker.patch.object(world.draggable, "process_event", spec=True)

world.process_inputs()

if fullscreen:
world.draggable.process_event.assert_not_called()
else:
world.draggable.process_event.assert_called_once_with(event=pg.event.get()[0])


def test_world_update(mocker, create_world):
world = create_world()
world.dt = 0.1
mocker.patch.object(world.dog, "update", spec=True)

world.update()

world.dog.update.assert_called_once_with(dt=0.1)


def test_world_render(mocker, create_world):
world = create_world()
screen = mocker.patch.object(world, "screen", spec=True)
landscape = mocker.patch.object(world, "landscape", spec=True)
dog = mocker.patch.object(world, "dog", spec=True)
window_surf = mocker.patch.object(world, "window_surf", spec=True)
window = mocker.patch.object(world, "window", spec=True)

world.render()

screen.fill.assert_called_once_with((135, 206, 235))
landscape.background.draw.assert_called_once_with(screen=screen)
dog.draw.assert_called_once_with(screen=screen)
landscape.foreground.draw.assert_called_once_with(screen=screen)
window_surf.blit.assert_called_once_with(screen, (0, 0))
window.flip.assert_called_once()


def test_world_start(mocker, create_world):
world = create_world()
run = mocker.patch.object(world, "run", spec=True)

world.start()

assert world._running is True
run.assert_called_once()


def test_world_run_game_loop(mocker, create_world):
world = create_world()
mocker.patch.object(world, "is_running", side_effect=[True, False])
get_dt = mocker.patch.object(world, "get_dt", spec=True)
process_inputs = mocker.patch.object(world, "process_inputs", spec=True)
update = mocker.patch.object(world, "update", spec=True)
render = mocker.patch.object(world, "render", spec=True)
clock = mocker.patch.object(world, "clock", spec=True)
destroy = mocker.patch.object(world, "destroy", spec=True)

world.run()

get_dt.assert_called_once()
process_inputs.assert_called_once()
update.assert_called_once()
render.assert_called_once()
clock.tick.assert_called_once_with(world.fps)
destroy.assert_called_once()


@pytest.mark.parametrize("raise_exception", [KeyboardInterrupt, Exception])
def test_world_run_initializes_dedstruction_on_exception(
mocker, create_world, raise_exception
):
world = create_world()
mocker.patch.object(world, "get_dt", spec=True, side_effect=[raise_exception])
process_inputs = mocker.patch.object(world, "process_inputs", spec=True)
destroy = mocker.patch.object(world, "destroy", spec=True)

world.start() # It will call the run method.

process_inputs.assert_not_called()
destroy.assert_called_once()


def test_world_stop(mocker, create_world):
world = create_world()
mocker.patch.object(world, "run", spec=True)
world.start()

world.stop()

assert not world.is_running()


def test_world_destroy(mocker, create_world):
pg_quit = mocker.patch("pygame.quit")
sys_exit = mocker.patch("sys.exit")
world = create_world()

world.destroy()

pg_quit.assert_called_once()
sys_exit.assert_called_once()

0 comments on commit cad9fc8

Please sign in to comment.