diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4d65e4e..06fbf1d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,17 +62,34 @@ python scripts/generate_config.py configs/example_boot.yaml /tmp/generated/ ## Test Suites -eboot includes 7 unit test suites that run natively on the host: +eboot includes 17 C unit test suites that run natively on the host, plus a +set of Python tests that check the build files and tooling statically: | Test | Covers | |---|---| | `test_bootctl` | Boot control block save/load, CRC, rollback | | `test_crypto` | SHA-256 against known vectors | +| `test_image_verify` | Image header parse bounds | +| `test_recovery` | Recovery-mode UART protocol handler | +| `test_slot_size_bounds` | `verify_slot()` rejects `image_size` larger than the slot | +| `test_fw_transport` | UART raw / XMODEM / YMODEM firmware transport framing | | `test_device_table` | Device table create, add, validate | | `test_runtime_svc` | Runtime variable get/set/delete | | `test_board_config` | Pin/memory/IRQ config lookup | | `test_multicore` | Core state management, SMP/AMP init | | `test_board_registry` | Board register, find, activate | +| `test_slot_manager` | Production firmware slot manager | +| `test_boot_log` | Boot log subsystem | +| `test_ed25519` | Ed25519 signature verification (RFC 8032) | +| `test_keystore` | Boot keystore management | +| `test_rollback` | Anti-rollback security counter | +| `test_storage` | Unified storage bounds checking | + +Every `tests/unit/test_*.c` suite must be registered in `tests/CMakeLists.txt` +with both an `add_executable()` and an `add_test()`. A suite that is not +registered is never compiled and never run, and nothing else in the build +reports it as missing, so `tests/unit/test_cmake_test_registration.py` checks +this and fails if a suite is left out. Run all tests: diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c9e95ff..6a5473d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -21,6 +21,11 @@ add_executable(test_recovery unit/test_recovery.c) target_link_libraries(test_recovery PRIVATE eboot_core eboot_stage1) add_test(NAME test_recovery COMMAND test_recovery) +# --- test_fw_transport: UART raw/XMODEM/YMODEM firmware transport --- +add_executable(test_fw_transport unit/test_fw_transport.c) +target_link_libraries(test_fw_transport PRIVATE eboot_core) +add_test(NAME test_fw_transport COMMAND test_fw_transport) + # --- test_slot_size_bounds: verify_slot() must reject image_size > slot capacity --- add_executable(test_slot_size_bounds unit/test_slot_size_bounds.c) target_link_libraries(test_slot_size_bounds PRIVATE eboot_core) @@ -89,7 +94,7 @@ if(VALGRIND) test_device_table test_runtime_svc test_board_config test_multicore test_board_registry test_slot_manager test_boot_log test_image_verify test_recovery - test_slot_size_bounds) + test_slot_size_bounds test_fw_transport) add_test( NAME valgrind_${TEST_NAME} COMMAND ${VALGRIND} ${VALGRIND_OPTS} $ diff --git a/tests/unit/test_cmake_test_registration.py b/tests/unit/test_cmake_test_registration.py new file mode 100644 index 0000000..e803d55 --- /dev/null +++ b/tests/unit/test_cmake_test_registration.py @@ -0,0 +1,74 @@ +"""Regression tests for the CTest registrations in tests/CMakeLists.txt. + +Every C suite under tests/unit/ has to be named in tests/CMakeLists.txt to be +compiled and run at all. Nothing else notices when one is left out: the suite +stops building, ctest reports one fewer test, and the run still goes green. + +That is how test_fw_transport.c -- the 12-case regression suite covering the +unbounded raw length prefix, the unbounded YMODEM block-0 filename scan and +the missing block-number validation -- stopped running. A merge replaced its +registration block instead of appending a new one, so the file stayed in the +tree while the target that built it disappeared. + +These parse tests/CMakeLists.txt statically, so no cmake or compiler is needed. +""" + +import re +from pathlib import Path + +TESTS_DIR = Path(__file__).resolve().parent +CMAKELISTS = TESTS_DIR.parent / "CMakeLists.txt" + +ADD_EXECUTABLE_RE = re.compile(r"add_executable\(\s*(\w+)\s+([^)]*?)\)", re.S) +ADD_TEST_RE = re.compile(r"add_test\(\s*NAME\s+(\w+)\s+COMMAND\s+(\w+)") + + +def _cmake_text(): + return CMAKELISTS.read_text(encoding="utf-8") + + +def _c_suites(): + """Every C suite file under tests/unit/, by file name.""" + return sorted(p.name for p in TESTS_DIR.glob("test_*.c")) + + +def _registered_sources(): + """Source file names named by an add_executable() in tests/CMakeLists.txt.""" + sources = set() + for _target, source_list in ADD_EXECUTABLE_RE.findall(_cmake_text()): + for source in source_list.split(): + sources.add(Path(source).name) + return sources + + +def test_every_c_suite_is_built(): + suites = _c_suites() + assert suites, "expected to find test_*.c suites in tests/unit/" + + registered = _registered_sources() + missing = [name for name in suites if name not in registered] + + assert not missing, ( + "these suites exist under tests/unit/ but no add_executable() in " + f"tests/CMakeLists.txt builds them, so they never run: {missing}" + ) + + +def test_every_built_suite_is_registered_with_ctest(): + text = _cmake_text() + targets = {target for target, _sources in ADD_EXECUTABLE_RE.findall(text)} + commands = {command for _name, command in ADD_TEST_RE.findall(text)} + + unregistered = sorted(targets - commands) + assert not unregistered, ( + "these test executables are built but never added to ctest, so a " + f"failure in them cannot fail the build: {unregistered}" + ) + + +def test_fw_transport_suite_is_registered(): + """Pin the specific suite that was dropped, by name.""" + assert "test_fw_transport.c" in _registered_sources(), ( + "test_fw_transport.c is not built by tests/CMakeLists.txt -- the UART " + "transport regression suite would silently stop running again" + )