// 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 #include #include #include #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; }