Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to inject pygame events from pytest?

How can one inject events into a running pygame from a pytest test module?

The following is a minimal example of a pygame which draws a white rectangle when J is pressed and quits the game when Ctrl-Q is pressed.

#!/usr/bin/env python
"""minimal_pygame.py"""

import pygame


def minimal_pygame(testing: bool=False):
    pygame.init()
    game_window_sf = pygame.display.set_mode(
            size=(400, 300), 
        )
    pygame.display.flip()
    game_running = True
    while game_running:
        # Main game loop:
        # the following hook to inject events from pytest does not work:
        # if testing:
            # test_input = (yield)
            # pygame.event.post(test_input)
        for event in pygame.event.get():
            # React to closing the pygame window:
            if event.type == pygame.QUIT:
                game_running = False
                break
            # React to keypresses:
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_q:
                    # distinguish between Q and Ctrl-Q
                    mods = pygame.key.get_mods()
                    # End main loop if Ctrl-Q was pressed
                    if mods & pygame.KMOD_CTRL:
                        game_running = False
                        break
                # Draw a white square when key J is pressed:
                if event.key == pygame.K_j:
                    filled_rect = game_window_sf.fill(pygame.Color("white"), pygame.Rect(50, 50, 50, 50))
                    pygame.display.update([filled_rect])
    pygame.quit()


if __name__ == "__main__":
    minimal_pygame()

I want to write a pytest module which would automatically test it. I have read that one can inject events into running pygame. Here I read that yield from allows a bidirectional communication, so I thought I must implement some sort of a hook for pygame.events being injected from the pytest module, but it is not as simple as I thought, so I commented it out. If I uncomment the test hook under while game_running, pygame does not even wait for any input.

Here is the test module for pytest:

#!/usr/bin/env python
"""test_minimal_pygame.py"""

import pygame
import minimal_pygame


def pygame_wrapper(coro):
    yield from coro


def test_minimal_pygame():
    wrap = pygame_wrapper(minimal_pygame.minimal_pygame(testing=True))
    wrap.send(None) # prime the coroutine
    test_press_j = pygame.event.Event(pygame.KEYDOWN, {"key": pygame.K_j})
    for e in [test_press_j]:
        wrap.send(e)
like image 234
Zababa Avatar asked Aug 10 '20 13:08

Zababa


People also ask

How do you get events on pygame?

for event in pygame. event. get() handles the internal events an retrieves a list of external events (the events are removed from the internal event queue). If you press the close button of the window, than the causes the QUIT event and you'll get the event by for event in pygame.

What is pygame event return?

Pygame event loop: pygame. event. get() returns a list with all unprocessed events.

What is pygame event pump?

event. pump() to allow pygame to handle internal actions. This function is not necessary if your program is consistently processing events on the queue through the other pygame. event pygame module for interacting with events and queues functions.


1 Answers

Pygame can react to custom user events, not keypress or mouse events. Here is a working code where pytest sends a userevent to pygame, pygame reacts to it and sends a response back to pytest for evaluation:

#!/usr/bin/env python
"""minimal_pygame.py"""

import pygame


TESTEVENT = pygame.event.custom_type()


def minimal_pygame(testing: bool=False):
    pygame.init()
    game_window_sf = pygame.display.set_mode(
            size=(400, 300), 
        )
    pygame.display.flip()
    game_running = True
    while game_running:
        # Hook for testing
        if testing:
            attr_dict = (yield)
            test_event = pygame.event.Event(TESTEVENT, attr_dict)
            pygame.event.post(test_event)
        # Main game loop:
        pygame.time.wait(1000)
        for event in pygame.event.get():
            # React to closing the pygame window:
            if event.type == pygame.QUIT:
                game_running = False
                break
            # React to keypresses:
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_q:
                    # distinguish between Q and Ctrl-Q
                    mods = pygame.key.get_mods()
                    # End main loop if Ctrl-Q was pressed
                    if mods & pygame.KMOD_CTRL:
                        game_running = False
                        break
            # React to TESTEVENTS:
            if event.type == TESTEVENT:
                if event.instruction == "draw_rectangle":
                    filled_rect = game_window_sf.fill(pygame.Color("white"), pygame.Rect(50, 50, 50, 50))
                    pygame.display.update([filled_rect])
                    pygame.time.wait(1000)
                    if testing:
                        # Yield the color value of the pixel at (50, 50) back to pytest
                        yield game_window_sf.get_at((50, 50))
    pygame.quit()


if __name__ == "__main__":
    minimal_pygame()

Here's the test code:

#!/usr/bin/env python
"""test_minimal_pygame.py"""

import minimal_pygame
import pygame


def pygame_wrapper(coro):
    yield from coro


def test_minimal_pygame():
    wrap = pygame_wrapper(minimal_pygame.minimal_pygame(testing=True))
    wrap.send(None) # prime the coroutine
    # Create a dictionary of attributes for the future TESTEVENT
    attr_dict = {"instruction": "draw_rectangle"}
    response = wrap.send(attr_dict)
    assert response == pygame.Color("white")

It works, However, pytest, being a tool for stateless unit tests rather than integration tests, makes the pygame quit after it gets the first response (teardown test). It is not possible to continue and do more tests and assertions in the current pygame session. (Just try to duplicate the last two lines of the test code to resend the event, it will fail.) Pytest is not the right tool to inject a series of instructions into pygame to bring it to a precondition and then perform a series of tests.

That's at least what I heard from people on the pygame discord channel. For automated integration tests they suggest a BDD tool like Cucumber (or behave for python).

like image 149
Zababa Avatar answered Oct 10 '22 04:10

Zababa