write some noodly tests (#11)

pull/12/head
Josh Karpel 3 years ago committed by GitHub
parent 09ec4ae422
commit 6bd7c5706a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -20,7 +20,7 @@ class ImageSize(NamedTuple):
height: int
@dataclass
@dataclass(frozen=True)
class Image:
img: Img
justify: JustifyMethod = "center"
@ -31,22 +31,27 @@ class Image:
def _determine_size(self, options: ConsoleOptions) -> ImageSize:
width, height = self.img.size
# multiply the max height by 2, because we're going to print 2 "pixels" per row
max_height = options.height * 2 if options.height else None
if max_height:
width, height = width * max_height / self.img.height, max_height
if width > options.max_width:
width, height = options.max_width, height * options.max_width / width
return ImageSize(floor(width), floor(height))
def __rich_console__(self, console: Console, options: ConsoleOptions) -> Iterable[Segment]:
size = self._determine_size(options)
resized = self.img.resize(
def _resize(self, size: ImageSize) -> Img:
return self.img.resize(
size=size,
resample=Img.LANCZOS,
)
def __rich_console__(self, console: Console, options: ConsoleOptions) -> Iterable[Segment]:
size = self._determine_size(options)
resized = self._resize(size)
rows = [
[
Text(

@ -29,12 +29,7 @@ from .exceptions import DuplicateInputHandler
from .modes import Mode
from .state import State
IFLAG = 0
OFLAG = 1
CFLAG = 2
LFLAG = 3
ISPEED = 4
OSPEED = 5
CC = 6
@ -112,13 +107,6 @@ SPECIAL_CHARACTERS = {
"\n": SpecialCharacters.Enter,
}
ARROWS = [
SpecialCharacters.Up,
SpecialCharacters.Down,
SpecialCharacters.Right,
SpecialCharacters.Left,
]
def get_character(stream: TextIO) -> Union[str, SpecialCharacters]:
result = stream.read(1)
@ -158,11 +146,15 @@ class InputHandlerHelpInfo:
INPUT_HANDLER_HELP: List[InputHandlerHelpInfo] = []
def handle_input(state: State, stream: TextIO) -> Optional[NoReturn]:
def handle_input(
state: State,
stream: TextIO,
handlers: InputHandlers = INPUT_HANDLERS,
) -> Optional[NoReturn]:
character = get_character(stream)
try:
handler = INPUT_HANDLERS[(character, state.mode)]
handler = handlers[(character, state.mode)]
except KeyError:
return None
@ -204,37 +196,64 @@ def input_handler(
NOT_HELP = [Mode.SLIDE, Mode.DECK]
@input_handler("h", help=f"Enter {Mode.HELP} mode.")
@input_handler(
"h",
help=f"Enter {Mode.HELP} mode.",
)
def help_mode(state: State) -> None:
state.mode = Mode.HELP
@input_handler("s", help=f"Enter {Mode.SLIDE} mode.")
@input_handler(
"s",
help=f"Enter {Mode.SLIDE} mode.",
)
def slide_mode(state: State) -> None:
state.mode = Mode.SLIDE
@input_handler("d", help=f"Enter {Mode.DECK} mode.")
@input_handler(
"d",
help=f"Enter {Mode.DECK} mode.",
)
def deck_mode(state: State) -> None:
state.mode = Mode.DECK
@input_handler(SpecialCharacters.Right, "f", modes=NOT_HELP, help="Move to the next slide.")
@input_handler(
SpecialCharacters.Right,
"f",
modes=NOT_HELP,
help="Move to the next slide.",
)
def next_slide(state: State) -> None:
state.next_slide()
@input_handler(SpecialCharacters.Left, "b", modes=NOT_HELP, help="Move to the previous slide.")
@input_handler(
SpecialCharacters.Left,
"b",
modes=NOT_HELP,
help="Move to the previous slide.",
)
def previous_slide(state: State) -> None:
state.previous_slide()
@input_handler(SpecialCharacters.Up, modes=[Mode.DECK], help="Move to the previous deck grid row.")
@input_handler(
SpecialCharacters.Up,
modes=[Mode.DECK],
help="Move to the previous deck grid row.",
)
def up_grid_row(state: State) -> None:
state.previous_slide(move=state.deck_grid_width)
@input_handler(SpecialCharacters.Down, modes=[Mode.DECK], help="Move to the next deck grid row.")
@input_handler(
SpecialCharacters.Down,
modes=[Mode.DECK],
help="Move to the next deck grid row.",
)
def down_grid_row(state: State) -> None:
state.next_slide(move=state.deck_grid_width)
@ -295,11 +314,18 @@ def reset_trigger(state: State) -> None:
state.reset_trigger()
@input_handler("p", help="Toggle profiling information.")
@input_handler(
"p",
help="Toggle profiling information.",
)
def toggle_profiling(state: State) -> None:
state.toggle_profiling()
@input_handler(SpecialCharacters.CtrlK, SpecialCharacters.CtrlC, help=f"Exit {PACKAGE_NAME}.")
@input_handler(
SpecialCharacters.CtrlK,
SpecialCharacters.CtrlC,
help=f"Exit {PACKAGE_NAME}.",
)
def exit(state: State) -> None:
raise Exit(code=0)

@ -21,6 +21,3 @@ class RPSCounter:
return 0
return num_samples / (self.render_time_history[-1] - self.render_time_history[0])
def seconds_per_render(self) -> float:
return 1 / self.renders_per_second()

@ -1,8 +1,10 @@
import subprocess
import sys
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from pytest_mock import MockFixture
from typer.testing import CliRunner
from spiel.constants import PACKAGE_NAME, __version__
@ -34,6 +36,15 @@ def test_version(runner: CliRunner) -> None:
assert __version__ in result.stdout
def test_clean_keyboard_interrupt(runner: CliRunner, mocker: MockFixture) -> None:
mock = mocker.patch("spiel.main.present_deck", MagicMock(side_effect=KeyboardInterrupt()))
result = runner.invoke(app, ["present", str(DEMO_SOURCE)])
assert mock.called
assert result.exit_code == 0
@pytest.mark.parametrize("mode", list(Mode))
@pytest.mark.parametrize("stdin", ["", "s", "d", "h", "p"])
def test_display_demo_deck(runner: CliRunner, mode: Mode, stdin: str) -> None:
@ -79,3 +90,19 @@ def test_demo_copy_to_existing_dir(runner: CliRunner, tmp_path: Path) -> None:
result = runner.invoke(app, ["demo", "copy", str(target)])
assert result.exit_code == 2
def test_demo_copy_error_during_copytree(
runner: CliRunner,
tmp_path: Path,
mocker: MockFixture,
) -> None:
mock = mocker.patch("shutil.copytree", MagicMock(side_effect=Exception("foobar")))
target = tmp_path / "new"
result = runner.invoke(app, ["demo", "copy", str(target)])
assert mock.called
assert "foobar" in result.stdout
assert result.exit_code == 1

@ -0,0 +1,41 @@
import pytest
from PIL import Image as Img
from rich.console import Console
from spiel.image import Image, ImageSize
from spiel.main import DEMO_DIR
@pytest.fixture
def image() -> Image:
return Image(Img.new(mode="RGB", size=ImageSize(100, 100)))
@pytest.mark.parametrize(
"max_width, height, size",
[
(100, None, ImageSize(100, 100)),
(100, 50, ImageSize(100, 100)),
(100, 25, ImageSize(50, 50)),
(50, 25, ImageSize(50, 50)),
(50, 50, ImageSize(50, 50)),
(50, 100, ImageSize(50, 50)),
(50, 10, ImageSize(20, 20)),
],
)
def test_determine_size(
console: Console, image: Image, max_width: int, height: int, size: ImageSize
) -> None:
options = console.options.update(max_width=max_width, height=height)
assert image._determine_size(options) == size
def test_render_image(image: Image, console: Console) -> None:
console.print(image)
def test_render_image_from_file(console: Console) -> None:
image = Image.from_file(DEMO_DIR / "img.jpg")
console.print(image)

@ -1,6 +1,8 @@
from io import StringIO
import pytest
from pytest_mock import MockFixture
from rich.console import Console
from spiel.exceptions import DuplicateInputHandler
from spiel.input import (
@ -8,8 +10,10 @@ from spiel.input import (
InputHandlers,
SpecialCharacters,
get_character,
handle_input,
input_handler,
)
from spiel.modes import Mode
from spiel.state import State
@ -37,3 +41,47 @@ def test_get_character_recognizes_special_characters(
io = StringIO(input)
assert get_character(io) == expected
def test_handle_input_calls_matching_handler_and_returns_its_return_value(
console: Console, three_slide_state: State, mocker: MockFixture
) -> None:
mock = mocker.MagicMock(return_value="foobar")
result = handle_input(
state=three_slide_state,
stream=StringIO("a"),
handlers={("a", three_slide_state.mode): mock},
)
assert mock.called
assert result == "foobar"
def test_handle_input_returns_none_for_missed_input_based_on_character(
console: Console, three_slide_state: State, mocker: MockFixture
) -> None:
mock = mocker.MagicMock(return_value="foobar")
result = handle_input(
state=three_slide_state,
stream=StringIO("a"),
handlers={("b", three_slide_state.mode): mock},
)
assert result is None
def test_handle_input_returns_none_for_missed_input_based_on_mode(
console: Console, three_slide_state: State, mocker: MockFixture
) -> None:
mock = mocker.MagicMock(return_value="foobar")
three_slide_state.mode = Mode.SLIDE
result = handle_input(
state=three_slide_state,
stream=StringIO("a"),
handlers={("a", Mode.HELP): mock},
)
assert result is None

@ -29,7 +29,11 @@ def test_can_load_deck_from_valid_file(file_with_empty_deck: Path) -> None:
assert isinstance(load_deck(file_with_empty_deck), Deck)
def test_reloader_triggers_when_file_modified(file_with_empty_deck: Path) -> None:
def test_reloader_triggers_when_file_modified(
file_with_empty_deck: Path,
console: Console,
output: StringIO,
) -> None:
state = State(console=Console(), deck=load_deck(file_with_empty_deck))
reloader = DeckReloader(state=state, deck_path=file_with_empty_deck)
@ -49,7 +53,9 @@ def test_reloader_triggers_when_file_modified(file_with_empty_deck: Path) -> Non
sleep(0.01)
for attempt in range(10):
if state.deck.name == "modified":
console.print(state.message)
result = output.getvalue()
if state.deck.name == "modified" and "Reloaded deck" in result:
return # test succeeded
sleep(0.1)
@ -59,7 +65,9 @@ def test_reloader_triggers_when_file_modified(file_with_empty_deck: Path) -> Non
def test_reloader_captures_error_in_message(
file_with_empty_deck: Path, console: Console, output: StringIO
file_with_empty_deck: Path,
console: Console,
output: StringIO,
) -> None:
state = State(console=Console(), deck=load_deck(file_with_empty_deck))
reloader = DeckReloader(state=state, deck_path=file_with_empty_deck)

@ -0,0 +1,27 @@
import pytest
from spiel.rps import RPSCounter
@pytest.fixture
def counter() -> RPSCounter:
return RPSCounter()
def test_internals(counter: RPSCounter) -> None:
# 3 renders in 4 seconds
counter.render_time_history.extend([1, 2, 5])
assert counter.renders_per_second() == 3 / 4
def test_not_enough_samples(counter: RPSCounter) -> None:
counter.render_time_history.extend([1])
# 1 sample isn't enough
assert counter.renders_per_second() == 0
def test_custom_length() -> None:
assert RPSCounter(render_history_length=5).render_time_history.maxlen == 5
Loading…
Cancel
Save