Skip to content
Closed
225 changes: 73 additions & 152 deletions tests/unit/test_slot_manager.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,10 @@
/**
* @file test_slot_manager.c
* @brief Unit tests for the production firmware slot manager
*
* These tests exercise the real core/slot_manager.c. The three image
* verification steps it calls -- eos_image_parse_header(),
* eos_image_verify_integrity() and eos_image_verify_signature() -- are
* replaced with per-slot scriptable mocks so each stage can be failed
* independently without having to build and sign real images. Flash and
* slot geometry come from the standard simulated eos_board_ops_t, so
* eos_hal_slot_addr()/eos_hal_slot_size() behave as they do on a board.
*/

#include "eos_slot_manager.h"
#include "eos_image.h"
#include "eos_hal.h"
#include "eos_slot_manager.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
Expand All @@ -26,103 +17,70 @@
#define SLOT_B_ADDR 0x30000u
#define SLOT_SIZE 0x10000u

/* ---- Simulated flash / board ops ----
*
* Large enough to hold both slots (SLOT_B_ADDR + SLOT_SIZE) plus the low
* bootctl/backup/log region every other test file reserves below 0x4000. */
#define SIM_FLASH_SIZE (SLOT_B_ADDR + SLOT_SIZE)
static uint8_t sim_flash[SIM_FLASH_SIZE];

/* ---- Observed/scripted flash erase behaviour (read by sim_flash_erase
* below, scripted/checked by the erase tests further down) ---- */
static int erase_result;
static int parse_result[2];
static int integrity_result[2];
static int signature_result[2];
static uint32_t slot_version[2];
static int erase_result;
static uint32_t erased_addr;
static size_t erased_size;
static size_t erased_size;
static int tests_passed;

