Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Child 1 of the bf-56v harness split (grandparent bf-4ne). Establishes
the public header API (firmware/test/test_runner.h) that every test unit
and the runner compile against:
- TEST(name): declares a void body plus a GCC constructor
(__attribute__((constructor))) that self-registers it via
test_register(name, fn) before main() runs — so a new test_*.c is
picked up with zero SOURCES edits.
- ASSERT_EQ / ASSERT_TRUE / ASSERT_FALSE: on mismatch, call
test_record_failure(file, line, fmt...) which longjmp()s back to the
per-test setjmp (the live jmp target lives in test_runner.c, a later
child) — aborting only the current test, not the whole suite.
- Prototypes for test_register(const char*, test_fn) and
test_record_failure(...).
Header comment records the bf-21t decision (gcc host harness, not
ESP-IDF --target linux: csi.c/provision.c are blocked by esp_wifi.h /
driver/uart.h, which have no linux build, and main is one component)
and names the single run command ('make -C firmware/test test') that a
later child makes real.
Self-contained: only libc (setjmp.h, stdbool.h, stdint.h); no
firmware/main/* includes; no main(). A trivial includer using TEST(x){}
compiles cleanly with 'gcc -std=c11 -Wall -Wextra -c'.
test_runner.c, the Makefile, and the sanity test belong to sibling
beads and are intentionally not part of this commit.
Co-Authored-By: Claude <noreply@anthropic.com>
132 lines
6.3 KiB
C
132 lines
6.3 KiB
C
/*
|
|
* ============================================================================
|
|
* Spaxel firmware host test harness — gcc runner
|
|
* ============================================================================
|
|
*
|
|
* SINGLE COMMAND TO COMPILE AND RUN (from the repo root):
|
|
*
|
|
* make -C firmware/test test
|
|
*
|
|
* That compiles every test_*.c in firmware/test/ with plain gcc and runs the
|
|
* whole suite. The runner exits non-zero if ANY assertion fails, so it is safe
|
|
* to wire into CI (make's exit code reflects the test result).
|
|
*
|
|
* ----------------------------------------------------------------------------
|
|
* WHY A PLAIN gcc HARNESS (and not ESP-IDF --target linux / Unity-host)
|
|
* ----------------------------------------------------------------------------
|
|
* the firmware/main C sources cannot be compiled or linked for the ESP-IDF `linux`
|
|
* target, so the idf.py host-test / Unity path was rejected:
|
|
*
|
|
* - csi.c pulls in esp_wifi.h — CSI callback + promiscuous-mode
|
|
* API backed by the radio MAC/baseband
|
|
* blob. No host build exists.
|
|
* - provision.c pulls in driver/uart.h — UART peripheral driver. No host
|
|
* build exists.
|
|
*
|
|
* Worse, firmware/main builds as ONE idf_component_register(...) whose
|
|
* REQUIRES line names esp_wifi, bt, and driver — three components with no
|
|
* linux build — so the whole component (and thus every translation unit in
|
|
* it) is unhostable, even nvs_migration.c, whose own includes would otherwise
|
|
* be hostable in isolation. The IDF host-test model builds whole components,
|
|
* not cherry-picked TUs, so there is no way to host-compile csi.c/wifi.c/
|
|
* websocket.c/ble.c/provision.c/led.c/ntp.c without refactoring production
|
|
* firmware purely to satisfy tests.
|
|
*
|
|
* This harness therefore does NOT #include or link the firmware/main C sources. It tests
|
|
* pure-logic extractions and binary-format contracts in self-contained units
|
|
* that compile with nothing more than a C compiler. The esp_wifi/uart/nvs call
|
|
* sites themselves remain validated on-target and via the Go-side spaxel-sim
|
|
* acceptance suite.
|
|
*
|
|
* Full decision record: docs/notes/firmware-host-test-approach.md (bead
|
|
* bf-21t). This harness is the bf-4ne build-out.
|
|
*
|
|
* ----------------------------------------------------------------------------
|
|
* ADDING A TEST
|
|
* ----------------------------------------------------------------------------
|
|
* Drop a new file firmware/test/test_<thing>.c and write:
|
|
*
|
|
* #include "test_runner.h"
|
|
*
|
|
* TEST(my_thing) {
|
|
* ASSERT_EQ(1 + 1, 2);
|
|
* }
|
|
*
|
|
* The Makefile globs test_*.c, so the new file is compiled and its TEST() is
|
|
* self-registered via a GCC constructor — no change to the runner required.
|
|
* ============================================================================
|
|
*/
|
|
#pragma once
|
|
|
|
#include <setjmp.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
|
|
/* ---- Per-test self-registration ----------------------------------------- */
|
|
|
|
typedef void (*test_fn)(void);
|
|
|
|
typedef struct {
|
|
const char *name;
|
|
test_fn fn;
|
|
} test_entry_t;
|
|
|
|
/*
|
|
* Append a test to the global registry. Called from constructor functions
|
|
* emitted by the TEST() macro, so the list is fully populated before main().
|
|
*/
|
|
void test_register(const char *name, test_fn fn);
|
|
|
|
/*
|
|
* Define a self-registering test. Expands to a static body function plus a
|
|
* constructor that registers it.
|
|
*
|
|
* TEST(addition) { ASSERT_EQ(1 + 1, 2); }
|
|
*/
|
|
#define TEST(name) \
|
|
static void spaxel_test_##name##_body(void); \
|
|
static void spaxel_test_##name##_reg(void) __attribute__((constructor)); \
|
|
static void spaxel_test_##name##_reg(void) { \
|
|
test_register(#name, spaxel_test_##name##_body); \
|
|
} \
|
|
static void spaxel_test_##name##_body(void)
|
|
|
|
/* ---- Assertions --------------------------------------------------------- */
|
|
/*
|
|
* On failure these record the location, mark the suite failed, and longjmp out
|
|
* of the CURRENT test only — the remaining tests still run. main() returns
|
|
* non-zero if any assertion has failed across the whole run.
|
|
*/
|
|
|
|
void test_record_failure(const char *file, int line, const char *fmt, ...);
|
|
|
|
#define ASSERT_TRUE(cond) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
test_record_failure(__FILE__, __LINE__, \
|
|
"ASSERT_TRUE(%s) failed", #cond); \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_FALSE(cond) \
|
|
do { \
|
|
if ((cond)) { \
|
|
test_record_failure(__FILE__, __LINE__, \
|
|
"ASSERT_FALSE(%s) failed", #cond); \
|
|
} \
|
|
} while (0)
|
|
|
|
/*
|
|
* Integer-equality assertion (actual/expected are evaluated once each and
|
|
* compared as long). Use ASSERT_TRUE for floats, strings, or pointers.
|
|
*/
|
|
#define ASSERT_EQ(actual, expected) \
|
|
do { \
|
|
long _spaxel_a = (long)(actual); \
|
|
long _spaxel_e = (long)(expected); \
|
|
if (_spaxel_a != _spaxel_e) { \
|
|
test_record_failure(__FILE__, __LINE__, \
|
|
"ASSERT_EQ(%s, %s) failed: got %ld, want %ld", \
|
|
#actual, #expected, _spaxel_a, _spaxel_e); \
|
|
} \
|
|
} while (0)
|