Files
keyrelay/tests/test_keystate.cpp
ducoterra 9bc8d5213c Initial import: keyrelay, a WiFi-controlled USB HID keyboard
Browser or terminal controller drives an ESP32-S3 that acts as a Logitech
G815 into a target machine running no software. Disarmed by default; arming
requires the passphrase, the board button, or the serial console, and every
fail-safe path cancels in-flight work.

- src/: firmware with pure, host-testable modules (keystate, proto, text,
  ws_codec, sha1), the WebSocket control server, and the served page
- tests/: host suites for the pure modules, relayctl parity, and page JS
- tools/: relayctl terminal controller, serial console, bootloader helper,
  and an evdev target watcher
- platformio.ini: bridge (USB HID) and console (no-HID) environments
- README.md: single source of truth for behaviour, protocol and safety

LLM-Generated: true
2026-11-08 00:20:33 -05:00

203 lines
5.9 KiB
C++

// 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 <cstring>
#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<uint8_t>(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");
}