// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // serialHost.c - Host-cc harness for the shared serial gate/normalize layer // (src/core/serial.c). Built by scripts/check-serial.sh on the millisHost.c // pattern: compiled with -DJOEYLIB_PLATFORM_BLANK plus every -DJL_HAS_SERIAL_* // so port.h routes jlpSerial* to the in-memory mock HAL below instead of the // generic no-op. The mock is a loopback FIFO -- whatever the core writes is // readable back -- which lets the portable open/close/gate/normalize/read/ // write logic be tested end to end with no hardware and no cross toolchain. // // Fully deterministic (no time, no rand). Prints one line per check and a // final tally; exits nonzero if any check failed, so it doubles as a CI gate. #include #include #include #include "joey/serial.h" #include "port.h" #define MOCK_FIFO_SIZE 256u // A wait on a busy transmitter whose clock never runs ends at its poll limit. #define STALL_POLL_LIMIT 1000u // At a millisecond a poll, the wait reads the clock first at the 64th poll and // ends at the first read 100 ms or more after that: the 192nd. #define STALL_POLLS_TIMED 192u // ----- Mock HAL state (the loopback the core talks to) ----- static uint8_t gFifo[MOCK_FIFO_SIZE]; static uint16_t gFifoHead; static uint16_t gFifoTail; static bool gMockOpen; static jlSerialConfigT gMockLastCfg; // config the core handed the HAL static bool gMockOpenResult = true; // test can force open failure static bool gMockRts = true; // last RTS level the core handed the HAL static uint16_t gMockRtsCalls = 0u; static bool gMockDtr = true; // last DTR level the core handed the HAL static uint16_t gMockDtrCalls = 0u; static uint32_t gMockMs = 0u; // the clock jlSerialTxStalled reads // ----- Mock HAL (satisfies the jlpSerial* dispatch under -DJL_HAS_SERIAL_*) ----- uint32_t jlpMillisElapsed(void) { return gMockMs; } uint16_t jlpSerialAvailable(void) { return (uint16_t)(gFifoTail - gFifoHead); } void jlpSerialClose(void) { gMockOpen = false; } void jlpSerialFlush(void) { gFifoHead = gFifoTail; } bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) { (void)device; gMockLastCfg = *config; gFifoHead = 0u; gFifoTail = 0u; gMockOpen = gMockOpenResult; return gMockOpenResult; } void jlpSerialPoll(void) { // Loopback FIFO is filled directly by writes; nothing to pump. } uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) { uint16_t n; for (n = 0u; n < max && gFifoHead != gFifoTail; n++) { buf[n] = gFifo[gFifoHead % MOCK_FIFO_SIZE]; gFifoHead++; } return n; } void jlpSerialSetDtr(bool on) { gMockDtr = on; gMockDtrCalls++; } void jlpSerialSetRts(bool on) { gMockRts = on; gMockRtsCalls++; } uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) { uint16_t n; for (n = 0u; n < len; n++) { if ((uint16_t)(gFifoTail - gFifoHead) >= MOCK_FIFO_SIZE) { break; // FIFO full } gFifo[gFifoTail % MOCK_FIFO_SIZE] = buf[n]; gFifoTail++; } return n; } // ----- Test harness ----- static int gPass = 0; static int gFail = 0; static void check(const char *name, bool ok) { if (ok) { gPass++; printf(" ok %s\n", name); } else { gFail++; printf(" FAIL %s\n", name); } } // Registered before the first jlSerialOpen, so it runs after the serial add-on's // own atexit handler (last registered, first run): the port main leaves open // must be closed by then. static void checkClosedAtExit(void) { if (gMockOpen) { printf(" FAIL atexit: a port left open was not closed\n"); fflush(stdout); _Exit(1); } printf(" ok atexit: a port left open was closed\n"); } static jlSerialConfigT makeConfig(uint32_t baud, uint8_t dataBits, uint8_t stopBits) { jlSerialConfigT cfg; cfg.baud = baud; cfg.dataBits = dataBits; cfg.stopBits = stopBits; cfg.parity = JL_SERIAL_PARITY_NONE; cfg.flow = JL_SERIAL_FLOW_NONE; cfg.unit = 0u; return cfg; } int main(void) { jlSerialConfigT cfg; SerialTxWaitT wait; uint8_t buf[16]; uint32_t polls; int16_t b; printf("serial-host: core gate/normalize/loopback\n"); atexit(checkClosedAtExit); // 1. Every call is inert before a successful open. check("closed: available == 0", jlSerialAvailable() == 0u); check("closed: read == 0", jlSerialRead(buf, sizeof(buf)) == 0u); check("closed: write == 0", jlSerialWrite((const uint8_t *)"x", 1u) == 0u); check("closed: readByte == -1", jlSerialReadByte() == -1); check("closed: writeByte == false", jlSerialWriteByte('x') == false); jlSerialSetRts(false); check("closed: setRts never reaches the HAL", gMockRtsCalls == 0u); jlSerialSetDtr(false); check("closed: setDtr never reaches the HAL", gMockDtrCalls == 0u); // 2. NULL config and out-of-range framing are rejected, and a rejected // open must leave the port closed (a later valid open still works). check("open: NULL config rejected", jlSerialOpen(JL_SERIAL_DEFAULT, NULL) == false); cfg = makeConfig(9600u, 9u, 1u); check("open: dataBits 9 rejected", jlSerialOpen(JL_SERIAL_DEFAULT, &cfg) == false); cfg = makeConfig(9600u, 4u, 1u); check("open: dataBits 4 rejected", jlSerialOpen(JL_SERIAL_DEFAULT, &cfg) == false); cfg = makeConfig(9600u, 8u, 3u); check("open: stopBits 3 rejected", jlSerialOpen(JL_SERIAL_DEFAULT, &cfg) == false); cfg = makeConfig(9600u, 8u, 1u); cfg.parity = (jlSerialParityE)3; check("open: parity 3 rejected", jlSerialOpen(JL_SERIAL_DEFAULT, &cfg) == false); cfg = makeConfig(9600u, 8u, 1u); cfg.flow = (jlSerialFlowE)3; check("open: flow 3 rejected", jlSerialOpen(JL_SERIAL_DEFAULT, &cfg) == false); check("open: still closed after rejects", jlSerialAvailable() == 0u); // 3. Zero fields normalize to library defaults before reaching the HAL. cfg = makeConfig(0u, 0u, 0u); check("open: zero-field config accepted", jlSerialOpen(JL_SERIAL_DEFAULT, &cfg) == true); jlSerialSetRts(false); check("setRts: false reaches the HAL", gMockRtsCalls == 1u && !gMockRts); jlSerialSetRts(true); check("setRts: true reaches the HAL", gMockRtsCalls == 2u && gMockRts); jlSerialSetDtr(false); check("setDtr: false reaches the HAL", gMockDtrCalls == 1u && !gMockDtr); jlSerialSetDtr(true); check("setDtr: true reaches the HAL", gMockDtrCalls == 2u && gMockDtr); check("setDtr: RTS untouched", gMockRtsCalls == 2u && gMockRts); check("normalize: baud -> 9600", gMockLastCfg.baud == 9600u); check("normalize: dataBits -> 8", gMockLastCfg.dataBits == 8u); check("normalize: stopBits -> 1", gMockLastCfg.stopBits == 1u); // 4. A second open while open is refused. cfg = makeConfig(19200u, 7u, 2u); check("open: double-open refused", jlSerialOpen(JL_SERIAL_MODEM, &cfg) == false); // 5. Loopback: bytes written come back through read. check("write: 3 bytes accepted", jlSerialWrite((const uint8_t *)"ABC", 3u) == 3u); jlSerialPoll(); check("loopback: available == 3", jlSerialAvailable() == 3u); memset(buf, 0, sizeof(buf)); check("loopback: read 3 bytes", jlSerialRead(buf, sizeof(buf)) == 3u); check("loopback: payload == ABC", memcmp(buf, "ABC", 3) == 0); check("loopback: drained to 0", jlSerialAvailable() == 0u); // 6. Byte-at-a-time round trip. check("writeByte: Z accepted", jlSerialWriteByte('Z') == true); b = jlSerialReadByte(); check("readByte: got Z", b == (int16_t)'Z'); check("readByte: empty -> -1", jlSerialReadByte() == -1); // 7. Flush discards buffered input. jlSerialWrite((const uint8_t *)"junk", 4u); jlSerialFlush(); check("flush: available == 0", jlSerialAvailable() == 0u); // 8. Close re-gates the API; a fresh open works afterward. A hang-up // (DTR dropped before the close) is the HAL's to keep: the core must // not touch DTR on the way out. jlSerialSetDtr(false); jlSerialClose(); check("close: core leaves DTR alone", gMockDtrCalls == 3u && !gMockDtr); jlSerialSetDtr(true); check("closed again: setDtr never reaches the HAL", gMockDtrCalls == 3u && !gMockDtr); check("closed again: write == 0", jlSerialWrite((const uint8_t *)"x", 1u) == 0u); cfg = makeConfig(2400u, 8u, 1u); check("reopen after close", jlSerialOpen(JL_SERIAL_PRINTER, &cfg) == true); check("reopen: HAL saw baud 2400", gMockLastCfg.baud == 2400u); jlSerialClose(); // 9. A HAL that fails to open leaves the port closed. gMockOpenResult = false; cfg = makeConfig(9600u, 8u, 1u); check("open: HAL failure propagated", jlSerialOpen(JL_SERIAL_DEFAULT, &cfg) == false); check("open: closed after HAL failure", jlSerialWriteByte('x') == false); gMockOpenResult = true; // 10. A busy transmitter's wait (jlSerialTxStalled): a clock that does not // run ends it at the poll limit, a running one 100 ms on. gMockMs = 0u; jlSerialTxWaitStart(&wait, STALL_POLL_LIMIT); for (polls = 1u; !jlSerialTxStalled(&wait); polls++) { } check("tx stall: a stopped clock ends the wait at the poll limit", polls == STALL_POLL_LIMIT); jlSerialTxWaitStart(&wait, UINT32_MAX); for (polls = 1u; !jlSerialTxStalled(&wait); polls++) { gMockMs++; } check("tx stall: a running clock ends it 100 ms after its first read", polls == STALL_POLLS_TIMED); // 11. A port left open is closed at exit (checked by checkClosedAtExit). cfg = makeConfig(9600u, 8u, 1u); check("left open for the exit check", jlSerialOpen(JL_SERIAL_DEFAULT, &cfg) == true); printf("serial-host: %d passed, %d failed\n", gPass, gFail); return (gFail == 0) ? 0 : 1; }