joeylib2/tests/host/serialHost.c

256 lines
8.9 KiB
C

// 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "joey/serial.h"
#include "port.h"
#define MOCK_FIFO_SIZE 256u
// ----- 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;
// ----- Mock HAL (satisfies the jlpSerial* dispatch under -DJL_HAS_SERIAL_*) -----
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;
uint8_t buf[16];
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 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;
}