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); +}