/** * @file controller.c * @brief Unit tests for control maps and SDL input dispatch. * * SDL events are synthesized directly rather than pumped through the event * queue, so none of this needs a physical keyboard or gamepad. The device * enumeration helpers are exercised against the dummy drivers, where the * expected outcome is "no devices, no crash". */ #include #include #include #include // For the akgl_actor_cmhf_* handlers these tests bind. akgl/controller.h pulls // actor.h in for itself now; tests/headers.c is what keeps it doing so. #include #include #include #include #include #include #include "testutil.h" /* * akgl/controller.h declares these as akgl_controller_handle_*, but src has * always defined them as gamepad_handle_*. Declared here under the names that * actually link, so the tests can reach them without renaming shipped symbols. */ akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event); akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event); akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event); akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event); /** @brief Keyboard id the tests bind their control maps to. */ #define TEST_KBID 11 /** @brief Gamepad id the tests bind their control maps to. */ #define TEST_JSID 22 /** @brief Stand-in for the application state pointer SDL hands to callbacks. */ static int appstate_placeholder = 0; /** @brief The actor every control map in this file targets. */ static akgl_Actor *player = NULL; /** @brief Base character supplying the player's acceleration constants. */ static akgl_Character *playerchar = NULL; /** @brief Clear every control map so each test starts from an empty binding set. */ static void reset_control_maps(void) { int i = 0; for ( i = 0; i < AKGL_MAX_CONTROL_MAPS; i++ ) { memset(&akgl_controlmaps[i], 0x00, sizeof(akgl_ControlMap)); } } /** @brief Register the "player" actor the gamepad handlers look up by name. */ static akerr_ErrorContext *make_player(void) { PREPARE_ERROR(e); ATTEMPT { CATCH(e, akgl_registry_init_actor()); CATCH(e, akgl_registry_init_character()); CATCH(e, akgl_heap_init()); CATCH(e, akgl_heap_next_character(&playerchar)); CATCH(e, akgl_character_initialize(playerchar, "playerchar")); playerchar->ax = 6.0f; playerchar->ay = 8.0f; CATCH(e, akgl_heap_next_actor(&player)); CATCH(e, akgl_actor_initialize(player, "player")); player->basechar = playerchar; } CLEANUP { } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); } /** @brief Build a synthetic keyboard event. */ static void make_key_event(SDL_Event *event, uint32_t type, SDL_KeyboardID which, SDL_Keycode key) { memset(event, 0x00, sizeof(SDL_Event)); event->type = type; event->key.which = which; event->key.key = key; } /** @brief Build a synthetic keyboard event carrying modifier state. */ static void make_key_event_mod(SDL_Event *event, uint32_t type, SDL_KeyboardID which, SDL_Keycode key, SDL_Keymod mod) { make_key_event(event, type, which, key); event->key.mod = mod; } /** * @brief Build the text input event SDL sends after a key press composes. * * @p text is not copied by SDL or by the library at this point, so callers pass * a string literal. */ static void make_text_event(SDL_Event *event, const char *text) { memset(event, 0x00, sizeof(SDL_Event)); event->type = SDL_EVENT_TEXT_INPUT; event->text.text = text; } /** @brief Build a synthetic gamepad button event. */ static void make_button_event(SDL_Event *event, uint32_t type, SDL_JoystickID which, uint8_t button) { memset(event, 0x00, sizeof(SDL_Event)); event->type = type; event->gbutton.which = which; event->gbutton.button = button; } akerr_ErrorContext *test_controller_pushmap(void) { PREPARE_ERROR(e); akgl_Control control; int i = 0; ATTEMPT { reset_control_maps(); memset(&control, 0x00, sizeof(akgl_Control)); control.key = SDLK_SPACE; control.event_on = SDL_EVENT_KEY_DOWN; control.event_off = SDL_EVENT_KEY_UP; control.handler_on = &akgl_actor_cmhf_left_on; control.handler_off = &akgl_actor_cmhf_left_off; TEST_EXPECT_OK(e, akgl_controller_pushmap(0, &control), "pushing one control"); TEST_ASSERT(e, akgl_controlmaps[0].nextMap == 1, "pushing one control left nextMap at %d, expected 1", akgl_controlmaps[0].nextMap); TEST_ASSERT(e, akgl_controlmaps[0].controls[0].key == SDLK_SPACE, "the pushed control did not land in slot 0"); TEST_ASSERT(e, akgl_controlmaps[0].controls[0].handler_on == &akgl_actor_cmhf_left_on, "the pushed control lost its press handler"); // Pushes accumulate rather than overwrite. TEST_EXPECT_OK(e, akgl_controller_pushmap(0, &control), "pushing a second control"); TEST_ASSERT(e, akgl_controlmaps[0].nextMap == 2, "pushing a second control left nextMap at %d, expected 2", akgl_controlmaps[0].nextMap); // Maps are independent of each other. TEST_EXPECT_OK(e, akgl_controller_pushmap(3, &control), "pushing into a different map"); TEST_ASSERT(e, akgl_controlmaps[3].nextMap == 1, "map 3 nextMap is %d, expected 1", akgl_controlmaps[3].nextMap); TEST_ASSERT(e, akgl_controlmaps[0].nextMap == 2, "pushing into map 3 disturbed map 0 (nextMap %d)", akgl_controlmaps[0].nextMap); // Filling a map exactly to capacity still succeeds. reset_control_maps(); for ( i = 0; i < AKGL_MAX_CONTROLS; i++ ) { akerr_ErrorContext *pushresult = akgl_controller_pushmap(0, &control); if ( pushresult != NULL ) { pushresult->handled = true; pushresult = akerr_release_error(pushresult); FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, "pushing control %d of %d failed", i, AKGL_MAX_CONTROLS); } } TEST_ASSERT(e, akgl_controlmaps[0].nextMap == AKGL_MAX_CONTROLS, "a full map reports nextMap %d, expected %d", akgl_controlmaps[0].nextMap, AKGL_MAX_CONTROLS); // One past capacity is refused. TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_controller_pushmap(0, &control), "pushing into a full control map"); TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_pushmap(0, NULL), "pushing a NULL control"); TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_controller_pushmap(AKGL_MAX_CONTROL_MAPS, &control), "pushing into a control map id at the limit"); TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_controller_pushmap(AKGL_MAX_CONTROL_MAPS + 5, &control), "pushing into a control map id past the limit"); } CLEANUP { reset_control_maps(); } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); } akerr_ErrorContext *test_controller_default_bindings(void) { PREPARE_ERROR(e); ATTEMPT { reset_control_maps(); CATCH(e, make_player()); TEST_EXPECT_OK(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID), "installing the default control map"); TEST_ASSERT(e, akgl_controlmaps[0].target == player, "the default map did not target the player actor"); TEST_ASSERT(e, akgl_controlmaps[0].kbid == TEST_KBID, "the default map recorded keyboard %d, expected %d", akgl_controlmaps[0].kbid, TEST_KBID); TEST_ASSERT(e, akgl_controlmaps[0].jsid == TEST_JSID, "the default map recorded gamepad %d, expected %d", akgl_controlmaps[0].jsid, TEST_JSID); // Four keyboard bindings then four gamepad bindings. TEST_ASSERT(e, akgl_controlmaps[0].nextMap == 8, "the default map installed %d controls, expected 8", akgl_controlmaps[0].nextMap); TEST_ASSERT(e, akgl_controlmaps[0].controls[0].key == SDLK_DOWN, "the first default binding is not the down arrow"); TEST_ASSERT(e, akgl_controlmaps[0].controls[0].handler_on == &akgl_actor_cmhf_down_on, "the down arrow is not bound to the down handler"); TEST_ASSERT(e, akgl_controlmaps[0].controls[1].key == SDLK_UP, "the second default binding is not the up arrow"); TEST_ASSERT(e, akgl_controlmaps[0].controls[2].key == SDLK_LEFT, "the third default binding is not the left arrow"); TEST_ASSERT(e, akgl_controlmaps[0].controls[3].key == SDLK_RIGHT, "the fourth default binding is not the right arrow"); // The gamepad half binds buttons and leaves the keycode clear, so a // keyboard event cannot accidentally match a dpad binding. TEST_ASSERT(e, akgl_controlmaps[0].controls[4].button == SDL_GAMEPAD_BUTTON_DPAD_DOWN, "the fifth default binding is not the dpad down button"); TEST_ASSERT(e, akgl_controlmaps[0].controls[4].key == 0, "a gamepad binding also carries a keycode"); TEST_ASSERT(e, akgl_controlmaps[0].controls[7].button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT, "the eighth default binding is not the dpad right button"); // An unknown actor name is a registry error, not a crash. reset_control_maps(); TEST_EXPECT_STATUS(e, AKGL_ERR_REGISTRY, akgl_controller_default(1, "no_such_actor", TEST_KBID, TEST_JSID), "installing a default map for an unregistered actor"); TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_controller_default(AKGL_MAX_CONTROL_MAPS, "player", TEST_KBID, TEST_JSID), "installing a default map at a control map id past the limit"); } CLEANUP { reset_control_maps(); } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); } akerr_ErrorContext *test_controller_handle_keyboard_events(void) { PREPARE_ERROR(e); SDL_Event event; ATTEMPT { reset_control_maps(); CATCH(e, make_player()); CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID)); // Pressing left routes to the left press handler. player->state = 0; make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT); TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching a left key press"); TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT), "a left key press did not start the actor moving left (state %d)", player->state); TEST_ASSERT_FEQ(e, player->ax, -6.0f, "a left key press set ax to %f, expected -6", player->ax); // Releasing it routes to the release handler. make_key_event(&event, SDL_EVENT_KEY_UP, TEST_KBID, SDLK_LEFT); TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching a left key release"); TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_LEFT), "a left key release did not stop the actor (state %d)", player->state); // The other three directions route the same way. player->state = 0; make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_RIGHT); TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching a right key press"); TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT), "a right key press did not start the actor moving right (state %d)", player->state); player->state = 0; make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_UP); TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching an up key press"); TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP), "an up key press did not start the actor moving up (state %d)", player->state); player->state = 0; make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_DOWN); TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching a down key press"); TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_DOWN), "a down key press did not start the actor moving down (state %d)", player->state); // An unbound key is ignored. player->state = 0; make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_F12); TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching an unbound key"); TEST_ASSERT(e, player->state == 0, "an unbound key changed the actor state to %d", player->state); // A bound key from a different keyboard is ignored, so split-keyboard // local multiplayer does not cross-talk. make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID + 1, SDLK_LEFT); TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching a bound key from an unbound keyboard"); TEST_ASSERT(e, player->state == 0, "a key from another keyboard changed the actor state to %d", player->state); } CLEANUP { reset_control_maps(); } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); } akerr_ErrorContext *test_controller_handle_gamepad_events(void) { PREPARE_ERROR(e); SDL_Event event; ATTEMPT { reset_control_maps(); CATCH(e, make_player()); CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID)); player->state = 0; make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT); TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching a dpad left press"); TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT), "a dpad left press did not start the actor moving left (state %d)", player->state); make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_UP, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT); TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching a dpad left release"); TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_LEFT), "a dpad left release did not stop the actor (state %d)", player->state); player->state = 0; make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_UP); TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching a dpad up press"); TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP), "a dpad up press did not start the actor moving up (state %d)", player->state); // A press from an unbound gamepad is ignored. player->state = 0; make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID + 1, SDL_GAMEPAD_BUTTON_DPAD_LEFT); TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching a dpad press from an unbound gamepad"); TEST_ASSERT(e, player->state == 0, "a press from another gamepad changed the actor state to %d", player->state); // An unbound button on the bound gamepad is ignored. make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_START); TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching an unbound gamepad button"); TEST_ASSERT(e, player->state == 0, "an unbound gamepad button changed the actor state to %d", player->state); } CLEANUP { reset_control_maps(); } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); } akerr_ErrorContext *test_controller_handle_event_edge_cases(void) { PREPARE_ERROR(e); SDL_Event event; ATTEMPT { reset_control_maps(); CATCH(e, make_player()); // With no control maps installed at all, every event is a no-op. make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT); TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching into an empty control map table"); // A map in a later slot is still reached, so the scan does not stop at // the first unpopulated entry. CATCH(e, akgl_controller_default(5, "player", TEST_KBID, TEST_JSID)); player->state = 0; TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching to a control map in a later slot"); TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT), "a control map in slot 5 was not consulted (state %d)", player->state); // An event type that no binding uses falls through harmlessly. memset(&event, 0x00, sizeof(SDL_Event)); event.type = SDL_EVENT_MOUSE_MOTION; TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching an event type with no bindings"); TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_handle_event(NULL, &event), "dispatching with a NULL appstate"); TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_handle_event(&appstate_placeholder, NULL), "dispatching a NULL event"); } CLEANUP { reset_control_maps(); } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); } akerr_ErrorContext *test_controller_gamepad_button_handlers(void) { PREPARE_ERROR(e); SDL_Event event; ATTEMPT { CATCH(e, make_player()); // These handlers find their actor by looking up "player" in the registry // rather than taking it as an argument. player->state = 0; player->movement_controls_face = false; make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN); TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad down press handler"); TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_DOWN), "the dpad down handler did not set MOVING_DOWN (state %d)", player->state); // With automatic facing off, the handler sets the facing itself. TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_FACE_DOWN), "the dpad down handler did not set FACE_DOWN (state %d)", player->state); TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad down release handler"); TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_DOWN), "the dpad down release handler did not clear MOVING_DOWN (state %d)", player->state); TEST_ASSERT(e, player->curSpriteFrameId == 0, "the release handler did not reset the animation frame"); player->state = 0; make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_UP); TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad up press handler"); TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP), "the dpad up handler did not set MOVING_UP (state %d)", player->state); TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad up release handler"); player->state = 0; make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT); TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad left press handler"); TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT), "the dpad left handler did not set MOVING_LEFT (state %d)", player->state); TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad left release handler"); player->state = 0; make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_RIGHT); TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad right press handler"); TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT), "the dpad right handler did not set MOVING_RIGHT (state %d)", player->state); TEST_EXPECT_OK(e, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad right release handler"); // With automatic facing on, the handler leaves facing to the actor's own // face function. player->state = 0; player->movement_controls_face = true; make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN); TEST_EXPECT_OK(e, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad down press with automatic facing on"); TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_FACE_DOWN), "the handler set facing even though the actor faces automatically (state %d)", player->state); TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_button_down(NULL, &event), "dpad press handler with a NULL appstate"); TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_button_down(&appstate_placeholder, NULL), "dpad press handler with a NULL event"); TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_button_up(NULL, &event), "dpad release handler with a NULL appstate"); TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_button_up(&appstate_placeholder, NULL), "dpad release handler with a NULL event"); // With no actor named "player" registered there is nothing to drive. CATCH(e, akgl_registry_init_actor()); make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN); TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_button_down(&appstate_placeholder, &event), "dpad press handler with no player actor registered"); TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_button_up(&appstate_placeholder, &event), "dpad release handler with no player actor registered"); } CLEANUP { } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); } akerr_ErrorContext *test_controller_device_events(void) { PREPARE_ERROR(e); SDL_Event event; ATTEMPT { // The dummy drivers present no gamepads, so the add and remove handlers // take their "unknown device" paths. Neither may crash. make_button_event(&event, SDL_EVENT_GAMEPAD_ADDED, 999, 0); TEST_EXPECT_OK(e, gamepad_handle_added(&appstate_placeholder, &event), "handling an add for a device that cannot be opened"); make_button_event(&event, SDL_EVENT_GAMEPAD_REMOVED, 999, 0); TEST_EXPECT_OK(e, gamepad_handle_removed(&appstate_placeholder, &event), "handling a remove for a device that was never open"); TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_added(NULL, &event), "add handler with a NULL appstate"); TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_added(&appstate_placeholder, NULL), "add handler with a NULL event"); TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_removed(NULL, &event), "remove handler with a NULL appstate"); TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, gamepad_handle_removed(&appstate_placeholder, NULL), "remove handler with a NULL event"); } CLEANUP { } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); } akerr_ErrorContext *test_controller_device_enumeration(void) { PREPARE_ERROR(e); ATTEMPT { // Enumeration has to succeed on a machine with no input devices, which // is the normal state under the dummy drivers and in CI. TEST_EXPECT_OK(e, akgl_controller_list_keyboards(), "listing keyboards"); TEST_EXPECT_OK(e, akgl_controller_open_gamepads(), "opening gamepads"); } CLEANUP { } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); } akerr_ErrorContext *test_controller_poll_key(void) { PREPARE_ERROR(e); SDL_Event event; int keycode = -1; bool available = true; ATTEMPT { reset_control_maps(); CATCH(e, make_player()); CATCH(e, akgl_controller_flush_keys()); // An empty buffer answers "no key waiting" and succeeds. TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling an empty key buffer"); TEST_ASSERT(e, available == false, "polling an empty buffer reported a key was available"); TEST_ASSERT(e, keycode == 0, "polling an empty buffer left keycode at %d, expected 0", keycode); // A key press pumped through the handler is drained by the poller, // exactly once. make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_A); TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching a key press with no control maps installed"); TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling for that key"); TEST_ASSERT(e, available == true, "the pressed key was not available to the poller"); TEST_ASSERT(e, keycode == SDLK_A, "the poller returned keycode %d, expected %d", keycode, (int)SDLK_A); TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling again"); TEST_ASSERT(e, available == false, "the same keystroke was delivered twice"); // Key releases are not keystrokes. make_key_event(&event, SDL_EVENT_KEY_UP, TEST_KBID, SDLK_A); TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching a key release"); TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling after a release"); TEST_ASSERT(e, available == false, "a key release was buffered as a keystroke"); // Keys come back in the order they were pressed. CATCH(e, akgl_controller_flush_keys()); make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_1); CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event)); make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_2); CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event)); make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_3); CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event)); TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the first key"); TEST_ASSERT(e, keycode == SDLK_1, "the first key out was %d, expected %d", keycode, (int)SDLK_1); TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the second key"); TEST_ASSERT(e, keycode == SDLK_2, "the second key out was %d, expected %d", keycode, (int)SDLK_2); TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the third key"); TEST_ASSERT(e, keycode == SDLK_3, "the third key out was %d, expected %d", keycode, (int)SDLK_3); // A key that a control map also acts on still reaches the poller, so a // game and an embedded interpreter can share one keyboard. CATCH(e, akgl_controller_flush_keys()); CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID)); player->state = 0; make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT); TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching a bound key press"); TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT), "the bound key stopped driving the actor (state %d)", player->state); TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling for a key a control map claimed"); TEST_ASSERT(e, available == true, "a bound key never reached the key buffer"); TEST_ASSERT(e, keycode == SDLK_LEFT, "the poller returned keycode %d, expected %d", keycode, (int)SDLK_LEFT); // Flushing discards the backlog. CATCH(e, akgl_controller_flush_keys()); make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_B); CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event)); TEST_EXPECT_OK(e, akgl_controller_flush_keys(), "flushing a buffer with a key in it"); TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling after a flush"); TEST_ASSERT(e, available == false, "a flush left a keystroke behind"); TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_poll_key(NULL, &available), "polling into a NULL keycode"); TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_poll_key(&keycode, NULL), "polling into a NULL availability flag"); } CLEANUP { reset_control_maps(); IGNORE(akgl_controller_flush_keys()); } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); } akerr_ErrorContext *test_controller_poll_key_overflow(void) { PREPARE_ERROR(e); SDL_Event event; int keycode = -1; bool available = true; bool ordered = true; bool pumped = true; int i = 0; ATTEMPT { reset_control_maps(); CATCH(e, akgl_controller_flush_keys()); // Fill the buffer exactly, then press one more. The overflowing key is // the one that is dropped -- what was typed first survives. for ( i = 0; i < AKGL_CONTROLLER_KEY_BUFFER + 1; i++ ) { akerr_ErrorContext *pumpresult = NULL; make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_A + i); pumpresult = akgl_controller_handle_event(&appstate_placeholder, &event); if ( pumpresult != NULL ) { pumpresult->handled = true; pumpresult = akerr_release_error(pumpresult); pumped = false; } } TEST_ASSERT(e, pumped == true, "dispatching the overflow key presses failed"); for ( i = 0; i < AKGL_CONTROLLER_KEY_BUFFER; i++ ) { akerr_ErrorContext *pollresult = akgl_controller_poll_key(&keycode, &available); if ( pollresult != NULL ) { pollresult->handled = true; pollresult = akerr_release_error(pollresult); ordered = false; } TEST_ASSERT_FLAG(ordered, available == true); TEST_ASSERT_FLAG(ordered, keycode == (int)(SDLK_A + i)); } TEST_ASSERT(e, ordered == true, "a full buffer did not return the first %d keys in order", AKGL_CONTROLLER_KEY_BUFFER); TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling after draining a full buffer"); TEST_ASSERT(e, available == false, "the key pressed past capacity was buffered anyway (keycode %d)", keycode); // The buffer is reusable after an overflow rather than wedged. make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_Z); TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event), "dispatching a key after an overflow"); TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling after an overflow"); TEST_ASSERT(e, available == true, "the buffer stayed full after being drained"); TEST_ASSERT(e, keycode == SDLK_Z, "the poller returned keycode %d, expected %d", keycode, (int)SDLK_Z); } CLEANUP { reset_control_maps(); IGNORE(akgl_controller_flush_keys()); } PROCESS(e) { } FINISH(e, true); SUCCEED_RETURN(e); } akerr_ErrorContext *test_controller_poll_keystroke(void) { PREPARE_ERROR(errctx); SDL_Event event; akgl_Keystroke keystroke; int keycode = -1; bool available = true; ATTEMPT { reset_control_maps(); CATCH(errctx, akgl_controller_flush_keys()); // An empty buffer answers "nothing waiting" and zeroes the destination. memset(&keystroke, 0xff, sizeof(akgl_Keystroke)); TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available), "polling an empty keystroke buffer"); TEST_ASSERT(errctx, available == false, "polling an empty buffer reported a keystroke was available"); TEST_ASSERT(errctx, keystroke.key == 0, "polling an empty buffer left a keycode of %d behind", (int)keystroke.key); TEST_ASSERT(errctx, keystroke.text[0] == '\0', "polling an empty buffer left text behind"); // The case the whole thing exists for: a shifted key. The keycode is // the unshifted one, so the composed character is the only place the // double quote can come from. make_key_event_mod(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_APOSTROPHE, SDL_KMOD_LSHIFT); CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); make_text_event(&event, "\""); CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available), "polling for a shifted key"); TEST_ASSERT(errctx, available == true, "a shifted key press never reached the buffer"); TEST_ASSERT(errctx, keystroke.key == SDLK_APOSTROPHE, "the keystroke reported keycode %d, expected %d", (int)keystroke.key, (int)SDLK_APOSTROPHE); TEST_ASSERT(errctx, (keystroke.mod & SDL_KMOD_SHIFT) != 0, "the keystroke lost its shift state (mod %d)", (int)keystroke.mod); TEST_ASSERT(errctx, strcmp(keystroke.text, "\"") == 0, "the keystroke composed to \"%s\", expected a double quote", keystroke.text); // A key that composes to nothing still arrives, with empty text -- that // is how a line editor recognises Backspace and the arrows. CATCH(errctx, akgl_controller_flush_keys()); make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT); CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available), "polling for a non-printing key"); TEST_ASSERT(errctx, keystroke.key == SDLK_LEFT, "a non-printing key reported keycode %d, expected %d", (int)keystroke.key, (int)SDLK_LEFT); TEST_ASSERT(errctx, keystroke.text[0] == '\0', "a non-printing key composed to \"%s\", expected nothing", keystroke.text); // Text with no key press behind it -- an input method, or a character // finished by a dead key -- is buffered on its own rather than dropped. CATCH(errctx, akgl_controller_flush_keys()); make_text_event(&event, "e"); CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available), "polling for composed text with no key press"); TEST_ASSERT(errctx, available == true, "composed text with no key press was dropped"); TEST_ASSERT(errctx, keystroke.key == 0, "text with no key press reported keycode %d, expected 0", (int)keystroke.key); TEST_ASSERT(errctx, strcmp(keystroke.text, "e") == 0, "text with no key press composed to \"%s\", expected \"e\"", keystroke.text); TEST_ASSERT(errctx, keystroke.mod == 0, "a text-only entry reported modifiers %d; the character it carries already " "reflects them", (int)keystroke.mod); // The keycode form has nowhere to report that entry, so it discards it // on the way past rather than handing back a keystroke with no key. CATCH(errctx, akgl_controller_flush_keys()); make_text_event(&event, "e"); CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_B); CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); TEST_EXPECT_OK(errctx, akgl_controller_poll_key(&keycode, &available), "polling by keycode past a text-only entry"); TEST_ASSERT(errctx, available == true, "the key behind a text-only entry was lost"); TEST_ASSERT(errctx, keycode == SDLK_B, "polling by keycode returned %d, expected %d", keycode, (int)SDLK_B); // Both pollers drain the same buffer. CATCH(errctx, akgl_controller_flush_keys()); make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_C); CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); CATCH(errctx, akgl_controller_poll_keystroke(&keystroke, &available)); TEST_EXPECT_OK(errctx, akgl_controller_poll_key(&keycode, &available), "polling by keycode after the same key was taken as a keystroke"); TEST_ASSERT(errctx, available == false, "one keystroke was delivered to both pollers"); // Text that arrives after its press has already been drained becomes an // entry of its own instead of being attached to somebody else's key. CATCH(errctx, akgl_controller_flush_keys()); make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_D); CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); CATCH(errctx, akgl_controller_poll_keystroke(&keystroke, &available)); make_text_event(&event, "d"); CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available), "polling for text that outlived its key press"); TEST_ASSERT(errctx, available == true, "text that outlived its key press was dropped"); TEST_ASSERT(errctx, keystroke.key == 0, "text that outlived its key press was attached to keycode %d", (int)keystroke.key); // More text than an entry holds is cut on a character boundary, never // through the middle of one. Each of these is two bytes, so three fit // in the eight-byte field and the fourth does not. CATCH(errctx, akgl_controller_flush_keys()); make_text_event(&event, "\xce\xb1\xce\xb1\xce\xb1\xce\xb1"); CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available), "polling for text longer than one entry holds"); TEST_ASSERT(errctx, strcmp(keystroke.text, "\xce\xb1\xce\xb1\xce\xb1") == 0, "oversized text was truncated to \"%s\", expected three whole characters", keystroke.text); // A sequence the string ends in the middle of is dropped rather than // copied by reading past the terminator to find its other half. CATCH(errctx, akgl_controller_flush_keys()); make_text_event(&event, "a\xce"); CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event)); TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available), "polling for text that ends mid-character"); TEST_ASSERT(errctx, strcmp(keystroke.text, "a") == 0, "a truncated character came back as \"%s\", expected just the whole one", keystroke.text); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_controller_poll_keystroke(NULL, &available), "polling into a NULL keystroke"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_controller_poll_keystroke(&keystroke, NULL), "polling into a NULL availability flag"); } CLEANUP { reset_control_maps(); IGNORE(akgl_controller_flush_keys()); } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } int main(void) { PREPARE_ERROR(errctx); SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); ATTEMPT { CATCH(errctx, akgl_error_init()); TEST_TRAP_UNHANDLED_ERRORS(); FAIL_ZERO_BREAK( errctx, SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD), AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError()); CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_registry_init()); CATCH(errctx, test_controller_pushmap()); CATCH(errctx, test_controller_default_bindings()); CATCH(errctx, test_controller_handle_keyboard_events()); CATCH(errctx, test_controller_handle_gamepad_events()); CATCH(errctx, test_controller_handle_event_edge_cases()); CATCH(errctx, test_controller_gamepad_button_handlers()); CATCH(errctx, test_controller_device_events()); CATCH(errctx, test_controller_device_enumeration()); CATCH(errctx, test_controller_poll_key()); CATCH(errctx, test_controller_poll_key_overflow()); CATCH(errctx, test_controller_poll_keystroke()); } CLEANUP { SDL_Quit(); } PROCESS(errctx) { } FINISH_NORETURN(errctx); }