test: land firmware host-test build scaffolding (Makefile, .gitignore, sanity)
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run

The per-test setjmp/longjmp recovery loop in test_runner.c (main()) was
already delivered by sibling bead bf-bq9 (commit 549dc1f) and verified to
satisfy bf-53ut's contract: a failing assertion longjmps back to main()'s
setjmp, which falls through to advance the loop, so a failure in test N
never blocks tests N+1..end (proven with a temp failing-test followed by a
passing test — both ran and the process completed without aborting).

This commit lands the three harness files that were staged but absent from
HEAD (Makefile, .gitignore, test_sanity.c) so that 'make -C firmware/test
test' — the command bf-53ut's acceptance invokes — actually builds and runs
against the committed tree. Without them the committed test_runner.c could
not be built from a fresh clone.

Verified against the bf-53ut acceptance criteria:
- gcc -std=c11 -Wall -Wextra (-Werror): clean; no -Wclobbered/setjmp warnings
- normal suite: PASS, exit 0
- failing test + later passing test: both per-test lines print, process
  completes (non-fatal assertions)
- no changes to firmware/main/* or firmware/CMakeLists.txt

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-07-03 11:56:34 -04:00
parent 549dc1f179
commit c66f62e616
3 changed files with 84 additions and 0 deletions

3
firmware/test/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
# Build output of `make -C firmware/test test` (BUILD_DIR = build).
# The runner binary and object artifacts land here; never tracked.
/build/

58
firmware/test/Makefile Normal file
View file

@ -0,0 +1,58 @@
# ============================================================================
# Spaxel firmware host test harness — Makefile
# ============================================================================
#
# Build & run the whole suite, in one command, from the repo root:
#
# make -C firmware/test test
#
# That compiles every test_*.c in this directory together with test_runner.c
# using plain gcc (no ESP-IDF toolchain) and runs the resulting binary. The
# runner returns non-zero if ANY assertion fails, and make propagates that exit
# code, so the command is safe to wire straight into CI.
#
# See test_runner.h for the gcc-harness rationale (decision record: bead bf-21t;
# this build-out is the split of parent bead bf-4ne into child bf-56v).
#
# Adding a test: drop firmware/test/test_<thing>.c that uses the TEST() macro
# from test_runner.h. The wildcard below picks it up automatically; its TEST()
# self-registers via a GCC constructor — no SOURCES edit, no runner change.
# ============================================================================
CC ?= gcc
CFLAGS ?= -std=c11 -Wall -Wextra -O2 -g
LDFLAGS ?=
# Every self-registering test unit, plus the runner that holds main(). The
# wildcard means a new test_*.c is compiled and linked with zero edits here.
#
# test_runner.c is itself matched by the test_*.c glob, so it is filtered out of
# the test set and added back exactly once as RUNNER_SRC — otherwise main() and
# the registry functions would be defined twice and the link would fail.
RUNNER_SRC := test_runner.c
TEST_SRCS := $(filter-out $(RUNNER_SRC), $(wildcard test_*.c))
ALL_SRCS := $(TEST_SRCS) $(RUNNER_SRC)
BUILD_DIR := build
BIN := $(BUILD_DIR)/spaxel_host_tests
# Object files (not archives) are linked directly, so every test_*.c's TEST()
# constructor is retained and runs at load — no --whole-archive needed. Only
# unused *archive* members get dropped by the linker, never direct .o files.
.PHONY: test build run clean
# Default goal: the documented single command. `build` produces the binary,
# `run` executes it; make returns the binary's exit code, so failures surface.
test: build run
build: $(BIN)
$(BIN): $(ALL_SRCS) test_runner.h
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(ALL_SRCS) -o $@ $(LDFLAGS)
run: $(BIN)
./$(BIN)
clean:
rm -rf $(BUILD_DIR)

View file

@ -0,0 +1,23 @@
/*
* Sanity test for the Spaxel firmware host test harness.
*
* This is the minimal passing test that proves the harness wires up end to end:
* the TEST() macro self-registers via its GCC constructor, the runner drives the
* registered test, and ASSERT_EQ reports correctly.
*
* Real module tests (nvs/csi/prov logic extractions + binary-format contracts)
* are deliberately NOT here they are added by the sibling beads that follow.
* Keeping this bead (bf-56v) to scaffolding + a single sanity test is what lets
* the "compiles + runs + exits non-zero on failure" contract be verified in
* isolation before any module behavior is exercised.
*/
#include "test_runner.h"
/*
* 1 + 1 == 2: the smallest possible assertion. If this fails, the harness itself
* is broken, not the firmware logic under test.
*/
TEST(arithmetic_sanity)
{
ASSERT_EQ(1 + 1, 2);
}