/* * test_baro.c — Unit tests for the baro module (Issue #672). * * Build (host, no hardware): * gcc -I include -I test/stubs -DTEST_HOST -lm \ * -o /tmp/test_baro src/baro.c test/test_baro.c * * All tests are self-contained; no HAL, no I2C, no UART required. * bmp280_init/read/humidity/alt and jlink_send_baro_tlm are stubbed below. */ /* ---- Stubs ---- */ /* Prevent jlink.h from pulling in HAL / pid_flash types */ #define JLINK_H #include #include /* Minimal jlink_tlm_baro_t matching the real definition */ typedef struct __attribute__((packed)) { int32_t pressure_pa; int16_t temp_x10; int32_t alt_cm; int16_t humidity_pct_x10; } jlink_tlm_baro_t; /* Capture last transmitted baro tlm for inspection */ static jlink_tlm_baro_t g_last_baro_tlm; static int g_baro_tlm_count = 0; void jlink_send_baro_tlm(const jlink_tlm_baro_t *tlm) { g_last_baro_tlm = *tlm; g_baro_tlm_count++; } /* Stub bmp280_read() — returns configurable values */ static int32_t g_stub_pressure_pa = 101325; static int16_t g_stub_temp_x10 = 230; /* 23.0 °C */ void bmp280_read(int32_t *pressure_pa, int16_t *temp_x10) { *pressure_pa = g_stub_pressure_pa; *temp_x10 = g_stub_temp_x10; } /* Stub bmp280_read_humidity() */ static int16_t g_stub_humidity = 500; /* 50.0 %RH */ int16_t bmp280_read_humidity(void) { return g_stub_humidity; } /* Stub bmp280_pressure_to_alt_cm() — proportional approx for testing */ int32_t bmp280_pressure_to_alt_cm(int32_t pressure_pa) { /* Simplified: sea level = 101325 Pa = 0 cm; decrease of 12 Pa ≈ 100 cm */ return (int32_t)((101325 - pressure_pa) * 100 / 12); } /* ---- Include implementation directly ---- */ #include "../src/baro.c" /* ---- Test framework ---- */ #include #include #include static int g_pass = 0; static int g_fail = 0; #define ASSERT(cond, msg) do { \ if (cond) { g_pass++; } \ else { g_fail++; printf("FAIL [%s:%d] %s\n", __FILE__, __LINE__, msg); } \ } while(0) /* ---- Helper ---- */ static void reset_stubs(void) { g_stub_pressure_pa = 101325; g_stub_temp_x10 = 230; g_stub_humidity = 500; g_baro_tlm_count = 0; memset(&g_last_baro_tlm, 0, sizeof(g_last_baro_tlm)); } /* ---- Tests ---- */ static void test_init_absent_chip_no_read(void) { reset_stubs(); baro_init(0); /* chip absent */ /* baro_tick must be a no-op */ baro_tick(1000u); ASSERT(g_baro_tlm_count == 0, "absent chip: no telemetry sent"); baro_data_t d; ASSERT(!baro_get(&d), "absent chip: baro_get returns false"); ASSERT(baro_get_alt_cm() == 0, "absent chip: baro_get_alt_cm returns 0"); } static void test_first_tick_fires_immediately(void) { /* * After baro_init() timestamps are pre-wound so that the very first * baro_tick() triggers a read and a telemetry send. */ reset_stubs(); baro_init(0x60); /* BME280 */ baro_tick(1000u); ASSERT(g_baro_tlm_count == 1, "first tick sends telemetry immediately"); baro_data_t d; ASSERT(baro_get(&d), "first tick: data valid"); } static void test_data_populated_after_tick(void) { reset_stubs(); g_stub_pressure_pa = 100000; g_stub_temp_x10 = 150; /* 15.0 °C */ g_stub_humidity = 400; /* 40.0 %RH */ baro_init(0x60); baro_tick(0u); baro_data_t d; ASSERT(baro_get(&d), "data valid after tick"); ASSERT(d.pressure_pa == 100000, "pressure_pa correct"); ASSERT(d.temp_x10 == 150, "temp_x10 correct"); ASSERT(d.humidity_pct_x10 == 400, "humidity correct (BME280)"); ASSERT(d.valid, "valid flag set"); } static void test_altitude_computed(void) { reset_stubs(); g_stub_pressure_pa = 101325 - 120; /* 120 Pa below sea level → ~1000 cm */ baro_init(0x58); /* BMP280 */ baro_tick(0u); baro_data_t d; baro_get(&d); /* stub: alt_cm = (101325 - 101205)*100/12 = 12000/12 = 1000 cm */ ASSERT(d.alt_cm == 1000, "altitude computed correctly"); ASSERT(baro_get_alt_cm() == 1000, "baro_get_alt_cm matches"); } static void test_bmp280_no_humidity(void) { reset_stubs(); baro_init(0x58); /* BMP280 — no humidity */ baro_tick(0u); baro_data_t d; baro_get(&d); ASSERT(d.humidity_pct_x10 == (int16_t)-1, "BMP280: humidity_pct_x10 == -1"); } static void test_bme280_has_humidity(void) { reset_stubs(); g_stub_humidity = 650; baro_init(0x60); /* BME280 */ baro_tick(0u); baro_data_t d; baro_get(&d); ASSERT(d.humidity_pct_x10 == 650, "BME280: humidity_pct_x10 populated"); } static void test_rate_limited_1hz(void) { /* * At BARO_READ_HZ = 1, only one read per second. * Call baro_tick() every ms for 3000 ms → expect 3 sends. */ reset_stubs(); baro_init(0x60); for (uint32_t ms = 0; ms < 3000u; ms++) { baro_tick(ms); } ASSERT(g_baro_tlm_count == 3, "1 Hz rate limit: 3 sends in 3000 ms"); } static void test_no_tlm_before_interval(void) { reset_stubs(); baro_init(0x60); /* First tick at ms=0 fires (pre-wound). Next must not fire until ms=1000. */ baro_tick(0u); ASSERT(g_baro_tlm_count == 1, "first tick sends"); baro_tick(500u); /* only 500 ms later — must not send again */ ASSERT(g_baro_tlm_count == 1, "second tick before interval: no extra send"); } static void test_tlm_payload_encoding(void) { reset_stubs(); g_stub_pressure_pa = 95000; g_stub_temp_x10 = -50; /* -5.0 °C */ g_stub_humidity = 900; baro_init(0x60); baro_tick(0u); ASSERT(g_baro_tlm_count == 1, "TLM sent"); ASSERT(g_last_baro_tlm.pressure_pa == 95000, "TLM: pressure_pa encoded"); ASSERT(g_last_baro_tlm.temp_x10 == (int16_t)-50, "TLM: negative temp encoded"); ASSERT(g_last_baro_tlm.humidity_pct_x10 == 900, "TLM: humidity encoded"); /* alt_cm via stub: (101325-95000)*100/12 = 632500/12 ≈ 52708 */ ASSERT(g_last_baro_tlm.alt_cm == g_last_baro_tlm.alt_cm, /* sanity */ "TLM: alt_cm field present"); /* verify alt matches baro_get */ baro_data_t d; baro_get(&d); ASSERT(g_last_baro_tlm.alt_cm == d.alt_cm, "TLM: alt_cm matches baro_get"); } static void test_get_returns_false_before_first_tick(void) { reset_stubs(); baro_init(0x60); /* Trick: call with a time that won't fire yet by advancing only 1 ms * BUT init pre-winds timestamps so tick at 0 fires. Test the initial * state by checking valid flag before any tick. */ /* We can observe this by checking the internal s_data.valid = false * by attempting a get on a freshly reinitialised module. * We simulate by re-init'ing with chip=0 so tick is no-op. */ baro_init(0); /* chip absent — tick will never fire */ baro_data_t d; ASSERT(!baro_get(&d), "no tick yet: baro_get returns false"); } static void test_get_alt_zero_before_valid(void) { reset_stubs(); baro_init(0); /* no chip → never valid */ ASSERT(baro_get_alt_cm() == 0, "alt_cm == 0 when no valid data"); } static void test_data_updates_each_interval(void) { /* * Change stub pressure between ticks. * Verify baro_get() reflects the latest reading. */ reset_stubs(); baro_init(0x58); g_stub_pressure_pa = 101325; baro_tick(0u); baro_data_t d1; baro_get(&d1); g_stub_pressure_pa = 100000; baro_tick(1000u); /* one interval later */ baro_data_t d2; baro_get(&d2); ASSERT(d1.pressure_pa == 101325, "first reading correct"); ASSERT(d2.pressure_pa == 100000, "second reading updated"); ASSERT(d1.alt_cm != d2.alt_cm, "alt_cm updates between reads"); } static void test_reinit_clears_valid(void) { reset_stubs(); baro_init(0x60); baro_tick(0u); baro_data_t d; ASSERT(baro_get(&d), "data valid after tick"); /* Re-init with absent chip — valid flag cleared */ baro_init(0); ASSERT(!baro_get(&d), "after re-init (absent): valid cleared"); ASSERT(baro_get_alt_cm() == 0, "after re-init (absent): alt == 0"); } /* ---- main ---- */ int main(void) { printf("=== test_baro ===\n"); test_init_absent_chip_no_read(); test_first_tick_fires_immediately(); test_data_populated_after_tick(); test_altitude_computed(); test_bmp280_no_humidity(); test_bme280_has_humidity(); test_rate_limited_1hz(); test_no_tlm_before_interval(); test_tlm_payload_encoding(); test_get_returns_false_before_first_tick(); test_get_alt_zero_before_valid(); test_data_updates_each_interval(); test_reinit_clears_valid(); printf("\nResults: %d passed, %d failed\n", g_pass, g_fail); return g_fail ? 1 : 0; }