Skip to content

Commit

Permalink
Merge #315 #328
Browse files Browse the repository at this point in the history
315: Add AssetLoaded event r=pathunstrom a=astronouth7303

Fire an event when an asset finishes loading and becomes available.

Depends on #306, #316 

328: Manual Testing r=pathunstrom a=astronouth7303

Initial version of manual/visual tests.

Co-authored-by: Jamie Bliss <jamie@ivyleav.es>
  • Loading branch information
bors[bot] and AstraLuma committed Jul 23, 2019
3 parents 53e99bf + 7411806 + ede2b0d commit 0ac8d2d
Show file tree
Hide file tree
Showing 13 changed files with 159 additions and 17 deletions.
24 changes: 22 additions & 2 deletions ppb/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import threading

import ppb.vfs as vfs
import ppb.events as events
from ppb.systemslib import System

__all__ = 'Asset', 'AssetLoadingSystem',
Expand Down Expand Up @@ -58,10 +59,14 @@ def _finished_background(self, fut):
if hasattr(self, 'file_missing'):
logger.warning("File not found: %r", self.name)
self._data = self.file_missing()
if _finished is not None:
_finished(self)
else:
raise
else:
self._data = self.background_parse(raw)
if _finished is not None:
_finished(self)
except Exception as exc:
# Save unhandled exceptions to be raised in the main thread
self._raise_error = exc
Expand Down Expand Up @@ -101,15 +106,18 @@ def load(self, timeout: float = None):


class AssetLoadingSystem(System):
def __init__(self, **_):
def __init__(self, *, engine, **_):
super().__init__(**_)
self.engine = engine
self._executor = concurrent.futures.ThreadPoolExecutor()
self._queue = {} # maps names to futures

def __enter__(self):
# 1. Register ourselves as the hint provider
global _hint, _backlog
global _hint, _finished, _backlog
assert _hint is _default_hint
_hint = self._hint
_finished = self._finished

# 2. Grab-n-clear the backlog (atomically?)
queue, _backlog = _backlog, []
Expand All @@ -134,6 +142,17 @@ def _load(filename):
with vfs.open(filename) as file:
return file.read()

def _finished(self, asset):
statuses = [
fut.running()
for fut in self._queue.values()
]
self.engine.signal(events.AssetLoaded(
asset=asset,
total_loaded=sum(not s for s in statuses),
total_queued=sum(s for s in statuses),
))


_backlog = []

Expand All @@ -143,3 +162,4 @@ def _default_hint(filename, callback=None):


_hint = _default_hint
_finished = None
7 changes: 6 additions & 1 deletion ppb/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from typing import Type
from typing import Union

import ppb.events as events
from ppb import events
from ppb.eventlib import EventMixin
from ppb.systems import EventPoller
from ppb.systems import Renderer
Expand Down Expand Up @@ -114,6 +114,11 @@ def activate(self, next_scene: dict):
self.scenes.append(scene(*args, **kwargs))

def signal(self, event):
"""
Add an event to the event queue.
Thread-safe.
"""
self.events.append(event)

def publish(self):
Expand Down
14 changes: 14 additions & 0 deletions ppb/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
'SceneStopped',
'StopScene',
'Update',
'AssetLoaded',
)

