joeylib2/tests/host/inputHost.c

504 lines
17 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// inputHost.c - Host-cc harness for the typed-character queue
// (jlInputGetChar / jlInputCharPush in src/core/input.c). Exercises
// FIFO ordering, the character filter, drop-newest overflow, reset
// semantics, and the jlInputPoll refill path via a mocked
// jlpInputPoll; a one-button stick's tap (button 0) and hold
// (button 1), with a mocked jlMillisElapsed; the dead zone, axis scaling
// and chord rule every port shares (inputDeadZone, inputAxisScale,
// inputCharTyped); a key two physical keys give, down while either is
// held (inputKeyHeld); the mouse every port starts in the middle of the
// stage and keeps on it (inputReset, inputMouseMove); and what ends
// jlWaitForAnyKey, through a scripted jlpInputPoll.
//
// Built by scripts/check-input.sh with -DJOEYLIB_PLATFORM_BLANK so
// port.h routes jlpInputPoll/jlpJoystickReset to the mocks below.
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include "joey/core.h"
#include "joey/input.h"
#include "inputInternal.h"
#include "port.h"
// Characters the mocked jlpInputPoll delivers on its next call
// (one-shot; cleared after delivery).
static const uint8_t *gMockChars = NULL;
static int gMockCount = 0;
// What the mocked jlpInputPoll reports for stick 0's buttons, and the
// mocked clock.
static uint8_t gMockButtons[JL_JOY_BUTTON_COUNT];
static uint32_t gMockMillis = 0;
// A script for jlWaitForAnyKey: each poll applies its next step, the keys
// held and the characters typed. Past its end the poll holds ESCAPE, which
// ends any wait, and says so in gWaitOverrun.
typedef struct {
uint8_t key; // a jlKeyE held this step, or JL_KEY_NONE
uint8_t key2; // another, or JL_KEY_NONE
const char *chars; // typed this step
} WaitStepT;
// One of a key's two physical keys going down or up, and whether the key
// must then be down.
typedef struct {
uint8_t key; // the key, with INPUT_KEY_SECOND for its second
bool down;
bool held;
} KeyStepT;
static const WaitStepT *gWaitScript = NULL;
static int gWaitSteps = 0;
static int gWaitAt = 0;
static bool gWaitOverrun = false;
// Prototypes (harness-local, alphabetical).
static bool checkEmpty(const char *name);
static bool pollJoy(const char *name, uint32_t millis, bool fire, bool down0, bool down1);
static bool popExpect(const char *name, const uint8_t *expect, int count);
static void pushAll(const uint8_t *chars, int count);
static bool runAxisScale(void);
static bool runChord(void);
static bool runDeadZone(void);
static bool runFilter(void);
static bool runInterleave(void);
static bool runMouse(void);
static bool runOrder(void);
static bool runOverflow(void);
static bool runPollRefill(void);
static bool runReset(void);
static bool runSecondKey(void);
static bool runTapOrHold(void);
static bool runWait(void);
static bool waitEndsAt(const char *name, const WaitStepT *script, int steps, int expect);
void jlpInputPoll(void) {
const char *c;
int i;
if (gWaitScript != NULL) {
memset(gKeyState, 0, sizeof(gKeyState));
if (gWaitAt < gWaitSteps) {
gKeyState[gWaitScript[gWaitAt].key] = 1u;
gKeyState[gWaitScript[gWaitAt].key2] = 1u;
for (c = gWaitScript[gWaitAt].chars; *c != '\0'; c++) {
jlInputCharPush((uint8_t)*c);
}
gWaitAt++;
} else {
gKeyState[JL_KEY_ESCAPE] = 1u;
gWaitOverrun = true;
}
gKeyState[JL_KEY_NONE] = 0u;
return;
}
for (i = 0; i < gMockCount; i++) {
jlInputCharPush(gMockChars[i]);
}
gMockCount = 0;
gJoyButtonState[JL_JOYSTICK_0][JL_JOY_BUTTON_0] = gMockButtons[JL_JOY_BUTTON_0];
gJoyButtonState[JL_JOYSTICK_0][JL_JOY_BUTTON_1] = gMockButtons[JL_JOY_BUTTON_1];
}
uint32_t jlMillisElapsed(void) {
return gMockMillis;
}
void jlpJoystickReset(jlJoystickE js) {
(void)js;
}
static bool checkEmpty(const char *name) {
int got;
got = jlInputGetChar();
if (got != -1) {
printf(" %s: expected empty (-1), got %d\n", name, got);
return false;
}
return true;
}
// One poll at `millis` with stick 0's (first) button `fire`; true when
// buttons 0 and 1 then read down0 and down1.
static bool pollJoy(const char *name, uint32_t millis, bool fire, bool down0, bool down1) {
bool got0;
bool got1;
gMockMillis = millis;
gMockButtons[JL_JOY_BUTTON_0] = fire;
jlInputPoll();
got0 = jlJoyDown(JL_JOYSTICK_0, JL_JOY_BUTTON_0);
got1 = jlJoyDown(JL_JOYSTICK_0, JL_JOY_BUTTON_1);
if (got0 != down0 || got1 != down1) {
printf(" %s: at %u ms, fire %d: buttons %d %d, want %d %d\n", name, (unsigned)millis, fire, got0, got1, down0, down1);
return false;
}
return true;
}
static bool popExpect(const char *name, const uint8_t *expect, int count) {
int got;
int i;
for (i = 0; i < count; i++) {
got = jlInputGetChar();
if (got != (int)expect[i]) {
printf(" %s: index %d: expected 0x%02X, got %d\n", name, i, expect[i], got);
return false;
}
}
return checkEmpty(name);
}
static void pushAll(const uint8_t *chars, int count) {
int i;
for (i = 0; i < count; i++) {
jlInputCharPush(chars[i]);
}
}
// A raw axis reads -127..127 rounded to the nearest: the ends at the ends,
// and a centred stick 0 from either middle value of an even span (DVX's
// -32768..32767 read -1 at rest before it was rounded).
static bool runAxisScale(void) {
static const int32_t kPos[] = { INT16_MIN, INT16_MAX, 0, -1, 32767, 32768, 0, 65535, -100 };
static const int32_t kMin[] = { INT16_MIN, INT16_MIN, INT16_MIN, INT16_MIN, 0, 0, 0, 0, 0 };
static const int32_t kMax[] = { INT16_MAX, INT16_MAX, INT16_MAX, INT16_MAX, 65535, 65535, 65535, 65535, 65535 };
static const int8_t kOut[] = { -127, 127, 0, 0, 0, 0, -127, 127, -127 };
uint16_t i;
bool ok;
ok = true;
for (i = 0u; i < (uint16_t)(sizeof(kPos) / sizeof(kPos[0])); i++) {
if (inputAxisScale(kPos[i], kMin[i], kMax[i]) != kOut[i]) {
printf(" axis-scale: %ld in %ld..%ld gave %d, want %d\n", (long)kPos[i], (long)kMin[i], (long)kMax[i], inputAxisScale(kPos[i], kMin[i], kMax[i]), kOut[i]);
ok = false;
}
}
printf(" axis-scale: %s\n", ok ? "PASS" : "FAIL");
return ok;
}
// A chord types nothing, printable or not; the same characters typed
// plainly arrive.
static bool runChord(void) {
bool ok;
jlInputCharReset();
inputCharTyped('m', true);
inputCharTyped(JL_CHAR_RETURN, true);
inputCharTyped(JL_CHAR_DELETE, true);
ok = jlInputGetChar() == -1;
inputCharTyped('m', false);
inputCharTyped(JL_CHAR_RETURN, false);
ok = ok && jlInputGetChar() == 'm' && jlInputGetChar() == JL_CHAR_RETURN && jlInputGetChar() == -1;
printf(" chord: %s\n", ok ? "PASS" : "FAIL");
return ok;
}
// A reading within the dead zone of centre is 0, at its edge too; one past
// it is the reading; a zone of 0 leaves every reading but 0 alone.
static bool runDeadZone(void) {
static const int8_t kIn[] = { 10, 11, -10, -11, 0, 1, -1, 127, -127 };
static const uint8_t kZone[] = { 10, 10, 10, 10, 0, 0, 0, 10, 10 };
static const int8_t kOut[] = { 0, 11, 0, -11, 0, 1, -1, 127, -127 };
uint16_t i;
bool ok;
ok = true;
for (i = 0u; i < (uint16_t)(sizeof(kIn) / sizeof(kIn[0])); i++) {
if (inputDeadZone(kIn[i], kZone[i]) != kOut[i]) {
printf(" dead-zone: %d in a zone of %u gave %d, want %d\n", kIn[i], kZone[i], inputDeadZone(kIn[i], kZone[i]), kOut[i]);
ok = false;
}
}
printf(" dead-zone: %s\n", ok ? "PASS" : "FAIL");
return ok;
}
// Control characters other than BS/TAB/CR/ESC, and 8-bit values, must
// never surface; those four, the printable range and JL_CHAR_DELETE
// (0x7F, forward delete) must pass through unchanged.
static bool runFilter(void) {
static const uint8_t rejected[] = { 0x00, 0x01, 0x07, 0x0A, 0x0C, 0x1F, 0x80, 0xA0, 0xFF };
static const uint8_t accepted[] = {
JL_CHAR_BACKSPACE, JL_CHAR_TAB, JL_CHAR_RETURN, JL_CHAR_ESCAPE,
0x20, '0', 'Z', 'z', '~', 0x7E, JL_CHAR_DELETE
};
bool ok;
jlInputCharReset();
pushAll(rejected, (int)sizeof(rejected));
ok = checkEmpty("filter-rejects");
pushAll(accepted, (int)sizeof(accepted));
ok &= popExpect("filter-accepts", accepted, (int)sizeof(accepted));
printf(" filter: %s\n", ok ? "PASS" : "FAIL");
return ok;
}
// Pops interleaved with pushes keep FIFO order across the wrap point.
static bool runInterleave(void) {
static const uint8_t first[] = { 'a', 'b', 'c' };
static const uint8_t second[] = { 'd', 'e' };
static const uint8_t rest[] = { 'b', 'c', 'd', 'e' };
bool ok;
int got;
jlInputCharReset();
pushAll(first, (int)sizeof(first));
got = jlInputGetChar();
ok = (got == 'a');
if (!ok) {
printf(" interleave: expected 'a', got %d\n", got);
}
pushAll(second, (int)sizeof(second));
ok &= popExpect("interleave", rest, (int)sizeof(rest));
printf(" interleave: %s\n", ok ? "PASS" : "FAIL");
return ok;
}
// The definition-of-done string from the RetroNet handoff must come
// back byte-for-byte.
// jlInit's reset puts the mouse in the middle of the stage; a port's moves
// stay on it.
static bool runMouse(void) {
bool ok;
inputMouseMove(5, 7);
inputReset();
ok = jlMouseX() == JL_SURFACE_WIDTH / 2 && jlMouseY() == JL_SURFACE_HEIGHT / 2;
inputMouseMove(-5, 250);
ok = ok && jlMouseX() == 0 && jlMouseY() == JL_SURFACE_HEIGHT - 1;
inputMouseMove(400, -1);
ok = ok && jlMouseX() == JL_SURFACE_WIDTH - 1 && jlMouseY() == 0;
inputMouseMove(100, 50);
ok = ok && jlMouseX() == 100 && jlMouseY() == 50;
printf(" mouse: %s\n", ok ? "PASS" : "FAIL");
return ok;
}
static bool runOrder(void) {
static const char target[] = "192.168.1.10:6510";
bool ok;
jlInputCharReset();
pushAll((const uint8_t *)target, (int)(sizeof(target) - 1));
ok = popExpect("order", (const uint8_t *)target, (int)(sizeof(target) - 1));
printf(" order: %s\n", ok ? "PASS" : "FAIL");
return ok;
}
// Overflow drops the NEWEST characters: exactly JL_CHAR_QUEUE_SIZE - 1
// survive, and they are the first ones pushed.
static bool runOverflow(void) {
uint8_t burst[JL_CHAR_QUEUE_SIZE + 8];
bool ok;
int i;
jlInputCharReset();
for (i = 0; i < (int)sizeof(burst); i++) {
burst[i] = (uint8_t)('!' + (i % 0x5E));
}
pushAll(burst, (int)sizeof(burst));
ok = popExpect("overflow", burst, JL_CHAR_QUEUE_SIZE - 1);
printf(" overflow: %s\n", ok ? "PASS" : "FAIL");
return ok;
}
// The real app-facing path: jlInputPoll runs the port poll, which
// pushes translated characters; jlInputGetChar then pops them.
static bool runPollRefill(void) {
static const uint8_t typed[] = { 'H', 'i', '!', JL_CHAR_RETURN };
bool ok;
jlInputCharReset();
gMockChars = typed;
gMockCount = (int)sizeof(typed);
jlInputPoll();
ok = popExpect("poll-refill", typed, (int)sizeof(typed));
jlInputPoll();
ok &= checkEmpty("poll-refill-once");
printf(" poll-refill: %s\n", ok ? "PASS" : "FAIL");
return ok;
}
// jlInputCharReset empties the queue (the jlInit path).
static bool runReset(void) {
static const uint8_t some[] = { 'x', 'y', 'z' };
bool ok;
jlInputCharReset();
pushAll(some, (int)sizeof(some));
jlInputCharReset();
ok = checkEmpty("reset");
printf(" reset: %s\n", ok ? "PASS" : "FAIL");
return ok;
}
// A one-button stick: a press let go within half a second is button 0,
// down for the one poll after it is let go; a longer press is button 1
// from half a second until it is let go, with no button 0 after it. A
// two-button stick's buttons pass through as they are.
// A key two physical keys give is down while either is held, whichever goes
// up first; a repeated press or a stray release of one changes nothing.
static bool runSecondKey(void) {
static const KeyStepT kSteps[] = {
{ JL_KEY_UP, true, true },
{ JL_KEY_UP | INPUT_KEY_SECOND, true, true },
{ JL_KEY_UP, false, true },
{ JL_KEY_UP, true, true },
{ JL_KEY_UP | INPUT_KEY_SECOND, false, true },
{ JL_KEY_UP | INPUT_KEY_SECOND, false, true },
{ JL_KEY_UP, false, false },
{ JL_KEY_UP | INPUT_KEY_SECOND, true, true },
{ JL_KEY_UP | INPUT_KEY_SECOND, true, true },
{ JL_KEY_UP | INPUT_KEY_SECOND, false, false },
};
uint16_t i;
uint8_t held;
bool ok;
ok = true;
held = 0u;
for (i = 0u; i < (uint16_t)(sizeof(kSteps) / sizeof(kSteps[0])); i++) {
held = inputKeyHeld(held, kSteps[i].key, kSteps[i].down);
if ((held != 0u) != kSteps[i].held) {
printf(" second-key: step %u left the key %s\n", i, held != 0u ? "down" : "up");
ok = false;
}
}
printf(" second-key: %s\n", ok ? "PASS" : "FAIL");
return ok;
}
static bool runTapOrHold(void) {
bool ok = true;
gJoyOneButton[JL_JOYSTICK_0] = true;
ok &= pollJoy("tap down", 1000u, true, false, false);
ok &= pollJoy("tap held", 1300u, true, false, false);
ok &= pollJoy("tap let go", 1350u, false, true, false);
ok &= jlJoyPressed(JL_JOYSTICK_0, JL_JOY_BUTTON_0);
ok &= pollJoy("tap over", 1370u, false, false, false);
ok &= jlJoyReleased(JL_JOYSTICK_0, JL_JOY_BUTTON_0);
ok &= pollJoy("hold down", 2000u, true, false, false);
ok &= pollJoy("hold at 499 ms", 2499u, true, false, false);
ok &= pollJoy("hold at 500 ms", 2500u, true, false, true);
ok &= jlJoyPressed(JL_JOYSTICK_0, JL_JOY_BUTTON_1);
ok &= pollJoy("hold on", 2900u, true, false, true);
ok &= pollJoy("hold let go", 2950u, false, false, false);
ok &= jlJoyReleased(JL_JOYSTICK_0, JL_JOY_BUTTON_1);
ok &= pollJoy("hold over", 2970u, false, false, false);
gJoyOneButton[JL_JOYSTICK_0] = false;
gMockButtons[JL_JOY_BUTTON_1] = true;
ok &= pollJoy("two buttons", 3000u, true, true, true);
gMockButtons[JL_JOY_BUTTON_1] = false;
ok &= pollJoy("two buttons up", 3020u, false, false, false);
printf(" tap-or-hold: %s\n", ok ? "PASS" : "FAIL");
return ok;
}
// What ends jlWaitForAnyKey: a named key pressed, or a character typed, but
// not a key held from the start, nor a held key's own auto-repeat.
static bool runWait(void) {
static const WaitStepT kPunctuation[] = {
{ JL_KEY_NONE, JL_KEY_NONE, "" }, { JL_KEY_NONE, JL_KEY_NONE, "" }, { JL_KEY_NONE, JL_KEY_NONE, "," }
};
static const WaitStepT kRepeat[] = {
{ JL_KEY_SPACE, JL_KEY_NONE, "" }, { JL_KEY_SPACE, JL_KEY_NONE, " " }, { JL_KEY_SPACE, JL_KEY_NONE, " " },
{ JL_KEY_NONE, JL_KEY_NONE, "" }, { JL_KEY_NONE, JL_KEY_NONE, "." }
};
static const WaitStepT kShifted[] = {
{ JL_KEY_NONE, JL_KEY_NONE, "" }, { JL_KEY_LSHIFT, JL_KEY_NONE, "<" }
};
static const WaitStepT kNamed[] = {
{ JL_KEY_NONE, JL_KEY_NONE, "" }, { JL_KEY_NONE, JL_KEY_NONE, "" }, { JL_KEY_A, JL_KEY_NONE, "a" }
};
static const WaitStepT kHeld[] = {
{ JL_KEY_A, JL_KEY_NONE, "" }, { JL_KEY_A, JL_KEY_NONE, "a" }, { JL_KEY_A, JL_KEY_NONE, "a" },
{ JL_KEY_NONE, JL_KEY_NONE, "" }, { JL_KEY_A, JL_KEY_NONE, "a" }
};
bool ok = true;
ok &= waitEndsAt("wait: a punctuation key", kPunctuation, 3, 3);
ok &= waitEndsAt("wait: a held key's repeat", kRepeat, 5, 5);
ok &= waitEndsAt("wait: shifted punctuation", kShifted, 2, 2);
ok &= waitEndsAt("wait: a named key", kNamed, 3, 3);
ok &= waitEndsAt("wait: a key held from the start", kHeld, 5, 5);
return ok;
}
// Run jlWaitForAnyKey on script: it should return on poll expect (the
// first poll is 1), and the character queue be empty.
static bool waitEndsAt(const char *name, const WaitStepT *script, int steps, int expect) {
bool ok;
jlInputCharReset();
memset(gKeyState, 0, sizeof(gKeyState));
gWaitScript = script;
gWaitSteps = steps;
gWaitAt = 0;
gWaitOverrun = false;
jlWaitForAnyKey();
gWaitScript = NULL;
ok = !gWaitOverrun && gWaitAt == expect && jlInputGetChar() == -1;
printf(" %-34s %s (ended on poll %d%s, %d expected)\n", name, ok ? "PASS" : "FAIL", gWaitAt, gWaitOverrun ? ", past the script" : "", expect);
return ok;
}
int main(void) {
bool ok = true;
printf("inputHost: typed-character queue, joystick, and mouse scenarios\n");
ok &= runOrder();
ok &= runFilter();
ok &= runOverflow();
ok &= runInterleave();
ok &= runReset();
ok &= runPollRefill();
ok &= runTapOrHold();
ok &= runDeadZone();
ok &= runAxisScale();
ok &= runChord();
ok &= runSecondKey();
ok &= runMouse();
ok &= runWait();
if (!ok) {
printf("inputHost: FAIL\n");
return 1;
}
printf("inputHost: PASS\n");
return 0;
}