// tests for the key-state tracker: tests/test_keystate.cpp // // The properties under test are the ones that decide whether the target machine // ends up with a key stuck down: idempotence, transition preservation, rollover, // and that clear() really clears. #include #include "keystate.h" #include "test.h" using kr::KeyboardReport; using kr::KeyboardState; namespace { // A report as 8 hex-byte triples, for readable assertions. void hexreport(const KeyboardReport &r, char out[32]) { static const char kHex[] = "0123456789abcdef"; const uint8_t bytes[8] = {r.modifiers, r.reserved, r.keys[0], r.keys[1], r.keys[2], r.keys[3], r.keys[4], r.keys[5]}; for (int i = 0; i < 8; i++) { out[3 * i] = kHex[bytes[i] >> 4]; out[3 * i + 1] = kHex[bytes[i] & 0xF]; out[3 * i + 2] = i == 7 ? '\0' : ' '; } out[23] = '\0'; } } // namespace int main() { char h1[32], h2[32]; CASE("empty state is all zeroes"); { KeyboardState s; CHECK(!s.dirty()); hexreport(s.report(), h1); CHECK_STR(h1, "00 00 00 00 00 00 00 00"); } CASE("reserved byte is always zero"); { KeyboardState s; const uint8_t keys[] = {kr::kUsageA}; s.apply(kr::kModLctrl | kr::kModRgui, keys, 1); CHECK_EQ(s.report().reserved, 0); CHECK_EQ(s.report().modifiers, kr::kModLctrl | kr::kModRgui); } CASE("apply is idempotent -- same state twice dirties nothing"); { KeyboardState s; const uint8_t keys[] = {kr::kUsageA, kr::kUsageZ}; s.apply(kr::kModLshift, keys, 2); CHECK(s.dirty()); s.ackDirty(); s.apply(kr::kModLshift, keys, 2); CHECK(!s.dirty()); s.apply(kr::kModLshift, keys, 2); CHECK(!s.dirty()); } CASE("duplicates collapse, order is preserved"); { KeyboardState s; const uint8_t keys[] = {kr::kUsageA, kr::kUsageA, kr::kUsageZ}; s.apply(0, keys, 3); CHECK_EQ(s.held(), 2u); hexreport(s.report(), h1); CHECK_STR(h1, "00 00 04 1d 00 00 00 00"); CHECK(s.isDown(kr::kUsageA)); CHECK(s.isDown(kr::kUsageZ)); } CASE("usage 0x00 is not a key"); { KeyboardState s; const uint8_t keys[] = {0x00, kr::kUsageEnter, 0x00}; s.apply(0, keys, 3); CHECK_EQ(s.held(), 1u); hexreport(s.report(), h1); CHECK_STR(h1, "00 00 28 00 00 00 00 00"); } CASE("a distinct transition always produces a distinct report"); { // A fast tap: press then release. Both states have to be reportable, or the // keypress vanishes and the target never sees the character. KeyboardState s; const uint8_t down[] = {kr::kUsageA}; s.apply(0, down, 1); CHECK(s.dirty()); s.ackDirty(); const uint8_t none = 0; s.apply(0, &none, 0); CHECK(s.dirty()); hexreport(s.report(), h1); CHECK_STR(h1, "00 00 00 00 00 00 00 00"); } CASE("modifiers alone are a reportable change"); { KeyboardState s; s.apply(kr::kModLalt, nullptr, 0); CHECK(s.dirty()); CHECK_EQ(s.modifiers(), kr::kModLalt); s.apply(0, nullptr, 0); CHECK(s.dirty()); } CASE("six keys is exactly the limit"); { KeyboardState s; const uint8_t keys[] = {0x04, 0x05, 0x06, 0x07, 0x08, 0x09}; s.apply(0, keys, 6); CHECK(!s.rollover()); CHECK_EQ(s.held(), 6u); hexreport(s.report(), h1); CHECK_STR(h1, "00 00 04 05 06 07 08 09"); } CASE("a seventh key reports ErrorRollOver in every slot"); { KeyboardState s; const uint8_t keys[] = {0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A}; s.apply(0, keys, 7); CHECK(s.rollover()); hexreport(s.report(), h1); CHECK_STR(h1, "00 00 01 01 01 01 01 01"); // ... and releasing back under the limit reports the real keys again, so the // host is never left with a phantom key held down. const uint8_t six[] = {0x04, 0x05, 0x06, 0x07, 0x08, 0x09}; s.apply(0, six, 6); CHECK(!s.rollover()); hexreport(s.report(), h2); CHECK_STR(h2, "00 00 04 05 06 07 08 09"); } CASE("more keys than we track still cannot wedge the state"); { KeyboardState s; uint8_t keys[40]; for (size_t i = 0; i < sizeof(keys); i++) keys[i] = static_cast(0x04 + (i % 0x1A)); s.apply(0xFF, keys, sizeof(keys)); CHECK(s.rollover()); CHECK(s.held() <= kr::kMaxTrackedKeys); s.clear(); CHECK_EQ(s.held(), 0u); CHECK_EQ(s.modifiers(), 0); } CASE("clear releases everything; a repeat clear adds nothing"); { KeyboardState s; const uint8_t keys[] = {kr::kUsageA, kr::kUsageSpace}; s.apply(kr::kModLctrl | kr::kModLshift, keys, 2); s.ackDirty(); s.clear(); CHECK(s.dirty()); // the all-keys-up report still has to go out hexreport(s.report(), h1); CHECK_STR(h1, "00 00 00 00 00 00 00 00"); s.ackDirty(); s.clear(); CHECK(!s.dirty()); // nothing changed, so no pointless second report s.apply(kr::kModLctrl, nullptr, 0); CHECK(s.dirty()); // ... and a real change after a clear is still seen } CASE("makeReport builds the Ctrl+Alt+Del chord byte-exactly"); { const uint8_t del = kr::kUsageDelForward; const KeyboardReport r = kr::makeReport(kr::kModLctrl | kr::kModLalt, &del, 1); hexreport(r, h1); // 05 = LCtrl|LAlt, 4C = "Delete Forward". This is the exact sequence Windows // accepts as the secure attention sequence and logind sees as CAD. CHECK_STR(h1, "05 00 4c 00 00 00 00 00"); } CASE("makeReport rolls over on more than six keys"); { const uint8_t keys[] = {0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A}; const KeyboardReport r = kr::makeReport(0, keys, 7); CHECK_EQ(r.keys[0], kr::kUsageErrorRollover); CHECK_EQ(r.keys[5], kr::kUsageErrorRollover); } CASE("reportsEqual distinguishes modifiers and slots"); { KeyboardReport a = {0, 0, {4, 5, 0, 0, 0, 0}}; KeyboardReport b = a; CHECK(kr::reportsEqual(a, b)); b.modifiers = kr::kModLshift; CHECK(!kr::reportsEqual(a, b)); b = a; b.keys[2] = 9; CHECK(!kr::reportsEqual(a, b)); } return report("keystate"); }