From 58cf2424e46973dcee19f0538edc39eb35af310a Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 10 Sep 2026 15:13:55 +0200 Subject: [PATCH] test: cover keyboardTest --- src/keyboardTest.c | 216 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 src/keyboardTest.c diff --git a/src/keyboardTest.c b/src/keyboardTest.c new file mode 100644 index 0000000..065bb91 --- /dev/null +++ b/src/keyboardTest.c @@ -0,0 +1,216 @@ +#include "ktest.h" +#include "keyboard.h" +#include "string.h" + +/* + * Coverage for keyboard.c's per-reader console input queues (keyboard.h) -- + * the fan-out/blocking-channel machinery added this wave to close the gap + * called out at the end of the previous one: a single global keyboard + * buffer shared by every consumer, with no per-process queueing and no + * scheduler-visible blocking. What is pure state machine (open/close, + * scancode decoding, fan-out to N independent queues, exhaustion/reuse) + * lives here, driven directly through keyboardHandleScancode -- the same + * seam the live PS/2 IRQ handler (keyboardHandler) calls into, just fed a + * synthetic scancode instead of a real inb() -- exactly the way + * pipeShareTest.c drives pipe.c's state machine without a live scheduler. + * The genuine cross-process park/wake handshake (real blocking read() on + * fd 0 from two independent ring-3 processes, woken by a real keystroke) + * only runs for real at boot (kmain.c's consolereader1/consolereader2 demo, + * needles "consolereader1: got byte" / "consolereader2: got byte" in + * tools/ci-test.sh), matching how every other blocking-I/O subsystem in + * this kernel splits its test coverage. + * + * Scancode reference (set 1, US QWERTY, keyboard.c's own tables): 0x25 is + * 'k'/'K', 0x2A/0xAA are left-shift make/break. + */ +#define scancodeK 0x25 +#define scancodeLeftShiftMake 0x2A +#define scancodeLeftShiftBreak 0xAA + +KTEST(keyboardOpenReaderReturnsDistinctHandlesAndEmptyReadWouldBlock) +{ + int a = keyboardOpenReader(); + int b = keyboardOpenReader(); + KT_NE(t, a, consoleReaderInvalid); + KT_NE(t, b, consoleReaderInvalid); + KT_NE(t, a, b); + + char buffer[4]; + KT_EQ(t, keyboardReaderRead(a, buffer, sizeof(buffer)), -1); + KT_EQ(t, keyboardReaderRead(b, buffer, sizeof(buffer)), -1); + + keyboardCloseReader(a); + keyboardCloseReader(b); +} + +KTEST(keyboardReaderReadRejectsInvalidHandlesAndZeroLength) +{ + KT_EQ(t, keyboardReaderRead(consoleReaderInvalid, nullptr, 4), -1); + KT_EQ(t, keyboardReaderRead(9999, nullptr, 4), -1); + + int h = keyboardOpenReader(); + KT_NE(t, h, consoleReaderInvalid); + KT_EQ(t, keyboardReaderRead(h, nullptr, 0), 0); + keyboardCloseReader(h); + + /* Once closed, the handle is dead -- not silently reusable by the + caller who just gave it up. */ + KT_EQ(t, keyboardReaderRead(h, nullptr, 0), -1); +} + +KTEST(keyboardHandleScancodeBroadcastsToEveryOpenReaderIndependently) +{ + int a = keyboardOpenReader(); + int b = keyboardOpenReader(); + KT_NE(t, a, consoleReaderInvalid); + KT_NE(t, b, consoleReaderInvalid); + + keyboardHandleScancode(scancodeK); + + char ca = 0, cb = 0; + KT_EQ(t, keyboardReaderRead(a, &ca, 1), 1); + KT_EQ(t, keyboardReaderRead(b, &cb, 1), 1); + KT_EQ(t, ca, 'k'); + KT_EQ(t, cb, 'k'); + + /* Each reader's copy is independent: draining a's queue must not touch + b's, and vice versa -- proven above by both still having their own + byte to read rather than one starving the other. Both are empty now. */ + KT_EQ(t, keyboardReaderRead(a, &ca, 1), -1); + KT_EQ(t, keyboardReaderRead(b, &cb, 1), -1); + + keyboardCloseReader(a); + keyboardCloseReader(b); +} + +KTEST(keyboardHandleScancodeAppliesShiftMakeAndBreak) +{ + int h = keyboardOpenReader(); + KT_NE(t, h, consoleReaderInvalid); + + keyboardHandleScancode(scancodeK); + char c = 0; + KT_EQ(t, keyboardReaderRead(h, &c, 1), 1); + KT_EQ(t, c, 'k'); + + keyboardHandleScancode(scancodeLeftShiftMake); + keyboardHandleScancode(scancodeK); + KT_EQ(t, keyboardReaderRead(h, &c, 1), 1); + KT_EQ(t, c, 'K'); + + keyboardHandleScancode(scancodeLeftShiftBreak); + keyboardHandleScancode(scancodeK); + KT_EQ(t, keyboardReaderRead(h, &c, 1), 1); + KT_EQ(t, c, 'k'); + + keyboardCloseReader(h); +} + +KTEST(keyboardHandleScancodeIgnoresKeyReleaseAndUnmappedCodes) +{ + int h = keyboardOpenReader(); + KT_NE(t, h, consoleReaderInvalid); + + /* Key-release (high bit set) for 'k' must queue nothing. */ + keyboardHandleScancode(scancodeK | 0x80); + char c; + KT_EQ(t, keyboardReaderRead(h, &c, 1), -1); + + /* Scancode 0 maps to 0 (no character) in both tables. */ + keyboardHandleScancode(0); + KT_EQ(t, keyboardReaderRead(h, &c, 1), -1); + + keyboardCloseReader(h); +} + +KTEST(keyboardReaderReadDeliversQueuedBytesInFifoOrderAcrossPartialReads) +{ + int h = keyboardOpenReader(); + KT_NE(t, h, consoleReaderInvalid); + + /* "kkk" via three separate scancode events, like three real keystrokes. */ + keyboardHandleScancode(scancodeK); + keyboardHandleScancode(scancodeK); + keyboardHandleScancode(scancodeK); + + char buffer[8]; + memset(buffer, 0, sizeof(buffer)); + + /* Ask for only 2 of the 3 queued bytes -- must not block or fail, and + must leave the third for the next call, same partial-transfer + contract ring.h's ringRead already documents. */ + int64 got = keyboardReaderRead(h, buffer, 2); + KT_EQ(t, got, 2); + KT_EQ(t, buffer[0], 'k'); + KT_EQ(t, buffer[1], 'k'); + + got = keyboardReaderRead(h, buffer, 8); + KT_EQ(t, got, 1); + KT_EQ(t, buffer[0], 'k'); + + KT_EQ(t, keyboardReaderRead(h, buffer, 1), -1); + + keyboardCloseReader(h); +} + +KTEST(keyboardReaderChannelIsNullForInvalidAndDistinctPerReader) +{ + KT_NULL(t, keyboardReaderChannel(consoleReaderInvalid)); + KT_NULL(t, keyboardReaderChannel(9999)); + + int a = keyboardOpenReader(); + int b = keyboardOpenReader(); + KT_NE(t, a, consoleReaderInvalid); + KT_NE(t, b, consoleReaderInvalid); + + const void* channelA = keyboardReaderChannel(a); + const void* channelB = keyboardReaderChannel(b); + KT_NOTNULL(t, channelA); + KT_NOTNULL(t, channelB); + KT_NE(t, (int64)(uint64)channelA, (int64)(uint64)channelB); + + /* Stable across repeated calls -- schedBlockCurrent/schedWakeChannel + depend on the same reader always producing the same identity. */ + KT_EQP(t, keyboardReaderChannel(a), channelA); + + /* A closed reader's channel must go back to null immediately -- a + syscall handler using this as its retry condition must never park a + thread on a channel nobody can ever wake again. */ + keyboardCloseReader(a); + KT_NULL(t, keyboardReaderChannel(a)); + + keyboardCloseReader(b); +} + +KTEST(keyboardOpenReaderExhaustionThenCloseFreesSlotForReuse) +{ + /* Does not assume the table starts fully empty -- by the time ktests + run, schedInit() has already given the idle thread (id 0) its own fd + 0 reader (vfs.c's vfsFdInit), same convention vfsFifoTest.c's own + pipe-table exhaustion test uses for the shared pipes[] table. */ + int handles[maxConsoleReaders]; + int madeCount = 0; + while (madeCount < maxConsoleReaders) { + int h = keyboardOpenReader(); + if (h == consoleReaderInvalid) + break; + handles[madeCount++] = h; + } + KT_GT(t, madeCount, 0); + + /* Table is now genuinely full: one more must fail cleanly. */ + KT_EQ(t, keyboardOpenReader(), consoleReaderInvalid); + + keyboardCloseReader(handles[0]); + int reused = keyboardOpenReader(); + KT_NE(t, reused, consoleReaderInvalid); + KT_EQ(t, reused, handles[0]); + + /* Leave the shared reader table exactly as free as it was found, so + later boot-time console users (the shell, and the + consolereader1/consolereader2 live demo) are never starved by this + test having run. */ + keyboardCloseReader(reused); + for (int i = 1; i < madeCount; i++) + keyboardCloseReader(handles[i]); +}