fix: propagate SPI/ADC communication failures in ADAR1000_Manager

This commit is contained in:
SoufianeBouaddis
2026-04-30 01:22:50 +01:00
parent 89e688e9a2
commit 23c5f82753
11 changed files with 1347 additions and 375 deletions
+18 -1
View File
@@ -79,10 +79,17 @@ TESTS_WITH_PLATFORM := test_bug11_platform_spi_transmit_only
# C++ tests (AGC outer loop)
TESTS_WITH_CXX := test_agc_outer_loop
# ADAR1000 error/status propagation tests -- link real ADAR1000_Manager.o + mocks
TESTS_ADAR_STATUS := test_adar_init_aborts_on_scratchpad_mismatch \
test_adar_spi_write_failure_propagates \
test_adar_adc_timeout_returns_nan \
test_adar_mode_switch_does_not_lie \
test_adar_comm_stats_increment
# GPS driver tests (need mocks + GPS source + -lm)
TESTS_GPS := test_um982_gps
ALL_TESTS := $(TESTS_WITH_REAL) $(TESTS_MOCK_ONLY) $(TESTS_STANDALONE) $(TESTS_WITH_PLATFORM) $(TESTS_WITH_CXX) $(TESTS_GPS)
ALL_TESTS := $(TESTS_WITH_REAL) $(TESTS_MOCK_ONLY) $(TESTS_STANDALONE) $(TESTS_WITH_PLATFORM) $(TESTS_WITH_CXX) $(TESTS_ADAR_STATUS) $(TESTS_GPS)
.PHONY: all build test clean \
$(addprefix test_,bug1 bug2 bug3 bug4 bug5 bug6 bug7 bug8 bug9 bug10 bug11 bug12 bug13 bug14 bug15) \
@@ -204,6 +211,16 @@ test_agc_outer_loop: test_agc_outer_loop.cpp $(CXX_OBJS) $(MOCK_OBJS)
test_agc: test_agc_outer_loop
./test_agc_outer_loop
# --- ADAR1000 status-propagation test rules ---
# Each test links the real ADAR1000_Manager.cpp (compiled as ADAR1000_Manager.o)
# against the HAL mock so failure injection drives the production code paths.
$(TESTS_ADAR_STATUS): %: %.cpp ADAR1000_Manager.o $(MOCK_OBJS)
$(CXX) $(CXXFLAGS) $(INCLUDES) $< ADAR1000_Manager.o $(MOCK_OBJS) -o $@
.PHONY: test_adar_status
test_adar_status: $(TESTS_ADAR_STATUS)
@for t in $(TESTS_ADAR_STATUS); do echo "--- $$t ---"; ./$$t || exit 1; done
# --- GPS driver rules ---
$(GPS_OBJ): $(GPS_SRC)
@@ -63,6 +63,12 @@ static struct {
GPIO_PinState val;
} gpio_read_table[GPIO_READ_TABLE_SIZE];
/* SPI failure-injection state */
static int mock_spi_fail_remaining = 0;
static HAL_StatusTypeDef mock_spi_fail_status = HAL_OK;
static uint8_t mock_spi_rx_byte = 0;
static uint32_t mock_tick_auto_advance = 0;
void spy_reset(void)
{
spy_count = 0;
@@ -73,6 +79,26 @@ void spy_reset(void)
memset(mock_uart_rx, 0, sizeof(mock_uart_rx));
mock_uart_tx_len = 0;
memset(mock_uart_tx_buf, 0, sizeof(mock_uart_tx_buf));
mock_spi_fail_remaining = 0;
mock_spi_fail_status = HAL_OK;
mock_spi_rx_byte = 0;
mock_tick_auto_advance = 0;
}
void mock_spi_queue_failure(int call_count, HAL_StatusTypeDef status)
{
mock_spi_fail_remaining = call_count;
mock_spi_fail_status = status;
}
void mock_spi_set_rx_byte(uint8_t value)
{
mock_spi_rx_byte = value;
}
void mock_set_tick_auto_advance(uint32_t delta)
{
mock_tick_auto_advance = delta;
}
const SpyRecord *spy_get(int index)
@@ -177,14 +203,16 @@ void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
uint32_t HAL_GetTick(void)
{
uint32_t result = mock_tick;
spy_push((SpyRecord){
.type = SPY_HAL_GET_TICK,
.port = NULL,
.pin = 0,
.value = mock_tick,
.value = result,
.extra = NULL
});
return mock_tick;
mock_tick += mock_tick_auto_advance;
return result;
}
void HAL_Delay(uint32_t Delay)
@@ -369,6 +397,7 @@ void mock_tim_set_compare(TIM_HandleTypeDef *htim, uint32_t Channel, uint32_t Co
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout)
{
(void)pTxData;
spy_push((SpyRecord){
.type = SPY_SPI_TRANSMIT_RECEIVE,
.port = NULL,
@@ -376,11 +405,19 @@ HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxD
.value = Timeout,
.extra = hspi
});
if (mock_spi_fail_remaining > 0) {
mock_spi_fail_remaining--;
return mock_spi_fail_status;
}
if (pRxData && Size > 0) {
pRxData[Size - 1] = mock_spi_rx_byte;
}
return HAL_OK;
}
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
(void)pData;
spy_push((SpyRecord){
.type = SPY_SPI_TRANSMIT,
.port = NULL,
@@ -388,6 +425,10 @@ HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint
.value = Timeout,
.extra = hspi
});
if (mock_spi_fail_remaining > 0) {
mock_spi_fail_remaining--;
return mock_spi_fail_status;
}
return HAL_OK;
}
@@ -176,6 +176,11 @@ void mock_set_tick(uint32_t tick);
/* Advance the mock tick by `delta` ms */
void mock_advance_tick(uint32_t delta);
/* Each subsequent HAL_GetTick() call advances the mock tick by `delta` ms after
* returning the current value. Use to drive timeout loops without wall-clock
* waits. Set to 0 (default) for stable tick. */
void mock_set_tick_auto_advance(uint32_t delta);
/* ========================= Mock GPIO read returns ================= */
/* Set the value HAL_GPIO_ReadPin will return for a specific port/pin */
@@ -212,6 +217,15 @@ void mock_uart_tx_clear(void);
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
/* Queue the next N SPI calls (Transmit and TransmitReceive) to return `status`
* instead of HAL_OK. Decremented on each call. Use for failure-propagation tests. */
void mock_spi_queue_failure(int call_count, HAL_StatusTypeDef status);
/* Set the byte that HAL_SPI_TransmitReceive will write at pRxData[Size-1] for
* subsequent calls. ADAR1000 reads land at index 2 of a 3-byte transfer, so
* this lets tests inject scratchpad / register readback values. */
void mock_spi_set_rx_byte(uint8_t value);
/* ========================= no_os compat layer ===================== */
void no_os_udelay(uint32_t usecs);
@@ -0,0 +1,115 @@
// test_adar_adc_timeout_returns_nan.cpp
//
// readTemperature() previously returned -50.0 C silently when the on-chip
// ADC never completed a conversion (raw=0 timeout sentinel mapped through
// (raw * 0.5) - 50). A hung chip looked like a cold radar.
//
// New: on ADC timeout or any comm failure inside adarAdcRead(),
// readTemperature returns NaN, and comm_stats_.adc_timeouts increments.
// checkSystemHealth() in main.cpp uses isnan() to route NaN to the comm-error
// bucket (which triggers attemptErrorRecovery).
#include <cassert>
#include <cmath>
#include <cstdio>
#include "stm32_hal_mock.h"
#include "ADAR1000_Manager.h"
uint8_t GUI_start_flag_received = 0;
uint8_t USB_Buffer[64] = {0};
extern "C" void Error_Handler(void) {}
static int tests_passed = 0;
static int tests_total = 0;
#define RUN_TEST(fn) \
do { \
tests_total++; \
printf(" [%2d] %-65s ", tests_total, #fn); \
fn(); \
tests_passed++; \
printf("PASS\n"); \
} while (0)
// Helper: bring all 4 devices through init successfully so readTemperature()
// gets past its "not initialized" guard.
static void init_devices_clean(ADAR1000Manager& mgr)
{
mock_spi_set_rx_byte(0xA5);
bool ok = mgr.initializeAllDevices();
assert(ok);
}
// Default mock returns 0x00 to every read after init. Since bit 0 of the ADC
// status register never goes high, the polling loop in adarAdcRead is supposed
// to time out after 100 ms. Auto-advancing the tick on every HAL_GetTick()
// drives that timer forward without wall-clock waits.
static void test_polling_timeout_returns_nan()
{
spy_reset();
ADAR1000Manager mgr;
init_devices_clean(mgr);
// Switch the rx-byte back to 0x00 so the ADC status bit stays low forever
// and the polling loop runs to its 100 ms watchdog.
mock_spi_set_rx_byte(0x00);
mock_set_tick_auto_advance(150); // each HAL_GetTick() advances 150 ms
mgr.resetCommStats();
float t = mgr.readTemperature(0);
assert(std::isnan(t));
const auto& stats = mgr.getCommStats();
assert(stats.adc_timeouts >= 1);
}
// SPI failure during adarAdcRead's start-conversion write must also count as
// a timeout (caller has no valid ADC reading) and produce NaN.
static void test_start_conv_spi_failure_returns_nan()
{
spy_reset();
ADAR1000Manager mgr;
init_devices_clean(mgr);
mgr.resetCommStats();
// Fail the very next SPI call -- the start-conversion write inside
// adarAdcRead. Following polling reads will also fail, but the function
// bails on the start-conv write first.
mock_spi_queue_failure(100, HAL_ERROR);
float t = mgr.readTemperature(0);
assert(std::isnan(t));
const auto& stats = mgr.getCommStats();
assert(stats.adc_timeouts >= 1);
}
// Healthy path: ADC bit 0 is high on the first poll (rx_byte = 0xA5 after init),
// so adarAdcRead exits the loop immediately, and readTemperature returns a
// finite number. This guards against false-positive NaN from the new code path.
static void test_healthy_adc_returns_finite_temp()
{
spy_reset();
ADAR1000Manager mgr;
init_devices_clean(mgr);
mgr.resetCommStats();
float t = mgr.readTemperature(0);
assert(!std::isnan(t));
const auto& stats = mgr.getCommStats();
assert(stats.adc_timeouts == 0);
}
int main()
{
printf("=== ADAR1000 ADC timeout -> NaN propagation tests ===\n");
RUN_TEST(test_polling_timeout_returns_nan);
RUN_TEST(test_start_conv_spi_failure_returns_nan);
RUN_TEST(test_healthy_adc_returns_finite_temp);
printf("=== Results: %d/%d passed ===\n", tests_passed, tests_total);
return (tests_passed == tests_total) ? 0 : 1;
}
@@ -0,0 +1,191 @@
// test_adar_comm_stats_increment.cpp
//
// Documents and locks the CommStats observability surface added in this PR.
// Without this test, a future "simplification" could quietly remove the
// counters and nothing else would catch it.
//
// Contract:
// - Every successful adarWrite increments writes_ok.
// - Every failed adarWrite increments writes_fail and updates last_fail_dev.
// - Every successful adarRead increments reads_ok.
// - Every failed adarRead increments reads_fail and updates last_fail_dev.
// - resetCommStats() zeroes all counters and resets last_fail_dev to 0xFF.
// - PR2 may promote a richer OpStatus enum return type; this struct is the
// forward-compatible observability hook either way.
#include <cassert>
#include <cstdio>
#include "stm32_hal_mock.h"
#include "ADAR1000_Manager.h"
uint8_t GUI_start_flag_received = 0;
uint8_t USB_Buffer[64] = {0};
extern "C" void Error_Handler(void) {}
static int tests_passed = 0;
static int tests_total = 0;
#define RUN_TEST(fn) \
do { \
tests_total++; \
printf(" [%2d] %-65s ", tests_total, #fn); \
fn(); \
tests_passed++; \
printf("PASS\n"); \
} while (0)
static void test_default_stats_are_zero()
{
spy_reset();
ADAR1000Manager mgr;
const auto& s = mgr.getCommStats();
assert(s.writes_ok == 0);
assert(s.writes_fail == 0);
assert(s.reads_ok == 0);
assert(s.reads_fail == 0);
assert(s.adc_timeouts == 0);
assert(s.last_fail_dev == 0xFF);
}
static void test_successful_write_increments_writes_ok()
{
spy_reset();
ADAR1000Manager mgr;
mgr.resetCommStats();
bool ok = mgr.writeRegister(0, 0x010, 0x42);
assert(ok);
const auto& s = mgr.getCommStats();
assert(s.writes_ok == 1);
assert(s.writes_fail == 0);
assert(s.last_fail_dev == 0xFF);
}
static void test_failed_write_increments_writes_fail_and_records_dev()
{
spy_reset();
ADAR1000Manager mgr;
mgr.resetCommStats();
mock_spi_queue_failure(1, HAL_ERROR);
bool ok = mgr.writeRegister(2, 0x010, 0x42); // dev[2]
assert(ok == false);
const auto& s = mgr.getCommStats();
assert(s.writes_ok == 0);
assert(s.writes_fail == 1);
assert(s.last_fail_dev == 2);
}
static void test_successful_read_increments_reads_ok()
{
spy_reset();
mock_spi_set_rx_byte(0xA5);
ADAR1000Manager mgr;
mgr.resetCommStats();
uint8_t value = 0;
bool ok = mgr.adarReadChecked(1, 0x010, &value);
assert(ok);
assert(value == 0xA5);
const auto& s = mgr.getCommStats();
assert(s.reads_ok == 1);
assert(s.reads_fail == 0);
}
static void test_failed_read_increments_reads_fail_and_records_dev()
{
spy_reset();
ADAR1000Manager mgr;
mgr.resetCommStats();
// adarReadChecked sequence:
// 1. adarWrite(SDO active) -- HAL_SPI_Transmit
// 2. HAL_SPI_TransmitReceive (the actual read) <-- we want this to fail
// 3. adarWrite(SDO inactive)
// Letting calls 1+2 fail (queue 2 failures) covers the case where SDO-active
// also fails -- adarReadChecked aborts after #1 and bumps reads_fail.
mock_spi_queue_failure(2, HAL_ERROR);
uint8_t value = 0xCC; // pre-poison to confirm out param gets cleared
bool ok = mgr.adarReadChecked(3, 0x010, &value);
assert(ok == false);
assert(value == 0); // adarReadChecked must zero the out param on failure
const auto& s = mgr.getCommStats();
assert(s.reads_fail >= 1);
assert(s.last_fail_dev == 3);
}
// Reset must clear everything to defaults, including last_fail_dev back to 0xFF.
static void test_reset_clears_all_counters()
{
spy_reset();
ADAR1000Manager mgr;
// Generate some non-zero state in every field.
mgr.writeRegister(0, 0x010, 0x42); // writes_ok++
mock_spi_queue_failure(1, HAL_ERROR);
mgr.writeRegister(1, 0x010, 0x42); // writes_fail++, last_fail_dev=1
mock_spi_set_rx_byte(0xA5);
uint8_t v = 0;
mgr.adarReadChecked(0, 0x010, &v); // reads_ok++
{
const auto& s = mgr.getCommStats();
assert(s.writes_ok > 0);
assert(s.writes_fail > 0);
assert(s.reads_ok > 0);
assert(s.last_fail_dev != 0xFF);
}
mgr.resetCommStats();
const auto& s = mgr.getCommStats();
assert(s.writes_ok == 0);
assert(s.writes_fail == 0);
assert(s.reads_ok == 0);
assert(s.reads_fail == 0);
assert(s.adc_timeouts == 0);
assert(s.last_fail_dev == 0xFF);
}
// Out-of-range device index counts as a write/read failure (not a silent skip).
// This is the kind of bug that would otherwise hide behind a "device 4 ignored"
// log line and never escalate to the caller.
static void test_out_of_range_device_index_counts_as_failure()
{
spy_reset();
ADAR1000Manager mgr;
mgr.resetCommStats();
bool wok = mgr.writeRegister(99, 0x010, 0x42);
assert(wok == false);
assert(mgr.getCommStats().writes_fail == 1);
assert(mgr.getCommStats().last_fail_dev == 99);
uint8_t v = 0;
bool rok = mgr.adarReadChecked(99, 0x010, &v);
assert(rok == false);
assert(mgr.getCommStats().reads_fail == 1);
}
int main()
{
printf("=== ADAR1000 CommStats observability tests ===\n");
RUN_TEST(test_default_stats_are_zero);
RUN_TEST(test_successful_write_increments_writes_ok);
RUN_TEST(test_failed_write_increments_writes_fail_and_records_dev);
RUN_TEST(test_successful_read_increments_reads_ok);
RUN_TEST(test_failed_read_increments_reads_fail_and_records_dev);
RUN_TEST(test_reset_clears_all_counters);
RUN_TEST(test_out_of_range_device_index_counts_as_failure);
printf("=== Results: %d/%d passed ===\n", tests_passed, tests_total);
return (tests_passed == tests_total) ? 0 : 1;
}
@@ -0,0 +1,104 @@
// test_adar_init_aborts_on_scratchpad_mismatch.cpp
//
// previously initializeSingleDevice() logged a
// warning when the scratchpad readback didn't match 0xA5 but still set
// devices_[i]->initialized = true and returned true. That meant
// initializeAllDevices() would happily report success with four dead chips.
//
// New: any scratchpad mismatch aborts init. The device stays
// uninitialized, the function returns false, and downstream calls (e.g.
// readTemperature) return the "not initialized" sentinel -273.15.
#include <cassert>
#include <cmath>
#include <cstdio>
#include "stm32_hal_mock.h"
#include "ADAR1000_Manager.h"
uint8_t GUI_start_flag_received = 0;
uint8_t USB_Buffer[64] = {0};
extern "C" void Error_Handler(void) {}
static int tests_passed = 0;
static int tests_total = 0;
#define RUN_TEST(fn) \
do { \
tests_total++; \
printf(" [%2d] %-65s ", tests_total, #fn); \
fn(); \
tests_passed++; \
printf("PASS\n"); \
} while (0)
// With default mock state, HAL_SPI_TransmitReceive writes 0x00 into rx_buffer[2].
// The scratchpad write programs 0xA5; the readback gets 0x00; mismatch -> abort.
static void test_scratchpad_mismatch_aborts_init()
{
spy_reset();
ADAR1000Manager mgr;
bool ok = mgr.initializeAllDevices();
assert(ok == false); // would have been true under the old bug
// Downstream proof: readTemperature must report "not initialized" sentinel,
// not a temperature value, because the device is not marked initialized.
float t = mgr.readTemperature(0);
assert(t == -273.15f);
}
// When scratchpad readback matches, init succeeds. mock_spi_set_rx_byte(0xA5)
// makes every read return 0xA5, satisfying the verify step.
static void test_scratchpad_match_lets_init_succeed()
{
spy_reset();
mock_spi_set_rx_byte(0xA5);
ADAR1000Manager mgr;
bool ok = mgr.initializeAllDevices();
assert(ok == true);
// Now temperature read should produce a real number (not -273.15 sentinel).
// adarAdcRead loops on bit 0 of REG_ADC_CONTROL; with rx_byte=0xA5 (bit 0 = 1)
// the loop exits immediately, then REG_ADC_OUT also reads 0xA5.
float t = mgr.readTemperature(0);
assert(!std::isnan(t));
assert(t != -273.15f);
}
// Init aborts on the first device that fails. Stats reflect partial progress
// (some writes_ok before the abort) which is the trend signal callers will
// query via getCommStats().
static void test_init_failure_recorded_in_stats()
{
spy_reset();
ADAR1000Manager mgr;
mgr.resetCommStats();
bool ok = mgr.initializeAllDevices();
assert(ok == false);
const auto& stats = mgr.getCommStats();
// Several writes happened before scratchpad verify failed: soft reset,
// configA, RAM bypass, ADC enable, scratchpad-write itself, and the
// SDO-active toggles inside the scratchpad-read.
assert(stats.writes_ok > 0);
// The scratchpad readback succeeded at the SPI layer (HAL_OK) but produced
// a wrong value -- that's not a read failure, it's a verify mismatch. So
// reads_fail can stay 0 in the pure-mismatch case.
}
int main()
{
printf("=== ADAR1000 init scratchpad-mismatch propagation tests ===\n");
RUN_TEST(test_scratchpad_mismatch_aborts_init);
RUN_TEST(test_scratchpad_match_lets_init_succeed);
RUN_TEST(test_init_failure_recorded_in_stats);
printf("=== Results: %d/%d passed ===\n", tests_passed, tests_total);
return (tests_passed == tests_total) ? 0 : 1;
}
@@ -0,0 +1,113 @@
// test_adar_mode_switch_does_not_lie.cpp
//
// setAllDevicesTXMode / setAllDevicesRXMode previously updated current_mode_
// (both global and per-device) before issuing the SPI writes that actually
// reconfigure the chip, then returned true unconditionally. A SPI failure
// during the mode switch left software believing it was in TX mode while the
// hardware was still in RX (or vice-versa) -- a real safety hazard given
// that PA biasing is mode-dependent.
//
// New: current_mode_ only updates if every underlying write
// succeeded; otherwise the function returns false and the mode flag is left
// at its last-known-good value.
#include <cassert>
#include <cstdio>
#include "stm32_hal_mock.h"
#include "ADAR1000_Manager.h"
uint8_t GUI_start_flag_received = 0;
uint8_t USB_Buffer[64] = {0};
extern "C" void Error_Handler(void) {}
static int tests_passed = 0;
static int tests_total = 0;
#define RUN_TEST(fn) \
do { \
tests_total++; \
printf(" [%2d] %-65s ", tests_total, #fn); \
fn(); \
tests_passed++; \
printf("PASS\n"); \
} while (0)
static void init_devices_clean(ADAR1000Manager& mgr)
{
mock_spi_set_rx_byte(0xA5);
bool ok = mgr.initializeAllDevices();
assert(ok);
}
// After a clean init, the manager is in TX mode (initializeAllDevices ends
// with setAllDevicesTXMode). Force RX mode under sustained SPI failure: must
// return false AND must not flip current_mode_ to RX.
static void test_failed_rx_switch_does_not_update_mode()
{
spy_reset();
ADAR1000Manager mgr;
init_devices_clean(mgr);
assert(mgr.getCurrentMode() == ADAR1000Manager::BeamDirection::TX);
mgr.resetCommStats();
mock_spi_queue_failure(10000, HAL_ERROR);
bool ok = mgr.setAllDevicesRXMode();
assert(ok == false);
// Mode flag must NOT have moved -- this is the dangerous lie we are fixing.
assert(mgr.getCurrentMode() == ADAR1000Manager::BeamDirection::TX);
}
// Symmetric case: cleanly transition to RX, then fail TX setup. Mode flag
// must stay RX.
static void test_failed_tx_switch_does_not_update_mode()
{
spy_reset();
ADAR1000Manager mgr;
init_devices_clean(mgr);
mock_spi_set_rx_byte(0xA5);
bool rx_ok = mgr.setAllDevicesRXMode();
assert(rx_ok);
assert(mgr.getCurrentMode() == ADAR1000Manager::BeamDirection::RX);
mgr.resetCommStats();
mock_spi_queue_failure(10000, HAL_ERROR);
bool ok = mgr.setAllDevicesTXMode();
assert(ok == false);
assert(mgr.getCurrentMode() == ADAR1000Manager::BeamDirection::RX);
}
// Healthy round-trip: mode flag tracks the call that was made. Guards against
// the new code path accidentally refusing to advance the mode on success.
static void test_clean_mode_switches_update_flag()
{
spy_reset();
ADAR1000Manager mgr;
init_devices_clean(mgr);
assert(mgr.getCurrentMode() == ADAR1000Manager::BeamDirection::TX);
mock_spi_set_rx_byte(0xA5);
bool to_rx = mgr.setAllDevicesRXMode();
assert(to_rx);
assert(mgr.getCurrentMode() == ADAR1000Manager::BeamDirection::RX);
bool to_tx = mgr.setAllDevicesTXMode();
assert(to_tx);
assert(mgr.getCurrentMode() == ADAR1000Manager::BeamDirection::TX);
}
int main()
{
printf("=== ADAR1000 mode-switch honesty tests ===\n");
RUN_TEST(test_failed_rx_switch_does_not_update_mode);
RUN_TEST(test_failed_tx_switch_does_not_update_mode);
RUN_TEST(test_clean_mode_switches_update_flag);
printf("=== Results: %d/%d passed ===\n", tests_passed, tests_total);
return (tests_passed == tests_total) ? 0 : 1;
}
@@ -0,0 +1,128 @@
// test_adar_spi_write_failure_propagates.cpp
//
// When HAL_SPI_Transmit / HAL_SPI_TransmitReceive returns HAL_ERROR, every
// caller above must see the failure rather than silently continuing on.
// Previously adarWrite() returned void and dropped the SPI status on the
// floor, so a dead bus produced four "successful" inits.
#include <cassert>
#include <cmath>
#include <cstdio>
#include "stm32_hal_mock.h"
#include "ADAR1000_Manager.h"
uint8_t GUI_start_flag_received = 0;
uint8_t USB_Buffer[64] = {0};
extern "C" void Error_Handler(void) {}
static int tests_passed = 0;
static int tests_total = 0;
#define RUN_TEST(fn) \
do { \
tests_total++; \
printf(" [%2d] %-65s ", tests_total, #fn); \
fn(); \
tests_passed++; \
printf("PASS\n"); \
} while (0)
// First SPI transmit fails (the soft-reset write inside initializeSingleDevice).
// Init must abort immediately and report false.
static void test_first_spi_failure_aborts_init()
{
spy_reset();
mock_spi_queue_failure(1, HAL_ERROR);
ADAR1000Manager mgr;
bool ok = mgr.initializeAllDevices();
assert(ok == false);
const auto& stats = mgr.getCommStats();
assert(stats.writes_fail >= 1);
assert(stats.last_fail_dev == 0); // failure was on dev[0]
}
// Sustained SPI failure (every call) must not produce a green init even if
// individual writes early in the sequence have already counted as failures
// without aborting -- the function must still return false at the end.
static void test_sustained_spi_failure_aborts_init()
{
spy_reset();
mock_spi_queue_failure(10000, HAL_ERROR);
ADAR1000Manager mgr;
bool ok = mgr.initializeAllDevices();
assert(ok == false);
const auto& stats = mgr.getCommStats();
assert(stats.writes_ok == 0);
assert(stats.writes_fail >= 1);
}
// adarSetBit must NOT proceed with the write when the read-modify-write read
// fails -- otherwise it would clobber every other bit in the register by
// writing back (0 | mask) over a register whose actual contents are unknown.
// We use setTRSwitchPosition (which calls adarSetBit) and inject a failure
// on the read leg.
static void test_set_bit_skips_write_on_read_failure()
{
spy_reset();
ADAR1000Manager mgr;
mgr.resetCommStats();
// adarSetBit calls adarReadChecked first, which itself does:
// 1. adarWrite(SDO active) -- HAL_SPI_Transmit
// 2. HAL_SPI_TransmitReceive (the actual read)
// 3. adarWrite(SDO inactive) -- HAL_SPI_Transmit
// We want the actual read (call #2) to fail. Queue failure starting at
// call 2 by letting call 1 succeed first via a one-shot prime, then
// queueing the failure.
//
// Simpler approach: queue a failure of 100 calls, then call setTRSwitchPosition
// and assert reads_fail >= 1 and writes_fail >= 1 (the SDO-active write
// also fails). The key invariant: even though the read-modify-write was
// attempted, the function returned false.
mock_spi_queue_failure(100, HAL_ERROR);
bool ok = mgr.setTRSwitchPosition(0, true);
assert(ok == false);
const auto& stats = mgr.getCommStats();
assert(stats.reads_fail >= 1 || stats.writes_fail >= 1);
}
// Mode-switch must not fall through to another write block on the device that
// failed -- and the per-device current_mode must not be updated.
static void test_mode_switch_failure_propagates()
{
spy_reset();
mock_spi_set_rx_byte(0xA5); // make scratchpad verify succeed first
ADAR1000Manager mgr;
bool init_ok = mgr.initializeAllDevices();
assert(init_ok);
// Now inject sustained SPI failure for the mode switch.
mgr.resetCommStats();
mock_spi_queue_failure(10000, HAL_ERROR);
bool ok = mgr.setAllDevicesRXMode();
assert(ok == false);
const auto& stats = mgr.getCommStats();
assert(stats.writes_fail >= 1);
}
int main()
{
printf("=== ADAR1000 SPI write-failure propagation tests ===\n");
RUN_TEST(test_first_spi_failure_aborts_init);
RUN_TEST(test_sustained_spi_failure_aborts_init);
RUN_TEST(test_set_bit_skips_write_on_read_failure);
RUN_TEST(test_mode_switch_failure_propagates);
printf("=== Results: %d/%d passed ===\n", tests_passed, tests_total);
return (tests_passed == tests_total) ? 0 : 1;
}