PPAP has three categories of tests, each with its own framework, build process, and execution environment.
| Category | Location | Framework | Runs on | Build flag |
|---|---|---|---|---|
| Host unit tests | tests/host/ |
test_framework.h |
Developer machine | Always |
| Kernel integration tests | tests/kernel/ |
ktest.h |
QEMU / hardware | --test |
| User-space tests | tests/user/ |
utest.h |
QEMU / hardware | --test |
# Host unit tests (no cross-compiler needed)
./scripts/test.sh
# On-target tests (ARM, requires qemu-system-arm)
./scripts/run.sh --test
# On-target tests (m68k, requires qemu-system-m68k)
./scripts/run.sh --test qemu_m68k
# On-target tests (RISC-V, requires qemu-system-riscv32)
./scripts/run.sh --test qemu_rv32
# On-target tests (PC/XT i16, requires qemu-system-i386)
./scripts/run.sh --test pcxt
# Extended on-target tests (ARM lane with extra user tests)
./scripts/run.sh --test-extended qemu_arm
# Run only tests matching a substring
./scripts/run.sh --test --filter=pipe
# Also run tests marked FLAKY
./scripts/run.sh --test --flaky
# Everything at once (host + build all targets + QEMU tests)
./scripts/test.sh --all
# Everything + extended QEMU lanes
./scripts/test.sh --all --extendedPure C unit tests compiled with the system gcc/clang. No cross-compiler toolchain needed. Good for testing kernel modules that have no hardware dependencies (memory allocators, path helpers, CPU interpreters).
#include "test_framework.h"
static void test_example(void) {
ASSERT(1 + 1 == 2, "basic math");
ASSERT_EQ(page_alloc(), expected_addr);
ASSERT_NOT_NULL(ptr);
}
int main(void) {
TEST_GROUP("Example tests");
RUN_TEST(test_example);
TEST_SUMMARY();
}Macros:
ASSERT(cond, msg)— check condition, print file:line on failureASSERT_EQ(a, b)— compare values, print both on mismatchASSERT_NULL(p)/ASSERT_NOT_NULL(p)— pointer checksRUN_TEST(fn)— run a test functionTEST_GROUP(name)— section headerTEST_SUMMARY()— print totals, return exit code
| File | Tests |
|---|---|
test_kmem.c |
kmem_pool object allocator |
test_h68k_path.c |
Human68k path translation and errno mapping |
test_ecpu_z80.c |
Z80 emulator (85 tests: all instruction groups) |
test_ecpu_m68k.c |
m68k emulator (instruction groups, addressing modes) |
tests/host/CMakeLists.txt defines one executable per host-safe module.
Tests that depend on target architecture or kernel-only state now live in
the on-target user test suite instead of being forced through the host build.
# Manual build
cmake -S tests/host -B build/host -DCMAKE_BUILD_TYPE=Debug
cmake --build build/host
ctest --test-dir build/host --output-on-failure- Create
tests/host/test_foo.cwith#include "test_framework.h" - Add to
tests/host/CMakeLists.txt:add_executable(test_foo ${SRC}/kernel/path/foo.c test_foo.c) target_include_directories(test_foo PRIVATE ${SRC} ${CMAKE_CURRENT_SOURCE_DIR}) add_test(NAME foo COMMAND test_foo)
- Run
./scripts/test.sh
On-target tests that exercise kernel subsystems with real VFS, page
allocator, pipe, signal, and filesystem infrastructure. Compiled into
the kernel image when PPAP_TESTS=ON and run before the scheduler starts.
Tests call kernel APIs directly (sys_open, sys_read, sys_write,
sys_pipe, etc.) and report results via uart_puts().
static void my_integration_test(void)
{
uart_puts("\n=== My integration tests ===\n");
test_pass = 0;
test_fail = 0;
long fd = sys_open("/etc/hostname", O_RDONLY, 0);
test_report("open /etc/hostname", fd >= 0);
if (fd >= 0) sys_close(fd);
total_pass += test_pass;
total_fail += test_fail;
}./scripts/build.sh --test qemu_armsetsPPAP_TESTS=ON- CMake adds
ktest.cto the kernel build and definesPPAP_TESTS=1 - After VFS mount,
target_post_mount()callsktest_run_all() - All test suites run, printing
TEST: name ... PASS/FAILto UART - After kernel tests,
target_init_path()returns:
Note: qemu_rv32 has ktest_run_all() disabled — the blkdev/VFAT
kernel tests expect a FAT32 ramblk image that rv32 does not embed, and
the 18 test failures corrupt kernel state, crashing init startup.
pcxt does not run kernel tests (no ktest_run_all() call in
target_post_mount()).
/bin/runtestsin normal test builds/bin/runtests_extin extended test builds (instead of/sbin/init), launching the user-space test runner
- Add a
static void foo_integration_test(void)function inktest.c - Call it from
ktest_run_all()
On-target tests that run as real user processes. Each test is a
standalone ELF binary compiled with PIC, linked against crt0.o and
syscall.o (raw SVC stubs — no libc).
#include "utest.h"
int main(void)
{
void *p = brk((void *)0);
UT_ASSERT(p != (void *)0, "initial brk non-zero");
UT_ASSERT_EQ(1 + 1, 2);
UT_SUMMARY("test_example");
}Macros:
UT_ASSERT(cond, msg)— check condition; printFAIL+ file:lineUT_ASSERT_EQ(a, b)— compare values; print expected/actualUT_PRINT(s)— write a string literal to stdoutUT_SUMMARY(name)— print totals,return 0orreturn 1
Integer printing uses subtraction (power-of-10 lookup) — no hardware divide and no libgcc dependency.
Sequentially vfork + execve each test binary, collect exit status.
Every log line is prefixed with [T+S.CC] (elapsed seconds and
centiseconds since the runner started, 10 ms resolution). PASS/FAIL
lines also include per-test elapsed time. Final summary: counts +
ALL TESTS PASSED or SOME TESTS FAILED.
The test list is initialised at runtime (not as a static array) because PIC binaries cannot have initialised pointer arrays — the ELF loader only relocates GOT entries, not arbitrary data pointers.
Per-test flags
Each entry in the test list carries a flag:
| Flag | Meaning | Counted as |
|---|---|---|
TEST_ENABLED |
Run normally | run / passed / failed |
TEST_DISABLED |
Skip — broken or fix pending | skipped |
TEST_UNSUPPORTED |
Skip — feature not available on this target by design | n/a |
TEST_FLAKY |
Skip by default; run when /etc/test_run_flaky exists |
skipped |
TEST_SLOW |
Skip by default; run when /etc/test_run_slow exists |
skipped |
The TEST_DISABLED vs TEST_UNSUPPORTED split matters for triage:
TEST_DISABLEDflags real pending work (e.g. rv32's orphan-reaping leak or rv32clock_gettimereturning -EINVAL for the test's clock_ids). The summary'sskippedcount is the to-do list.TEST_UNSUPPORTEDflags by-design gaps that won't be fixed (8086 has no FPU; pcxt uses elf16 not elf.c; m68k-only binaries on non-m68k targets). Then/acount is informational only.
Platform-specific tests (test_x68k, test_h68k_dos) are TEST_UNSUPPORTED
on non-m68k and TEST_ENABLED on m68k via #if defined(__m68k__).
Runtime filter
If the file /etc/test_filter exists in the romfs, only tests whose path
contains the filter string (substring match) are run. All others are
silently skipped (not counted in totals).
Flaky/slow opt-in
If the file /etc/test_run_flaky exists in the romfs, TEST_FLAKY tests
run instead of being skipped.
If the file /etc/test_run_slow exists in the romfs, TEST_SLOW tests
run instead of being skipped.
Both files are created automatically by scripts/run.sh via a temporary
overlay directory baked into romfs at build time (see
--filter, --flaky, and --slow below).
| File | Tests |
|---|---|
test_exec.c |
ELF loading, XIP, GOT relocation |
test_elf.c |
ELF32 header validation, segment extraction, .got lookup |
test_vfork.c |
vfork + execve + waitpid |
test_pipe.c |
Pipe creation, read/write, EOF |
test_brk.c |
Heap growth via brk() |
test_fd.c |
dup, dup2, close, redirection |
test_signal.c |
Signal handler install + delivery |
test_poll.c |
ppoll syscall |
test_sleep_intr.c |
Process lifecycle, exit codes |
test_orphan.c |
Orphan reparenting to init |
test_fault.c |
CPU fault handlers (illegal insn, div-by-zero) |
test_id.c |
getpid, getppid, setpgid, getpgid, setsid |
test_fs.c |
Filesystem operations (open, read, readdir, stat) |
test_rw.c |
File read/write on writable filesystems |
test_time.c |
nanosleep behavior and error paths |
test_iov.c |
readv, writev scatter/gather I/O |
test_stat.c |
stat, getdents on romfs |
test_tmpfs.c |
tmpfs create, write, read, unlink, multi-page I/O (disabled on rv32) |
test_ufs.c |
UFS write+read (pcxt only; disabled on romfs-root targets) |
test_float.c |
FPU register preservation across context switch (disabled on m68k) |
test_signal_float.c |
FPU register preservation across signal delivery (disabled on m68k) |
test_x68k.c |
Human68k subsystem (X-format .x execution) |
test_cpm.c |
CP/M subsystem integration (.COM exec, BDOS bridge, signals, file I/O; disabled on rv32) |
test_sos.c |
S-OS SWORD subsystem integration (disabled on rv32) |
test_msdos.c |
MS-DOS subsystem integration (pcxt only; disabled on all other targets) |
test_trace.c |
ptrace exec + PPAP syscall trace integration (FLAKY; run with --flaky) |
test_pdb.c |
pdb scripted smoke and command coverage (TEST_SLOW on m68k; use --slow) |
test_pdb_arm_disas.c |
ARM-only pdb disas smoke (built in /bin/, not in default runtests) |
test_h68k_dos.c |
Human68k DOS bridge integration via R-format test binaries |
test_musl.c |
musl libc integration (harness); runs musl-linked child via vfork+execve |
test_musl_child.c |
musl-linked child binary exercising libc exit, stdio, malloc, strings |
User tests are built by cmake/user.cmake as custom commands, controlled
by the PPAP_TESTS CMake option. The build system:
- Cross-compiles each
test_*.cfromtests/user/ - Builds Human68k R-format user tests from
tests/user/r68k/on all targets - Links with
crt0.o+syscall.o(arch-specific SVC stubs) - Installs to romfs
/bin/ - Compiler flags:
-fPIC -msingle-pic-base -mpic-register=r9(ARM) or-msep-data(m68k)
- Create
tests/user/test_foo.c:#include "utest.h" int main(void) { UT_ASSERT(1, "sanity"); UT_SUMMARY("test_foo"); }
- Add
test_footo theUSER_TESTSlist incmake/user.cmake - Add an entry to the test list in
tests/user/runtests.c:For per-target gating, put the ifdef inside the struct initialiser:tests[t++] = (test_entry_t){ "/bin/test_foo", TEST_ENABLED };
Usetests[t++] = (test_entry_t){ "/bin/test_foo", #if defined(__riscv) TEST_DISABLED /* fix pending */ #elif defined(__ia16__) TEST_UNSUPPORTED /* feature n/a here by design */ #else TEST_ENABLED #endif };
TEST_FLAKYfor tests that are known to be unreliable and should be skipped in CI. - Build with
--testand run
Some bugs only manifest with musl-linked binaries (e.g. double-free on
process exit caused by musl's _Exit() calling SYS_exit_group then
SYS_exit in a loop). To cover these, the test suite includes a
split harness + child pattern:
test_musl.c— bare-metal harness (usesutest.h, linked withcrt0.o + syscall.olike all other tests). Itvfork+execvestest_musl_childwith differentargv[1]values and checks exit codes.test_musl_child.c— linked against musl libc (viappap_musl_test_program()incmake/user.cmake). Uses standard<stdio.h>,<stdlib.h>,<string.h>. Dispatches onargv[1]:exit,printf,malloc,string.
To add a new musl sub-test, add a function in test_musl_child.c and
a run_subtest() call in test_musl.c.
To add a new musl-linked test binary:
- Create
tests/user/test_foo_child.cwith#include <stdio.h>etc. - Add
test_foo_childtoUSER_MUSL_TESTSincmake/user.cmake - The build system uses
ppap_musl_test_program()which compiles with musl headers and links with musl'scrt1.o + libc.avia the specs file
- No libc. Only
syscall.hwrappers are available (except for musl-linked tests inUSER_MUSL_TESTS). - No division on m68k. GCC emits
__divsi3calls; use subtraction loops (seeut_print_intinutest.h). - No static pointer arrays. PIC relocation only fixes GOT entries.
Initialise pointer arrays at runtime, or use a
switchstatement returning string literals (which are GOT-resolved per call site). - Use
vfork+execve, notfork. PPAP has no MMU;vforkshares the parent's address space. The child must immediatelyexecveor_exit— do not modify parent data or trigger faults.
Current user-space tests are a solid regression baseline, but subsystem coverage is not exhaustive yet.
ioctlwrapper is untested from userland.src/user/syscall.hdeclaresioctl(), but notests/user/test_*.ccurrently calls it. This leaves tty/device control behavior mostly validated only indirectly (through higher-level programs), not with explicit syscall assertions.test_m68k_emu.cis present but not in the default on-target suite. The source exists intests/user/, but it is not listed incmake/user.cmakeUSER_TESTSor intests/user/runtests.c. As a result, regressions in m68k-eCPU user process execution can be missed unless the test is run manually.- Subsystem ptrace mode is not covered.
test_trace.cvalidatesPPAP_TRACE_MODE_PPAP_SYSCALL; there is no user test enablingPPAP_TRACE_MODE_SUBSYS_CALLto validate Human68k/CP/M subsystem enter/exit trace events. In practice, this means ABI-tagged subsystem events (H68K_DOS,H68K_IOCS,CPM_BDOS,CPM_BIOS) are implemented but not CI-verified end-to-end. - Human68k DOS bridge is partially covered.
human68k_dos_dispatch()implements 56 DOS function IDs; currenttests/user/r68k/test_dos_*.Scoverage is 24 IDs. Missing areas include several console/input calls, wildcard file search (_FILES/_NFILES), handle duplication (_DUP/_DUP2), and metadata/update paths (_FILEDATE,_SETDATE,_SETTIME, etc.). Current R-format tests are strongest on basic process/memory/file/dir flows, but weaker on "DOS utility" behaviors and less common compatibility APIs. - Human68k IOCS dispatch has no dedicated user test binary. IOCS handlers are implemented, but the current user suite does not have a focused IOCS test equivalent to the DOS R-format set. This is a notable blind spot for console and system-service compatibility.
- CP/M BDOS bridge is partially covered.
cpm_bdos_dispatch()implements 38 function IDs;test_cpm.ccurrently targets a core subset (about 16 IDs: 0, 1, 2, 6, 9, 12, 14, 15, 16, 20, 21, 22, 24, 25, 26, 32). Uncovered areas include search first/next, random-record variants, and several disk/attribute vector functions. CP/M test coverage is therefore good for bootstrapping and basic file I/O, but not yet complete for directory iteration and random-record compatibility. qemu_rv32: kernel tests disabled, user 16/16 pass. Kernel tests are disabled because blkdev/VFAT tests expect a FAT32 ramblk that rv32 does not provide; the 18 failures corrupt state and crash init startup. User: 16 tests pass (vfork, pipe, fd, poll, id, rw, iov, stat, float, musl, exec, brk, fs, signal, sleep_intr, signal_float). Seven are stillTEST_DISABLEDon RISC-V with per-test comments intests/user/runtests.c:test_elf(ELF parser rejects inputs with -ENOEXEC),test_fault(kernel trap handler terminates the whole system on illegal-instruction instead of delivering SIGILL),test_orphan(leak-check assertion at line 105 fails — more than 12 KB of user_pages unaccounted for after orphans exit),test_time(clock_gettimereturns -EINVAL),test_tmpfs(line 115 errno mismatch with corrupted summary counters),test_cpm/test_sos(Z80 eCPU path takes a load-access fault early and brings the kernel down).pdbscripted coverage is architecture-asymmetric.test_pdbhas 170/367 failures on m68k and is markedTEST_DISABLEDthere. On ARM it isTEST_SLOW(base runner) /TEST_ENABLED(extended runner). ARM also has a dedicatedtest_pdb_arm_disasbinary for disassembly smoke.
- Add
tests/user/test_ioctl.cwith basic tty/deviceioctlhappy/error paths and include it inUSER_TESTSandruntests. - Enable
tests/user/test_m68k_emu.cin the default suite. - Extend
tests/user/test_trace.cto exercisePPAP_TRACE_MODE_SUBSYS_CALLagainst one Human68k and one CP/M child. - Add one new Human68k R-format test focused on currently uncovered DOS calls
(for example
_FILES/_NFILESplus_FILEDATE). - Add one CP/M
.COMtest focused on uncovered BDOS functions (for example search first/next and at least one random-record operation). - Add CPUTEST (another Z80 exerciser) for additional coverage beyond ZEXALL.
Builds with PPAP_TESTS=ON, runs under QEMU, and greps output for the
exact marker ALL TESTS PASSED.
QEMU self-termination: After printing the test summary, runtests
calls poweroff() which invokes SYS_POWEROFF (0x0B00). The kernel
routes this to target_qemu_poweroff(), which writes to an
architecture-specific QEMU exit device:
| Target | Mechanism | Exit device |
|---|---|---|
qemu_arm |
ARM semihosting SYS_EXIT_EXTENDED |
bkpt 0xAB |
qemu_m68k |
virt-ctrl MMIO halt |
0xFF009004 |
qemu_rv32 |
sifive_test MMIO |
0x100000 |
pcxt |
isa-debug-exit I/O port |
0x501 |
Kernel panics and unhandled faults also trigger the same exit path via
kernel_panic_halt(1), so QEMU exits immediately on fatal errors
instead of spinning until the external timeout.
The external timeout in run.sh remains as a safety net but should
rarely fire.
- ARM default timeout: 90 seconds
- RISC-V default timeout: 90 seconds
- m68k default timeout: 90 seconds
- m68k with
--slow: 150 seconds - pcxt default timeout: 180 seconds
Current test results (as of 2026-04-18):
| Target | Kernel tests | User tests | Total |
|---|---|---|---|
qemu_arm |
69 pass | 23/23 pass | All pass |
qemu_m68k |
69 pass | 23/23 pass | All pass |
qemu_rv32 |
Disabled (pre-existing blkdev crash) | 16/16 pass | User pass |
pcxt |
N/A (no ktest) | 17/17 pass | All pass |
./scripts/run.sh --test # ARM (default)
./scripts/run.sh --test qemu_rv32 # RISC-V
./scripts/run.sh --test qemu_m68k # m68k
./scripts/run.sh --test pcxt # PC/XT (i16)On pcxt, the VGA console is invisible to the QEMU serial capture used
by the test runner. The runtests binary redirects stdout to
/dev/ttyS0 at startup so test output appears on the serial port.
(The redirect is unconditional — on other targets /dev/ttyS0 open
either succeeds harmlessly or fails and is skipped.)
The pcxt user suite runs 17 of the shared user tests today. The remaining entries fall into two buckets visible in the summary:
skipped(2):test_sleep_intr(nanosleep-interruption path not implemented on ia16) andtest_fault(ia16 routes CPU faults to kernel panic). Both areTEST_DISABLED, notTEST_UNSUPPORTED— they can be lit up when the underlying features land.n/a(12): tests that exercise features pcxt does not have — no FPU, no Z80/CP/M/S-OS subsystems, no musl libc, ELF32 parser n/a (pcxt uses elf16), arch-specific ptrace regsets, etc.
Builds with PPAP_TESTS=ON and PPAP_TESTS_EXTENDED=ON, runs under QEMU
with a larger timeout budget, and executes /bin/runtests_ext as PID 1.
- ARM extended timeout: 90 seconds
- m68k extended timeout: 150 seconds
- m68k extended with
--slow: 180 seconds
./scripts/run.sh --test-extended qemu_arm
./scripts/run.sh --test-extended qemu_m68kThese flags write config files into a temporary overlay directory that is merged into romfs at build time, then cleaned up.
# Run only tests whose path contains "pipe"
./scripts/run.sh --test --filter=pipe
# Also run tests marked TEST_FLAKY
./scripts/run.sh --test --flaky
# Also run tests marked TEST_SLOW
./scripts/run.sh --test --slow
# Combine: filter + flaky
./scripts/run.sh --test --filter=pdb --flaky
# Combine: filter + flaky + slow
./scripts/run.sh --test --filter=pdb --flaky --slow--filter writes /etc/test_filter (the substring to match).
--flaky writes /etc/test_run_flaky (existence is the signal; content
is ignored).
--slow writes /etc/test_run_slow (existence is the signal; content
is ignored). All imply --build.
On m68k lanes, enabling --slow increases run.sh timeout so slow tests
such as test_pdb do not fail due to harness timeout alone.
Full CI pipeline:
- Host unit tests
- Build all production targets (ARM and m68k,
PPAP_TESTS=OFF) - Print binary sizes
- QEMU on-target tests (ARM and m68k,
PPAP_TESTS=ON)
./scripts/test.sh --all --extended (or --all-extended) replaces step 4
with extended QEMU lanes (run.sh --test-extended).
boot → kernel init → VFS mount → target_post_mount()
│
┌───────┴───────┐
│ PPAP_TESTS=1 │
└───────┬───────┘
│
ktest_run_all()
(kernel tests)
│
sched_start()
│
target_init_path() = "/bin/runtests"
│
runtests (PID 1)
├── [T+0.00] test_exec
├── [T+0.3x] test_elf
├── test_vfork
├── test_fault
├── test_pipe
├── test_brk
├── test_fd
├── test_signal
├── test_poll
├── test_sleep_intr
├── test_orphan
├── test_id
├── test_fs
├── test_rw
├── test_time
├── test_iov
├── test_stat
├── test_tmpfs
├── test_x68k (ENABLED on m68k, DISABLED on ARM)
├── test_h68k_dos (ENABLED on m68k, DISABLED on ARM)
├── test_cpm
├── test_sos
├── test_musl
├── test_trace (FLAKY; run with --flaky)
└── test_pdb (SLOW; run with --slow)
│
"ALL TESTS PASSED"
│
poweroff()
(SYS_POWEROFF → QEMU exits)
│
run.sh --test checks output
Extended lane (--test-extended) uses the same flow except
target_init_path() resolves to /bin/runtests_ext.
| Flag | Where | Effect |
|---|---|---|
--test |
./scripts/run.sh / build.sh |
Sets PPAP_TESTS=ON in CMake |
--test-extended |
./scripts/run.sh / build.sh |
Sets PPAP_TESTS=ON and PPAP_TESTS_EXTENDED=ON |
--filter=<pattern> |
./scripts/run.sh |
Writes /etc/test_filter into romfs overlay; implies --build |
--flaky |
./scripts/run.sh |
Writes /etc/test_run_flaky into romfs overlay; implies --build |
--slow |
./scripts/run.sh |
Writes /etc/test_run_slow into romfs overlay; implies --build |
PPAP_TESTS=ON |
CMake option | Compiles ktest.c into kernel; defines PPAP_TESTS=1 C macro; enables user test builds |
PPAP_TESTS_EXTENDED=ON |
CMake option | Defines PPAP_TESTS_EXTENDED=1; selects /bin/runtests_ext as init path |
PPAP_TESTS=1 |
C preprocessor | Guards ktest_run_all() call in target_post_mount(); selects /bin/runtests as init path |
PPAP_TESTS_EXTENDED=1 |
C preprocessor | Selects /bin/runtests_ext as init path |
PPAP_TESTS=ON |
cmake/user.cmake |
Builds test binaries and installs to romfs |