#define ASSERT(condition) \
do { \
if (!(condition)) { \
fprintf(stderr, "[FAIL] %s:%d: %s\n", \
__FILE__, __LINE__, #condition); \
exit(1); \
} \
} while (0)

#define RUN(test) \
do { \
reset_fixture(); \
test(); \
tests_passed++; \
printf("[PASS] %s\n", #test); \
} while (0)

static int sim_flash_read(uint32_t addr, void *buf, size_t len)
static int slot_index(uint32_t addr)
{
if (addr + len > SIM_FLASH_SIZE) return EOS_ERR_FLASH;
memcpy(buf, &sim_flash[addr], len);
return EOS_OK;
if (addr == SLOT_A_ADDR) return EOS_SLOT_A;
if (addr == SLOT_B_ADDR) return EOS_SLOT_B;
return -1;
}

static int sim_flash_write(uint32_t addr, const void *buf, size_t len)
static void reset_fixture(void)
{
if (addr + len > SIM_FLASH_SIZE) return EOS_ERR_FLASH;
memcpy(&sim_flash[addr], buf, len);
return EOS_OK;
for (int i = 0; i < 2; i++) {
parse_result[i] = EOS_ERR_NO_IMAGE;
integrity_result[i] = EOS_OK;
signature_result[i] = EOS_OK;
slot_version[i] = 0;
}
erase_result = EOS_OK;
erased_addr = 0;
erased_size = 0;
}

static int sim_flash_erase(uint32_t addr, size_t len)
uint32_t eos_hal_slot_addr(eos_slot_t slot)
{
erased_addr = addr;
erased_size = len;
if (erase_result != EOS_OK) return erase_result;
if (addr + len > SIM_FLASH_SIZE) return EOS_ERR_FLASH;
memset(&sim_flash[addr], 0xFF, len);
return EOS_OK;
if (slot == EOS_SLOT_A) return SLOT_A_ADDR;
if (slot == EOS_SLOT_B) return SLOT_B_ADDR;
return 0;
}

static uint32_t sim_tick = 0;
static uint32_t sim_get_tick(void) { return sim_tick++; }
static void sim_noop(void) {}
static void sim_noop_u32(uint32_t x) { (void)x; }
static void sim_jump(uint32_t addr) { (void)addr; }
static eos_reset_reason_t sim_reset_reason(void) { return EOS_RESET_POWER_ON; }
static bool sim_recovery_pin(void) { return false; }
static void sim_system_reset(void) {}

static const eos_board_ops_t sim_ops = {
.flash_base = 0,
.flash_size = SIM_FLASH_SIZE,
.slot_a_addr = SLOT_A_ADDR,
.slot_a_size = SLOT_SIZE,
.slot_b_addr = SLOT_B_ADDR,
.slot_b_size = SLOT_SIZE,
.recovery_addr = 0,
.recovery_size = 0,
.bootctl_addr = 0,
.bootctl_backup_addr = 0x1000,
.log_addr = 0x2000,
.app_vector_offset = 0,
.flash_read = sim_flash_read,
.flash_write = sim_flash_write,
.flash_erase = sim_flash_erase,
.watchdog_init = sim_noop_u32,
.watchdog_feed = sim_noop,
.get_reset_reason = sim_reset_reason,
.system_reset = sim_system_reset,
.recovery_pin_asserted = sim_recovery_pin,
.jump = sim_jump,
.uart_init = NULL,
.uart_send = NULL,
.uart_recv = NULL,
.get_tick_ms = sim_get_tick,
.disable_interrupts = sim_noop,
.enable_interrupts = sim_noop,
.deinit_peripherals = sim_noop,
};

/* ---- Scriptable per-slot verification results ---- */

static int parse_result[2];
static int integrity_result[2];
static int signature_result[2];
static uint32_t slot_version[2];

/** Map a flash address back to the slot that starts there, or -1. */
static int slot_index(uint32_t addr)
uint32_t eos_hal_slot_size(eos_slot_t slot)
{
if (addr == SLOT_A_ADDR) return EOS_SLOT_A;
if (addr == SLOT_B_ADDR) return EOS_SLOT_B;
return -1;
return slot <= EOS_SLOT_B ? SLOT_SIZE : 0;
}

/* ---- Image verification mocks (override eboot_core's real ones) ----
*
* verify_slot() passes the parsed header straight to the integrity and
* signature checks, so the mocks stash the slot index in reserved[0] on
* parse and read it back to decide which scripted result to return. */
int eos_hal_flash_erase(uint32_t addr, size_t len)
{
erased_addr = addr;
erased_size = len;
return erase_result;
}

int eos_image_parse_header(uint32_t addr, eos_image_header_t *out)
{
Expand Down Expand Up @@ -150,57 +108,20 @@ int eos_image_verify_signature(const eos_image_header_t *hdr)
return signature_result[hdr->reserved[0]];
}

/* ---- Test harness ---- */

static int tests_run = 0;
static int tests_passed = 0;

#define TEST(name) \
static void name(void); \
static void run_##name(void) { \
memset(sim_flash, 0xFF, sizeof(sim_flash)); \
sim_tick = 0; \
for (int i = 0; i < 2; i++) { \
parse_result[i] = EOS_ERR_NO_IMAGE; \
integrity_result[i] = EOS_OK; \
signature_result[i] = EOS_OK; \
slot_version[i] = 0; \
} \
erase_result = EOS_OK; \
erased_addr = 0; \
erased_size = 0; \
eos_hal_init(&sim_ops); \
printf(" %-55s ", #name); \
name(); \
tests_passed++; \
printf("[PASS]\n"); \
} \
static void name(void)

#define ASSERT(cond) do { \
if (!(cond)) { \
printf("[FAIL] %s:%d: %s\n", __FILE__, __LINE__, #cond); \
exit(1); \
} \
} while(0)

/** Script a slot so that all three verification stages succeed. */
static void make_valid(eos_slot_t slot, uint32_t version)
{
parse_result[slot] = EOS_OK;
slot_version[slot] = version;
}

/* ---- Tests ---- */

TEST(test_scan_no_valid_slots)
static void test_scan_no_valid_slots(void)
{
ASSERT(eos_slot_scan_all() == 0);
ASSERT(eos_slot_get_state(EOS_SLOT_A) == EOS_SLOT_STATE_EMPTY);
ASSERT(eos_slot_get_state(EOS_SLOT_B) == EOS_SLOT_STATE_EMPTY);
}

TEST(test_scan_one_valid_slot)
static void test_scan_one_valid_slot(void)
{
make_valid(EOS_SLOT_A, EOS_VERSION_MAKE(1, 2, 3));

Expand All @@ -214,7 +135,7 @@ TEST(test_scan_one_valid_slot)
ASSERT(header.image_version == EOS_VERSION_MAKE(1, 2, 3));
}

TEST(test_scan_two_valid_slots)
static void test_scan_two_valid_slots(void)
{
make_valid(EOS_SLOT_A, EOS_VERSION_MAKE(1, 0, 0));
make_valid(EOS_SLOT_B, EOS_VERSION_MAKE(2, 0, 0));
Expand All @@ -225,7 +146,7 @@ TEST(test_scan_two_valid_slots)
ASSERT(eos_slot_get_version(EOS_SLOT_B) == EOS_VERSION_MAKE(2, 0, 0));
}

TEST(test_verification_failures_are_invalid)
static void test_verification_failures_are_invalid(void)
{
make_valid(EOS_SLOT_A, EOS_VERSION_MAKE(1, 0, 0));
make_valid(EOS_SLOT_B, EOS_VERSION_MAKE(2, 0, 0));
Expand All @@ -238,7 +159,7 @@ TEST(test_verification_failures_are_invalid)
ASSERT(eos_slot_get_header(EOS_SLOT_A, NULL) == EOS_ERR_INVALID);
}

TEST(test_invalid_slot_is_rejected)
static void test_invalid_slot_is_rejected(void)
{
ASSERT(!eos_slot_is_valid(EOS_SLOT_RECOVERY));
ASSERT(eos_slot_get_state(EOS_SLOT_RECOVERY) == EOS_SLOT_STATE_EMPTY);
Expand All @@ -247,7 +168,7 @@ TEST(test_invalid_slot_is_rejected)
ASSERT(eos_slot_erase(EOS_SLOT_RECOVERY) == EOS_ERR_INVALID);
}

TEST(test_erase_updates_state_only_on_success)
static void test_erase_updates_state_only_on_success(void)
{
make_valid(EOS_SLOT_A, EOS_VERSION_MAKE(1, 0, 0));
ASSERT(eos_slot_scan_all() == 1);
Expand All @@ -264,12 +185,13 @@ TEST(test_erase_updates_state_only_on_success)
ASSERT(eos_slot_get_version(EOS_SLOT_A) == 0);
}


/* The boot-attempt counter is what makes an unproven image fall back instead
* of bricking the device: mark_booting() has to increment it, confirm() has to
* clear it, and needs_rollback() has to fire once it reaches max_attempts.
* PR #37 added this behaviour but its test never compiled, so none of it was
* ever exercised. */
TEST(test_boot_attempts_drive_rollback)
static void test_boot_attempts_drive_rollback(void)
{
make_valid(EOS_SLOT_A, EOS_VERSION_MAKE(1, 0, 0));
ASSERT(eos_slot_scan_all() == 1);
Expand All @@ -295,7 +217,7 @@ TEST(test_boot_attempts_drive_rollback)
ASSERT(eos_slot_get_state(EOS_SLOT_A) == EOS_SLOT_STATE_CONFIRMED);
}

TEST(test_boot_attempts_reject_invalid_slot)
static void test_boot_attempts_reject_invalid_slot(void)
{
ASSERT(eos_slot_mark_booting(EOS_SLOT_RECOVERY) == EOS_ERR_INVALID);
ASSERT(eos_slot_confirm(EOS_SLOT_RECOVERY) == EOS_ERR_INVALID);
Expand All @@ -311,7 +233,7 @@ TEST(test_boot_attempts_reject_invalid_slot)

/* Erasing a slot must also drop its boot-attempt count; otherwise a freshly
* flashed image inherits the failures of the one it replaced. */
TEST(test_erase_resets_boot_attempts)
static void test_erase_resets_boot_attempts(void)
{
make_valid(EOS_SLOT_A, EOS_VERSION_MAKE(1, 0, 0));
ASSERT(eos_slot_scan_all() == 1);
Expand All @@ -328,17 +250,16 @@ TEST(test_erase_resets_boot_attempts)

int main(void)
{
printf("=== eBootloader Slot Manager Tests ===\n\n");
run_test_scan_no_valid_slots();
run_test_scan_one_valid_slot();
run_test_scan_two_valid_slots();
run_test_verification_failures_are_invalid();
run_test_invalid_slot_is_rejected();
run_test_erase_updates_state_only_on_success();
run_test_boot_attempts_drive_rollback();
run_test_boot_attempts_reject_invalid_slot();
run_test_erase_resets_boot_attempts();
tests_run = 9;
printf("\n%d/%d tests passed\n", tests_passed, tests_run);
return (tests_passed == tests_run) ? 0 : 1;
printf("=== eBootloader Slot Manager Tests ===\n");
RUN(test_scan_no_valid_slots);
RUN(test_scan_one_valid_slot);
RUN(test_scan_two_valid_slots);
RUN(test_verification_failures_are_invalid);
RUN(test_invalid_slot_is_rejected);
RUN(test_erase_updates_state_only_on_success);
RUN(test_boot_attempts_drive_rollback);
RUN(test_boot_attempts_reject_invalid_slot);
RUN(test_erase_resets_boot_attempts);
printf("\n%d/9 tests passed\n", tests_passed);
return 0;
}
Loading