From c66f62e616cfc905a480f4f75d65626328ea56a1 Mon Sep 17 00:00:00 2001 From: jedarden Date: Fri, 3 Jul 2026 11:56:34 -0400 Subject: [PATCH] test: land firmware host-test build scaffolding (Makefile, .gitignore, sanity) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- firmware/test/.gitignore | 3 ++ firmware/test/Makefile | 58 +++++++++++++++++++++++++++++++++++++ firmware/test/test_sanity.c | 23 +++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 firmware/test/.gitignore create mode 100644 firmware/test/Makefile create mode 100644 firmware/test/test_sanity.c diff --git a/firmware/test/.gitignore b/firmware/test/.gitignore new file mode 100644 index 0000000..aac261b --- /dev/null +++ b/firmware/test/.gitignore @@ -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/ diff --git a/firmware/test/Makefile b/firmware/test/Makefile new file mode 100644 index 0000000..bac8a83 --- /dev/null +++ b/firmware/test/Makefile @@ -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_.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) diff --git a/firmware/test/test_sanity.c b/firmware/test/test_sanity.c new file mode 100644 index 0000000..19c3ef7 --- /dev/null +++ b/firmware/test/test_sanity.c @@ -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); +}