// tests for the emit side of the bridge: tests/test_output.cpp // // This suite exists mainly for one property, and it is the property the project got // wrong once: releasing everything has to TELL the target. Clearing the tracker and the // queue is not a release, because the last report the target received still says keys are // down -- and disarm is exactly the moment the controller stops talking, so no later // frame is coming to carry it. With a mouse added, the same shape appears a second time: // a button held at disarm time is a drag nobody is holding. #include #include "config.h" #include "keystate.h" #include "mouse.h" #include "output.h" #include "test.h" using kr::KeyboardReport; using kr::mouse::Frame; using kr::mouse::kBtnLeft; using kr::mouse::kBtnRight; using kr::mouse::MouseReport; using kr::OutputStage; namespace { bool keysClear(const KeyboardReport &r) { return r.modifiers == 0 && r.reserved == 0 && r.keys[0] == 0 && r.keys[1] == 0 && r.keys[2] == 0 && r.keys[3] == 0 && r.keys[4] == 0 && r.keys[5] == 0; } bool mouseClear(const MouseReport &r) { return r.buttons == 0 && r.dx == 0 && r.dy == 0 && r.wheel == 0 && r.pan == 0; } // Drain a queue into a flat list so "what would the target have received" is a fact a // test can count, not an inference about the firmware's loop. size_t drainKeyboard(OutputStage &s, KeyboardReport *out, size_t cap) { size_t n = 0; while (!s.keyQ.empty() && n < cap) { out[n++] = s.keyQ.front(); s.keyQ.pop(); } return n; } size_t drainMouse(OutputStage &s, MouseReport *out, size_t cap) { size_t n = 0; while (!s.mouseQ.empty() && n < cap) { out[n++] = s.mouseQ.front(); s.mouseQ.pop(); } return n; } } // namespace int main() { CASE("a queue keeps distinct transitions and collapses identical ones"); { OutputStage s; s.keys.apply(0, nullptr, 0); CHECK(s.pushKeys() == OutputStage::Pushed::Nothing); // nothing changed yet const uint8_t a = 0x04; s.keys.apply(0, &a, 1); CHECK(s.pushKeys() == OutputStage::Pushed::Ok); CHECK(s.pushKeys() == OutputStage::Pushed::Nothing); // restating is a no-op // A fast press and release is two reports. Collapsing those is how a tap vanishes, // so only *identical* neighbours may collapse. s.keys.apply(0, nullptr, 0); CHECK(s.pushKeys() == OutputStage::Pushed::Ok); KeyboardReport got[8]; CHECK_EQ(drainKeyboard(s, got, 8), 2); CHECK_EQ(got[0].keys[0], 0x04); CHECK(keysClear(got[1])); } CASE("release with keys held queues the all-up report <-- the bug this guards"); { OutputStage s; const uint8_t k = 0x04; s.keys.apply(kr::kModLshift, &k, 1); CHECK(s.pushKeys() == OutputStage::Pushed::Ok); CHECK(s.releaseAll(true, true)); KeyboardReport got[8]; const size_t n = drainKeyboard(s, got, 8); CHECK_EQ(n, 1); // exactly one report: the release CHECK(keysClear(got[0])); // and it says nothing is pressed CHECK_EQ(s.keys.held(), 0); } CASE("release drops presses that had not gone out yet"); { OutputStage s; const uint8_t k = 0x04; s.keys.apply(0, &k, 1); s.pushKeys(); // queued, never sent s.releaseAll(true, true); KeyboardReport got[8]; CHECK_EQ(drainKeyboard(s, got, 8), 1); CHECK(keysClear(got[0])); // the press is gone; only the release is left } CASE("release with buttons held queues the all-up mouse report"); { OutputStage s; CHECK(s.pushMouse(Frame{kBtnLeft, 0, 0, 0, 0}) == OutputStage::Pushed::Ok); CHECK(s.releaseAll(true, true)); MouseReport got[8]; CHECK_EQ(drainMouse(s, got, 8), 1); CHECK(mouseClear(got[0])); CHECK_EQ(s.mouseState.buttons(), 0); } CASE("a mouse the target cannot receive is never queued, even by a fail-safe"); { OutputStage s; // The boot-protocol case. A 5-byte report reaching a target that is reading an // 8-byte boot keyboard report is not "the mouse is unavailable", it is keystrokes // nobody pressed -- so the release must not sneak one out on the way past. s.pushMouse(Frame{kBtnLeft, 0, 0, 0, 0}); CHECK_EQ(s.mouseQ.count(), 1u); s.releaseAll(true, false); CHECK_EQ(s.mouseQ.count(), 0u); KeyboardReport got[8]; CHECK_EQ(drainKeyboard(s, got, 8), 1); // the keyboard is unaffected ... CHECK(keysClear(got[0])); // ... and the button byte is cleared regardless, because what the board believes must // not outlive the fail-safe just because the target could not be told. CHECK_EQ(s.mouseState.buttons(), 0); } CASE("release covers both devices in one call"); { OutputStage s; const uint8_t k = 0x04; s.keys.apply(0, &k, 1); s.pushKeys(); s.pushMouse(Frame{kBtnLeft | kBtnRight, 3, 4, 0, 0}); CHECK_EQ(s.keyQ.count(), 1u); CHECK_EQ(s.mouseQ.count(), 1u); CHECK(s.releaseAll(true, true)); KeyboardReport kr8[8]; MouseReport mr8[8]; CHECK_EQ(drainKeyboard(s, kr8, 8), 1); CHECK_EQ(drainMouse(s, mr8, 8), 1); CHECK(keysClear(kr8[0])); CHECK(mouseClear(mr8[0])); } CASE("release with nothing held still says so, and that is the trade"); { OutputStage s; // The reports are unconditional on purpose: a panic path must not hinge on the // tracker's bookkeeping being right, and "no buttons, no movement" is what a real // mouse sends every time it presses a button without moving. Two extra reports on a // 1 ms bus, and no branch in the fail-safe. CHECK(s.releaseAll(true, true)); KeyboardReport kr8[8]; MouseReport mr8[8]; CHECK_EQ(drainKeyboard(s, kr8, 8), 1); CHECK_EQ(drainMouse(s, mr8, 8), 1); CHECK(keysClear(kr8[0])); CHECK(mouseClear(mr8[0])); CHECK_EQ(s.keys.held(), 0); CHECK_EQ(s.mouseState.buttons(), 0); } CASE("release with no target attached queues nothing to be counted as dropped"); { OutputStage s; const uint8_t k = 0x04; s.keys.apply(0, &k, 1); s.pushKeys(); s.pushMouse(Frame{kBtnLeft, 0, 0, 0, 0}); // Neither device reachable: the drain counts whatever is left as dropped, so the // fail-safe must not add release reports nobody can receive. The state is still // cleared, which is the part that matters for whatever comes next. CHECK(!s.releaseAll(false, false)); CHECK_EQ(s.keyQ.count(), 0u); CHECK_EQ(s.mouseQ.count(), 0u); CHECK_EQ(s.keys.held(), 0); CHECK_EQ(s.mouseState.buttons(), 0); } CASE("release cancels a pasted run -- clearing means stop, not pause"); { OutputStage s; const uint8_t text[] = {'h', 'e', 'l', 'l', 'o'}; CHECK(s.typer.feed(text, sizeof(text)).result == kr::text::Result::Ok); s.typer.start(1000, 12); CHECK(s.typer.active()); CHECK_EQ(s.typer.pending(), 5u); s.releaseAll(true, true); CHECK(!s.typer.active()); CHECK_EQ(s.typer.pending(), 0u); } CASE("release ends a chord mid-hold"); { OutputStage s; s.chord.active = true; s.chord.releaseAtMs = 999999; s.releaseAll(true, true); CHECK(!s.chord.active); } CASE("an idle mouse frame costs nothing and is not queued"); { OutputStage s; CHECK(s.pushMouse(Frame{0, 0, 0, 0, 0}) == OutputStage::Pushed::Nothing); CHECK_EQ(s.mouseQ.count(), 0u); CHECK(s.pushMouse(Frame{kBtnLeft, 0, 0, 0, 0}) == OutputStage::Pushed::Ok); CHECK(s.pushMouse(Frame{kBtnLeft, 0, 0, 0, 0}) == OutputStage::Pushed::Nothing); CHECK_EQ(s.mouseQ.count(), 1u); } CASE("a full queue is refused, and says so"); { OutputStage s; // Every report differs, so nothing collapses and the queue really fills. for (size_t i = 0; i < kr::kMouseReportQueueDepth; i++) { CHECK(s.pushMouse(Frame{0, (int8_t)(i + 1), 0, 0, 0}) == OutputStage::Pushed::Ok); } CHECK_EQ(s.mouseQ.count(), (unsigned)kr::kMouseReportQueueDepth); // Not "drop the movement, it is only a movement": the frame that does not fit may be // the one releasing a held button, and that is a stuck-mouse failure. CHECK(s.pushMouse(Frame{kBtnLeft, 0, 0, 0, 0}) == OutputStage::Pushed::Overflow); const uint8_t lo = 0x04, hi = 0x05; for (size_t i = 0; i < kr::kReportQueueDepth; i++) { const uint8_t k = (i & 1) ? lo : hi; // alternating: no two neighbours equal CHECK(s.pushKeyboardReport(kr::makeReport(0, &k, 1)) == OutputStage::Pushed::Ok); } CHECK_EQ(s.keyQ.count(), (unsigned)kr::kReportQueueDepth); const uint8_t other = 0x06; CHECK(s.pushKeyboardReport(kr::makeReport(0, &other, 1)) == OutputStage::Pushed::Overflow); // Overflow leaves the queue untouched: the caller's response is releaseEverything(), // and half-dropping a state transition is the thing that must not happen. CHECK_EQ(s.keyQ.count(), (unsigned)kr::kReportQueueDepth); CHECK(s.releaseAll(true, true)); CHECK_EQ(s.keyQ.count(), 1u); // ...and the release still gets out CHECK_EQ(s.mouseQ.count(), 1u); } CASE("queue depths are the ones config.h declares"); { OutputStage s; CHECK_EQ(s.keyQ.capacity(), (unsigned)kr::kReportQueueDepth); CHECK_EQ(s.mouseQ.capacity(), (unsigned)kr::kMouseReportQueueDepth); CHECK(kr::kMouseReportQueueDepth >= 8); // a flick is tens of reports CHECK(kr::kMouseReportQueueDepth <= 64); // and it is RAM on an 8 MB board } CASE("order is preserved across a movement burst under a held button"); { OutputStage s; s.pushMouse(Frame{kBtnLeft, 0, 0, 0, 0}); s.pushMouse(Frame{kBtnLeft, 10, 0, 0, 0}); s.pushMouse(Frame{kBtnLeft, 10, 0, 0, 0}); // identical neighbour: collapses s.pushMouse(Frame{kBtnLeft, 7, 0, 0, 0}); s.pushMouse(Frame{0, 0, 0, 0, 0}); MouseReport got[8]; CHECK_EQ(drainMouse(s, got, 8), 4); // 5 frames in, one identical pair out CHECK_EQ(got[0].buttons, kBtnLeft); CHECK_EQ(got[0].dx, 0); CHECK_EQ(got[1].dx, 10); CHECK_EQ(got[2].dx, 7); // the repeated +10 never arrived twice CHECK_EQ(got[2].buttons, kBtnLeft); CHECK(mouseClear(got[3])); // and the release is last } return report("output"); }