# Remember to define scene at the end so the pargs version of __init__() still works
Expand Down Expand Up @@ -232,3 +233,16 @@ class Update:
"""
time_delta: float
scene: BaseScene = None


import ppb


@dataclass
class AssetLoaded:
"""
Fired whenever an asset finished loading.
"""
asset: 'ppb.assets.Asset'
total_loaded: int
total_queued: int
6 changes: 3 additions & 3 deletions ppb/sprites.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
from pathlib import Path
from typing import Union

import ppb_vector
from ppb_vector import Vector

import ppb
from ppb import Vector
from ppb.eventlib import EventMixin
from ppb.utils import FauxFloat

import ppb_vector


TOP = "top"
BOTTOM = "bottom"
Expand Down
1 change: 1 addition & 0 deletions ppb/systems/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pygame

from ppb import buttons
import ppb.buttons as buttons
from ppb_vector import Vector
import ppb.events as events
Expand Down
2 changes: 1 addition & 1 deletion ppb/systemslib.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import ppb.eventlib as eventlib
from ppb import eventlib


class System(eventlib.EventMixin):
Expand Down
60 changes: 50 additions & 10 deletions tests/test_assets.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,44 @@
import pytest

from ppb import GameEngine, BaseScene
import ppb.events
import ppb.assets
from ppb.assets import Asset, AssetLoadingSystem
from ppb.testutils import Failer


def test_loading():
@pytest.fixture
def clean_assets():
"""
Cleans out the global state of the asset system, so that we start fresh every
test.
"""
ppb.assets._backlog = []


class AssetTestScene(BaseScene):
def on_asset_loaded(self, event, signal):
self.ale = event
signal(ppb.events.Quit())


def test_loading(clean_assets):
a = Asset('ppb/engine.py')
engine = GameEngine(BaseScene, basic_systems=[AssetLoadingSystem])
engine = GameEngine(
AssetTestScene, basic_systems=[AssetLoadingSystem, Failer],
fail=lambda e: False, message=None, run_time=1,
)
with engine:
engine.start()
ats = engine.current_scene

engine.main_loop()

assert a.load()
print(vars(ats))
assert ats.ale.asset is a
assert ats.ale.total_loaded == 1
assert ats.ale.total_queued == 0


# def test_loading_root():
Expand All @@ -22,46 +50,58 @@ def test_loading():
# assert a.load()


def test_missing_package():
def test_missing_package(clean_assets):
a = Asset('does/not/exist')
engine = GameEngine(BaseScene, basic_systems=[AssetLoadingSystem])
engine = GameEngine(
AssetTestScene, basic_systems=[AssetLoadingSystem, Failer],
fail=lambda e: False, message=None, run_time=1,
)
with engine:
engine.start()

with pytest.raises(FileNotFoundError):
assert a.load()


def test_missing_resource():
def test_missing_resource(clean_assets):
a = Asset('ppb/dont.touch.this')
engine = GameEngine(BaseScene, basic_systems=[AssetLoadingSystem])
engine = GameEngine(
AssetTestScene, basic_systems=[AssetLoadingSystem, Failer],
fail=lambda e: False, message=None, run_time=1,
)
with engine:
engine.start()

with pytest.raises(FileNotFoundError):
assert a.load()


def test_parsing():
def test_parsing(clean_assets):
class Const(Asset):
def background_parse(self, data):
return "nah"

a = Const('ppb/flags.py')
engine = GameEngine(BaseScene, basic_systems=[AssetLoadingSystem])
engine = GameEngine(
AssetTestScene, basic_systems=[AssetLoadingSystem, Failer],
fail=lambda e: False, message=None, run_time=1,
)
with engine:
engine.start()

assert a.load() == "nah"


def test_missing_parse():
def test_missing_parse(clean_assets):
class Const(Asset):
def file_missing(self):
return "igotu"

a = Const('spam/eggs')
engine = GameEngine(BaseScene, basic_systems=[AssetLoadingSystem])
engine = GameEngine(
AssetTestScene, basic_systems=[AssetLoadingSystem, Failer],
fail=lambda e: False, message=None, run_time=1,
)
with engine:
engine.start()

Expand Down
7 changes: 7 additions & 0 deletions viztests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Visual Tests
============

This is a test suite of manual tests, meant to help with the difficulty of
thorough automated testing of graphical software.

Run with `python -m viztests`
24 changes: 24 additions & 0 deletions viztests/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import ast
from pathlib import Path
import subprocess
import sys


def get_docstring(path):
tree = ast.parse(path.read_text(), path.name, mode='exec')

return ast.get_docstring(tree)


for script in Path(__file__).resolve().parent.glob('*.py'):
if script.name.startswith('_'):
continue
ds = get_docstring(script)
print("=" * len(script.name))
print(script.name)
print("=" * len(script.name))
print("")
if ds is not None:
print(ds)
print("")
subprocess.run([sys.executable, str(script)], check=True)
31 changes: 31 additions & 0 deletions viztests/angles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
Tests rotation vs Vector angles
The center sprite should always face the orbiting sprite
"""
import ppb

ROTATION_RATE = 90


class CenterSprite(ppb.BaseSprite):
image = ppb.Image('player.png')

def on_update(self, event, signal):
self.rotation += ROTATION_RATE * event.time_delta


class OrbitSprite(ppb.BaseSprite):
position = ppb.Vector(0, -2)
image = ppb.Image('target.png')

def on_update(self, event, signal):
self.position = self.position.rotate(ROTATION_RATE * event.time_delta)


def setup(scene):
scene.add(CenterSprite())
scene.add(OrbitSprite())


ppb.run(setup)
Binary file added viztests/bullet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added viztests/player.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added viztests/target.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 0ac8d2d

Please sign in to comment.