test: kernel host tests -- the harness, and gpt.c tested on the host

Captain Bob, 2026-11-12: "Build an appropriate test harness! Love the idea."
The kernel had no tests below the boots, so defects QEMU cannot provoke could
not be tested first. Plan: docs/v4.0.0/plans/2026-11-12-kernel-host-tests.md.

kernel/tests/: one host program per kernel source, built with that source as
it is, the kernel's flags (-std=c99 -Wall -Wextra -Werror), and fakes --
the console captured line by line, a deterministic RNG that can fail, and a RAM
disk whose reads, writes or flushes fail at chosen 1 KiB blocks.
make -C kernel/tests test runs them; SRC_<test>= runs a test against other
code, which is how a test is seen to fail against the code before a fix.

test_gpt.c, 12 checks: a table written and read back; 16, 17 and 18 used
entries against max 16 and an entry out of range after the 16th (#180); the
RNG, the first write, a later write and a flush failing (#168). Against gpt.c
before #168 and #180: 4 failures, each defect caught. Now: 0. Both runs in
logs/.../kernel-tests/. Not yet a protocol stage: to rule (the plan, step 5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RzZgrf7V7Zk2iczBXDRbm
This commit is contained in:
Claude
2026-11-12 17:57:04 +00:00
parent 8a0dab7f48
commit 37b1527279
7 changed files with 368 additions and 0 deletions
@@ -0,0 +1,37 @@
# Kernel host tests -- plan and progress (kept current, one line per increment)
Captain Bob, 2026-11-12: "Build an appropriate test harness! Love the idea." -- for the kernel's own
C code, so that a defect QEMU cannot provoke (a USB descriptor no emulated device sends, a write that
fails half-way, a stop that spins) is still tested first and seen to fail. v4 has had this since step
1 (`make -C v4 test`); the kernel had none.
## The design (my calls, open to a ruling)
- **`kernel/tests/`**, one program per kernel source under test: `test_<unit>.c` compiled on the
host with the kernel's own source file, `-std=c99 -Wall -Wextra -Werror`, and nothing else of the
kernel. `make -C kernel/tests test` builds and runs them all; each prints `N checks, M failures`
as v4's do, and the run fails if any does.
- **Fakes, not mocks** (`kernel/tests/fakes.[ch]`): the console captured line by line (so a test
reads what the kernel said), a deterministic RNG, and a RAM disk whose reads or writes fail at the
1 KiB blocks a test chooses -- the disk faults QEMU's blkdebug gives the boots, here per block and
without a boot.
- **Code is moved only to be testable, never changed in the move.** Where a unit is buried in a
larger function (the xHCI configuration-descriptor walk inside the event loop), it is moved out
into a function of its own in one commit, the boots and the tests unchanged, and then tested.
- **Not a substitute for acceptance.** The boots stay the only acceptance of the kernel; these run
beside them. Whether they become a stage of the protocol and the PR's gate (`nightly.sh
kernel-tests`, after test-32) is **to rule** -- proposed once there are enough tests to matter.
## Increments
- [x] 1. The harness (`Makefile`, `harness.h`, `fakes.[ch]`) and `test_gpt.c`: `gpt_read` and
`gpt_write_one` against a RAM disk -- a valid table; more used entries than `max` (#180); an
entry out of range past `max` (#180); the first write failing and a later one failing (#168).
Seen to fail against `gpt.c` as it was before #168 and #180 (`54311be1~1`): 12 checks, 4
failures -- each of the two defects caught. Now: 0 failures.
- [ ] 2. The xHCI configuration-descriptor walk moved out of the event loop, then #170 and #171
tested first and fixed.
- [ ] 3. #176 (seeds wiped on every exit), #177 (the fatal stops halt), #178 (the slot array) --
each with what it needs moved or faked.
- [ ] 4. #179 once #187 is ruled (the block layer's header read).
- [ ] 5. To rule: the kernel tests as a protocol stage and in the gate.
+39
View File
@@ -0,0 +1,39 @@
# SPDX-License-Identifier: LicenseRef-Starship-1.0
# Copyright (c) 2023-2026 Robert A. James
#
# The kernel host tests (docs/v4.0.0/plans/2026-11-12-kernel-host-tests.md): each test_<unit>.c
# built on the host with the kernel source file(s) it tests and the fakes, nothing else of the
# kernel, and run.
#
# make -C kernel/tests test build and run them all; fails if any check fails
# make -C kernel/tests run-test_gpt
#
# A test's sources are SRC_<test>; give another on the command line to run the same test against
# it (how a test is seen to fail against the code before a fix):
# make -C kernel/tests run-test_gpt SRC_test_gpt=/path/to/old/gpt.c
HERE := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
ROOT := $(abspath $(HERE)/../..)
BUILD := $(ROOT)/build/kernel-tests
CC ?= cc
CFLAGS := -std=c99 -Wall -Wextra -Werror -O2 -g -I$(HERE) -I$(ROOT)/kernel/include \
-I$(ROOT)/kernel/include/starkernel -I$(ROOT)/v3/include
TESTS := test_gpt
SRC_test_gpt := $(ROOT)/kernel/src/gpt.c
define TEST_RULE
$(BUILD)/$(1): $(HERE)/$(1).c $(HERE)/fakes.c $(HERE)/fakes.h $(HERE)/harness.h $$(SRC_$(1))
@mkdir -p $(BUILD)
$(CC) $(CFLAGS) $(HERE)/$(1).c $(HERE)/fakes.c $$(SRC_$(1)) -o $$@
.PHONY: run-$(1)
run-$(1): $(BUILD)/$(1)
@echo "$(1)"
@$(BUILD)/$(1)
endef
$(foreach t,$(TESTS),$(eval $(call TEST_RULE,$(t))))
.PHONY: test
test: $(addprefix run-,$(TESTS))
@echo "all kernel host tests passed"
+104
View File
@@ -0,0 +1,104 @@
/* SPDX-License-Identifier: LicenseRef-Starship-1.0
* Copyright (c) 2023-2026 Robert A. James
*/
/* fakes.c -- see fakes.h. */
#include "fakes.h"
#include <stdlib.h>
#include <string.h>
/* ---- the console ---- */
#define FAKE_LINES 256
#define FAKE_LINE 256
static char lines[FAKE_LINES][FAKE_LINE];
static unsigned n_lines;
void fake_console_reset(void) { n_lines = 0; }
unsigned fake_console_lines(void) { return n_lines; }
const char *fake_console_line(unsigned i) { return i < n_lines ? lines[i] : ""; }
int fake_console_said(const char *text)
{
unsigned i;
for (i = 0; i < n_lines; i++) if (strstr(lines[i], text)) return 1;
return 0;
}
static void take(const char *s)
{
if (n_lines < FAKE_LINES) {
strncpy(lines[n_lines], s, FAKE_LINE - 1);
lines[n_lines][FAKE_LINE - 1] = '\0';
n_lines++;
}
}
void console_println(const char *s) { take(s); }
void console_puts(const char *s) { take(s); }
void console_putc(char c) { (void)c; }
/* ---- the RNG ---- */
static uint32_t rng_state = 1u;
static int rng_fails;
void fake_rng_reset(void) { rng_state = 1u; rng_fails = 0; }
void fake_rng_fail(int fail) { rng_fails = fail; }
int rng_get_bytes(uint8_t *buf, size_t n)
{
size_t i;
if (rng_fails) return -1;
for (i = 0; i < n; i++) { rng_state = rng_state * 1103515245u + 12345u; buf[i] = (uint8_t)(rng_state >> 16); }
return 0;
}
/* ---- the disk ---- */
static int listed(const uint32_t *list, uint32_t n, uint32_t block)
{
uint32_t i;
for (i = 0; i < n; i++) if (list[i] == block) return 1;
return 0;
}
static int d_open(blkio_dev_t *dev, const blkio_params_t *p) { dev->state = p->opaque; return BLKIO_OK; }
static int d_close(blkio_dev_t *dev) { (void)dev; return BLKIO_OK; }
static int d_read(blkio_dev_t *dev, uint32_t fblock, void *dst)
{
fake_disk *d = (fake_disk *)dev->state;
if (fblock >= d->blocks) return BLKIO_EINVAL;
if (listed(d->fail_read, d->n_fail_read, fblock)) return BLKIO_EIO;
memcpy(dst, d->bytes + (size_t)fblock * BLKIO_FORTH_BLOCK_SIZE, BLKIO_FORTH_BLOCK_SIZE);
return BLKIO_OK;
}
static int d_write(blkio_dev_t *dev, uint32_t fblock, const void *src)
{
fake_disk *d = (fake_disk *)dev->state;
if (fblock >= d->blocks) return BLKIO_EINVAL;
if (listed(d->fail_write, d->n_fail_write, fblock)) return BLKIO_EIO;
memcpy(d->bytes + (size_t)fblock * BLKIO_FORTH_BLOCK_SIZE, src, BLKIO_FORTH_BLOCK_SIZE);
d->writes++;
return BLKIO_OK;
}
static int d_flush(blkio_dev_t *dev) { fake_disk *d = (fake_disk *)dev->state; d->flushes++; return d->fail_flush ? BLKIO_EIO : BLKIO_OK; }
static int d_info(blkio_dev_t *dev, blkio_info_t *out)
{
fake_disk *d = (fake_disk *)dev->state;
memset(out, 0, sizeof *out);
out->forth_block_size = BLKIO_FORTH_BLOCK_SIZE;
out->total_blocks = d->blocks;
out->phys_sector_size = 512u;
out->phys_size_bytes = (uint64_t)d->blocks * BLKIO_FORTH_BLOCK_SIZE;
return BLKIO_OK;
}
static const blkio_vtable_t fake_vt = { d_open, d_close, d_read, d_write, d_flush, d_info };
int fake_disk_open(blkio_dev_t *dev, fake_disk *d, uint32_t blocks)
{
blkio_params_t p;
memset(d, 0, sizeof *d);
d->bytes = (uint8_t *)calloc(blocks, BLKIO_FORTH_BLOCK_SIZE);
d->blocks = blocks;
if (!d->bytes) return BLKIO_EIO;
memset(&p, 0, sizeof p);
p.forth_block_size = BLKIO_FORTH_BLOCK_SIZE;
p.total_blocks = blocks;
p.opaque = d;
return blkio_open(dev, &fake_vt, &p);
}
void fake_disk_free(fake_disk *d) { free(d->bytes); d->bytes = 0; }
void fake_disk_fail_read(fake_disk *d, uint32_t block) { if (d->n_fail_read < FAKE_DISK_MAX_FAULTS) d->fail_read[d->n_fail_read++] = block; }
void fake_disk_fail_write(fake_disk *d, uint32_t block) { if (d->n_fail_write < FAKE_DISK_MAX_FAULTS) d->fail_write[d->n_fail_write++] = block; }
uint8_t *fake_disk_sector(fake_disk *d, uint64_t lba) { return d->bytes + (size_t)lba * 512u; }
+44
View File
@@ -0,0 +1,44 @@
/* SPDX-License-Identifier: LicenseRef-Starship-1.0
* Copyright (c) 2023-2026 Robert A. James
*/
/* fakes.h -- what a kernel unit calls, faked on the host: the console (captured), the RNG
* (deterministic), and a RAM disk whose reads or writes fail at chosen 1 KiB blocks
* (docs/v4.0.0/plans/2026-11-12-kernel-host-tests.md).
*/
#ifndef KERNEL_TESTS_FAKES_H
#define KERNEL_TESTS_FAKES_H
#include <stdint.h>
#include <stddef.h>
#include "blkio.h"
/* The console: every console_println/console_puts line, in order. */
void fake_console_reset(void);
int fake_console_said(const char *text); /* 1 if some line contains `text` */
unsigned fake_console_lines(void);
const char *fake_console_line(unsigned i);
/* The RNG: a fixed sequence, so a test's GUIDs are the same every run; fake_rng_fail makes
* rng_get_bytes return -1 (no backend). */
void fake_rng_reset(void);
void fake_rng_fail(int fail);
/* A RAM disk of `blocks` 1 KiB blocks (512-byte sectors, so 2 per block). A read or write of a
* block in its fail lists returns BLKIO_EIO and changes nothing; every write is counted. */
#define FAKE_DISK_MAX_FAULTS 8
typedef struct {
uint8_t *bytes;
uint32_t blocks;
uint32_t fail_read[FAKE_DISK_MAX_FAULTS], n_fail_read;
uint32_t fail_write[FAKE_DISK_MAX_FAULTS], n_fail_write;
unsigned writes, flushes;
int fail_flush;
} fake_disk;
int fake_disk_open(blkio_dev_t *dev, fake_disk *d, uint32_t blocks); /* zeroed; BLKIO_OK */
void fake_disk_free(fake_disk *d);
void fake_disk_fail_read(fake_disk *d, uint32_t block);
void fake_disk_fail_write(fake_disk *d, uint32_t block);
uint8_t *fake_disk_sector(fake_disk *d, uint64_t lba); /* a 512-byte sector */
#endif
+21
View File
@@ -0,0 +1,21 @@
/* SPDX-License-Identifier: LicenseRef-Starship-1.0
* Copyright (c) 2023-2026 Robert A. James
*/
/* harness.h -- the kernel host tests' checks (docs/v4.0.0/plans/2026-11-12-kernel-host-tests.md).
*
* As v4's tests: CHECK(cond, fmt, ...) counts a check, prints FAIL with the file, line and message
* when it fails; DONE() prints "N checks, M failures" and is main's return value.
*/
#ifndef KERNEL_TESTS_HARNESS_H
#define KERNEL_TESTS_HARNESS_H
#include <stdio.h>
static int harness_checks = 0, harness_failures = 0;
#define CHECK(c, ...) do { harness_checks++; if (!(c)) { harness_failures++; \
printf("FAIL %s:%d: ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
#define DONE() (printf(" %d checks, %d failures\n", harness_checks, harness_failures), harness_failures != 0)
#endif
+120
View File
@@ -0,0 +1,120 @@
/* SPDX-License-Identifier: LicenseRef-Starship-1.0
* Copyright (c) 2023-2026 Robert A. James
*/
/* test_gpt.c -- kernel/src/gpt.c on the host: gpt_read and gpt_write_one against a RAM disk whose
* writes can fail at chosen blocks (docs/v4.0.0/plans/2026-11-12-kernel-host-tests.md, increment 1).
* #168: gpt_write_one tells "nothing written" (GPT_EIO) from "the disk is changed" (GPT_PART).
* #180: gpt_read checks every entry and says when there are more than max (GPT_MANY).
*/
#include "harness.h"
#include "fakes.h"
#include "starkernel/gpt.h"
#include <string.h>
#define DISK_BLOCKS 8192u /* 8 MiB: 16384 sectors */
#define SECTORS (DISK_BLOCKS * 2u)
static void put_le32(uint8_t *p, uint32_t v) { p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); p[2] = (uint8_t)(v >> 16); p[3] = (uint8_t)(v >> 24); }
static void put_le64(uint8_t *p, uint64_t v) { put_le32(p, (uint32_t)v); put_le32(p + 4, (uint32_t)(v >> 32)); }
/* A GPT written by hand: `used` entries of 64 sectors each from LBA 2048, the first two of them
* the StarshipOS identity and Artemis types when `ours_at` is 0, or at entries ours_at and
* ours_at+1; entry `bad` (if not -1) given a range past the last usable LBA. Both CRCs good. */
static void make_table(fake_disk *d, unsigned used, unsigned ours_at, int bad)
{
static const uint8_t linux_t[16] = { 0xAF, 0x3D, 0xC6, 0x0F, 0x83, 0x84, 0x72, 0x47, 0x8E, 0x79, 0x3D, 0x69, 0xD8, 0x47, 0x7D, 0xE4 };
uint8_t *mbr = fake_disk_sector(d, 0), *h = fake_disk_sector(d, 1), *e = fake_disk_sector(d, 2);
uint64_t last_usable = SECTORS - 34u;
unsigned i;
memset(d->bytes, 0, (size_t)34 * 512u);
mbr[446 + 4] = 0xEE; put_le32(mbr + 446 + 8, 1u); put_le32(mbr + 446 + 12, SECTORS - 1u);
mbr[510] = 0x55; mbr[511] = 0xAA;
for (i = 0; i < used; i++) {
uint8_t *x = e + (size_t)i * 128u;
const uint8_t *t = (i == ours_at) ? gpt_type_starship_identity : (i == ours_at + 1u) ? gpt_type_starship_artemis : linux_t;
memcpy(x, t, 16);
x[16] = (uint8_t)(i + 1); /* a partition GUID */
put_le64(x + 32, 2048u + (uint64_t)i * 64u);
put_le64(x + 40, 2048u + (uint64_t)i * 64u + 63u);
if ((int)i == bad) { put_le64(x + 32, last_usable - 8u); put_le64(x + 40, last_usable + 64u); }
}
memcpy(h, "EFI PART", 8);
put_le32(h + 8, 0x00010000u); put_le32(h + 12, 92u);
put_le64(h + 24, 1u); put_le64(h + 32, SECTORS - 1u);
put_le64(h + 40, 34u); put_le64(h + 48, last_usable);
h[56] = 0x5A; /* a disk GUID */
put_le64(h + 72, 2u); put_le32(h + 80, 128u); put_le32(h + 84, 128u);
put_le32(h + 88, gpt_crc32(e, 128u * 128u));
put_le32(h + 16, 0u); put_le32(h + 16, gpt_crc32(h, 92u));
}
int main(void)
{
blkio_dev_t dev;
fake_disk d;
gpt_part_t parts[16];
unsigned n = 0;
uint8_t guid[16];
int rc;
fake_rng_reset();
fake_console_reset();
/* gpt_write_one, then gpt_read: the table it writes reads back */
CHECK(fake_disk_open(&dev, &d, DISK_BLOCKS) == BLKIO_OK, "a RAM disk");
rc = gpt_write_one(&dev, gpt_type_starship_identity, 4096u, guid);
CHECK(rc == GPT_OK, "gpt_write_one on a blank disk: GPT_OK (%d)", rc);
rc = gpt_read(&dev, parts, 16u, &n);
CHECK(rc == GPT_OK && n == 1u && memcmp(parts[0].type, gpt_type_starship_identity, 16) == 0
&& parts[0].first_lba == 2048u, "and reads back: one identity partition at 2048 (rc %d, n %u)", rc, n);
fake_disk_free(&d);
/* #180: every entry checked, more than max said */
fake_disk_open(&dev, &d, DISK_BLOCKS);
make_table(&d, 16u, 0u, -1);
rc = gpt_read(&dev, parts, 16u, &n);
CHECK(rc == GPT_OK && n == 16u, "16 used entries, max 16: GPT_OK, 16 (rc %d, n %u)", rc, n);
make_table(&d, 18u, 16u, -1);
rc = gpt_read(&dev, parts, 16u, &n);
CHECK(rc == GPT_MANY && n == 16u, "#180: 18 used entries, ours the 17th and 18th, max 16: GPT_MANY, not cut to 16 as if they were all (rc %d, n %u)", rc, n);
make_table(&d, 17u, 0u, 16);
rc = gpt_read(&dev, parts, 16u, &n);
CHECK(rc == GPT_BAD, "#180: the 17th entry out of range, max 16: GPT_BAD -- it is checked (rc %d)", rc);
make_table(&d, 4u, 0u, 2);
rc = gpt_read(&dev, parts, 16u, &n);
CHECK(rc == GPT_BAD, "an entry out of range within max: GPT_BAD (rc %d)", rc);
fake_disk_free(&d);
/* #168: gpt_write_one says whether it changed the disk */
fake_disk_open(&dev, &d, DISK_BLOCKS);
fake_rng_fail(1);
rc = gpt_write_one(&dev, gpt_type_starship_identity, 4096u, guid);
CHECK(rc == GPT_EIO && d.writes == 0u, "no RNG: GPT_EIO before any write (rc %d, %u writes)", rc, d.writes);
fake_rng_fail(0);
fake_disk_free(&d);
fake_disk_open(&dev, &d, DISK_BLOCKS);
make_table(&d, 2u, 0u, -1);
fake_disk_fail_write(&d, 0u); /* sector 0: its first write */
rc = gpt_write_one(&dev, gpt_type_starship_identity, 4096u, guid);
CHECK(rc == GPT_EIO && d.writes == 0u, "#168: its first write fails: GPT_EIO, nothing written (rc %d, %u writes)", rc, d.writes);
rc = gpt_read(&dev, parts, 16u, &n);
CHECK(rc == GPT_OK && n == 2u, "and the old table is still there (rc %d, n %u)", rc, n);
fake_disk_free(&d);
fake_disk_open(&dev, &d, DISK_BLOCKS);
make_table(&d, 2u, 0u, -1);
fake_disk_fail_write(&d, 1u); /* the primary entries, after the MBR is zeroed */
rc = gpt_write_one(&dev, gpt_type_starship_identity, 4096u, guid);
CHECK(rc == GPT_PART && d.writes > 0u, "#168: a later write fails: GPT_PART, the disk changed (rc %d, %u writes)", rc, d.writes);
fake_disk_free(&d);
fake_disk_open(&dev, &d, DISK_BLOCKS);
d.fail_flush = 1;
rc = gpt_write_one(&dev, gpt_type_starship_identity, 4096u, guid);
CHECK(rc == GPT_PART && d.writes == 1u, "#168: the first write made, its flush failing: GPT_PART (rc %d, %u writes)", rc, d.writes);
fake_disk_free(&d);
return DONE();
}
Binary file not shown.