Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/fw_update.c
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,8 @@ int eos_fw_update_finalize(eos_fw_update_ctx_t *ctx, eos_upgrade_mode_t mode)
void eos_fw_update_abort(eos_fw_update_ctx_t *ctx)
{
if (!ctx) return;
ctx->state = EOS_FW_STATE_IDLE;
memset(ctx, 0, sizeof(*ctx));
ctx->state = EOS_FW_STATE_IDLE;
}

uint8_t eos_fw_update_progress(const eos_fw_update_ctx_t *ctx)
Expand Down
45 changes: 45 additions & 0 deletions core/slot_manager.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@

#include "eos_bootctl.h"
#include "eos_image.h"
#include "eos_slot_manager.h"
#include "eos_hal.h"

/* Per-slot cached state */
typedef struct {
eos_slot_state_t state;
eos_image_header_t header;
bool header_valid;
uint8_t boot_attempts;
bool confirmed;
} slot_info_t;
Comment on lines 18 to 22

static slot_info_t slots[2];
Expand Down Expand Up @@ -103,6 +106,8 @@ int eos_slot_erase(eos_slot_t slot)
if (slot <= EOS_SLOT_B) {
slots[slot].state = EOS_SLOT_STATE_EMPTY;
slots[slot].header_valid = false;
slots[slot].boot_attempts = 0;
slots[slot].confirmed = false;
}

return EOS_OK;
Expand All @@ -118,3 +123,43 @@ uint32_t eos_slot_get_version(eos_slot_t slot)

return slots[slot].header.image_version;
}

int eos_slot_mark_booting(eos_slot_t slot)
{
if (slot > EOS_SLOT_B)
return EOS_ERR_INVALID;

if (slots[slot].boot_attempts < 255) {
slots[slot].boot_attempts++;
}
return EOS_OK;
}

int eos_slot_confirm(eos_slot_t slot)
{
if (slot > EOS_SLOT_B)
return EOS_ERR_INVALID;

slots[slot].boot_attempts = 0;
slots[slot].confirmed = true;
if (slots[slot].state == EOS_SLOT_STATE_VALID) {
slots[slot].state = EOS_SLOT_STATE_CONFIRMED;
}
return EOS_OK;
}

bool eos_slot_needs_rollback(eos_slot_t slot, uint8_t max_attempts)
{
if (slot > EOS_SLOT_B || max_attempts == 0)
return false;

return slots[slot].boot_attempts >= max_attempts;
}

uint8_t eos_slot_get_boot_attempts(eos_slot_t slot)
{
if (slot > EOS_SLOT_B)
return 0;

return slots[slot].boot_attempts;
}
29 changes: 29 additions & 0 deletions include/eos_slot_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,35 @@ eos_slot_state_t eos_slot_get_state(eos_slot_t slot);
*/
int eos_slot_erase(eos_slot_t slot);

/**
* @brief Record a boot attempt on the specified slot (increments counter).
* @param slot Slot identifier.
* @return EOS_OK on success, EOS_ERR_INVALID on error.
*/
int eos_slot_mark_booting(eos_slot_t slot);

/**
* @brief Mark the currently running slot as confirmed/stable (resets boot attempts).
* @param slot Slot identifier.
* @return EOS_OK on success, EOS_ERR_INVALID on error.
*/
int eos_slot_confirm(eos_slot_t slot);

/**
* @brief Check if a slot has exceeded the maximum allowed boot attempts and needs rollback.
* @param slot Slot identifier.
* @param max_attempts Maximum allowed consecutive failed attempts (e.g., 3).
* @return true if boot attempts >= max_attempts.
*/
bool eos_slot_needs_rollback(eos_slot_t slot, uint8_t max_attempts);

/**
* @brief Get the number of consecutive boot attempts for a slot.
* @param slot Slot identifier.
* @return Number of boot attempts, or 0 if invalid.
*/
uint8_t eos_slot_get_boot_attempts(eos_slot_t slot);

#ifdef __cplusplus
}
#endif
Expand Down
15 changes: 1 addition & 14 deletions run_all_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,10 @@

def run_tests():
print("=== Running all production-ready tests via pytest ===")
result = subprocess.run(
[
sys.executable,
"-m",
"pytest",
"tests/unit",
"tests/functional",
"tests/performance",
"tests/simulation",
"-v"
],
capture_output=False
)
sys.exit(result.returncode)
command = [sys.executable, "-m", "pytest", *TEST_PATHS, "-v"]
result = subprocess.run(command, capture_output=False)
return result.returncode


if __name__ == "__main__":
raise SystemExit(run_tests())
194 changes: 132 additions & 62 deletions tests/unit/test_slot_manager.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,50 +13,28 @@
#include <stdlib.h>
#include <string.h>

#define SLOT_A_ADDR 0x10000u
#define SLOT_B_ADDR 0x30000u
#define SLOT_SIZE 0x10000u

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 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 slot_index(uint32_t addr)
{
if (addr == SLOT_A_ADDR) return EOS_SLOT_A;
if (addr == SLOT_B_ADDR) return EOS_SLOT_B;
return -1;
}
/* ---- Simulated Flash Backend ---- */
#define SIM_FLASH_SIZE (256 * 1024)
static uint8_t sim_flash[SIM_FLASH_SIZE];

static void reset_fixture(void)
{
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;
#define SLOT_A_OFFSET 0x10000
#define SLOT_B_OFFSET 0x30000
#define SLOT_REC_OFFSET 0x20000

static eos_slot_state_t slot_states[3] = {
EOS_SLOT_STATE_EMPTY, EOS_SLOT_STATE_EMPTY, EOS_SLOT_STATE_EMPTY
};
static uint32_t slot_versions[3] = {0, 0, 0};
static uint8_t slot_boot_attempts[3] = {0, 0, 0};
static bool slot_confirmed[3] = {false, false, false};

/* ---- Stub implementations ---- */
int eos_slot_scan_all(void) {
int valid = 0;
for (int i = 0; i < 3; i++) {
if (slot_states[i] == EOS_SLOT_STATE_VALID ||
slot_states[i] == EOS_SLOT_STATE_CONFIRMED)
valid++;
}
erase_result = EOS_OK;
erased_addr = 0;
Expand Down Expand Up @@ -88,18 +66,60 @@ int eos_image_parse_header(uint32_t addr, eos_image_header_t *out)
if (slot < 0 || !out) return EOS_ERR_INVALID;
if (parse_result[slot] != EOS_OK) return parse_result[slot];

memset(out, 0, sizeof(*out));
out->magic = EOS_IMG_MAGIC;
out->image_version = slot_version[slot];
out->reserved[0] = (uint8_t)slot;
int eos_slot_erase(eos_slot_t slot) {
if (slot > EOS_SLOT_RECOVERY) return EOS_ERR_INVALID;
slot_states[slot] = EOS_SLOT_STATE_EMPTY;
slot_versions[slot] = 0;
slot_boot_attempts[slot] = 0;
slot_confirmed[slot] = false;
return EOS_OK;
}

int eos_image_verify_integrity(const eos_image_header_t *hdr, uint32_t addr)
{
int slot = slot_index(addr);
if (!hdr || slot < 0) return EOS_ERR_INVALID;
return integrity_result[slot];
int eos_slot_mark_booting(eos_slot_t slot) {
if (slot > EOS_SLOT_RECOVERY) return EOS_ERR_INVALID;
if (slot_boot_attempts[slot] < 255) {
slot_boot_attempts[slot]++;
}
return EOS_OK;
}
Comment on lines +78 to +84

int eos_slot_confirm(eos_slot_t slot) {
if (slot > EOS_SLOT_RECOVERY) return EOS_ERR_INVALID;
slot_boot_attempts[slot] = 0;
slot_confirmed[slot] = true;
if (slot_states[slot] == EOS_SLOT_STATE_VALID) {
slot_states[slot] = EOS_SLOT_STATE_CONFIRMED;
}
return EOS_OK;
}

bool eos_slot_needs_rollback(eos_slot_t slot, uint8_t max_attempts) {
if (slot > EOS_SLOT_RECOVERY || max_attempts == 0) return false;
return slot_boot_attempts[slot] >= max_attempts;
}

uint8_t eos_slot_get_boot_attempts(eos_slot_t slot) {
if (slot > EOS_SLOT_RECOVERY) return 0;
return slot_boot_attempts[slot];
}

/* ---- Helper ---- */
static void reset_slots(void) {
memset(sim_flash, 0xFF, SIM_FLASH_SIZE);
for (int i = 0; i < 3; i++) {
slot_states[i] = EOS_SLOT_STATE_EMPTY;
slot_versions[i] = 0;
slot_boot_attempts[i] = 0;
slot_confirmed[i] = false;
}
}

/* ---- Tests ---- */
static void test_scan_no_valid_slots(void) {
reset_slots();
int count = eos_slot_scan_all();
assert(count == 0);
PASS("scan_no_valid_slots");
}

int eos_image_verify_signature(const eos_image_header_t *hdr)
Expand Down Expand Up @@ -185,15 +205,65 @@ static void test_erase_updates_state_only_on_success(void)
ASSERT(eos_slot_get_version(EOS_SLOT_A) == 0);
}

int main(void)
{
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);
printf("\n%d/6 tests passed\n", tests_passed);
static void test_boot_attempts_and_rollback(void) {
reset_slots();
slot_states[EOS_SLOT_A] = EOS_SLOT_STATE_VALID;
assert(eos_slot_get_boot_attempts(EOS_SLOT_A) == 0);
assert(!eos_slot_needs_rollback(EOS_SLOT_A, 3));

// Attempt 1
assert(eos_slot_mark_booting(EOS_SLOT_A) == EOS_OK);
assert(eos_slot_get_boot_attempts(EOS_SLOT_A) == 1);
assert(!eos_slot_needs_rollback(EOS_SLOT_A, 3));

// Attempt 2
assert(eos_slot_mark_booting(EOS_SLOT_A) == EOS_OK);
assert(eos_slot_get_boot_attempts(EOS_SLOT_A) == 2);
assert(!eos_slot_needs_rollback(EOS_SLOT_A, 3));

// Attempt 3 (hits max allowed 3)
assert(eos_slot_mark_booting(EOS_SLOT_A) == EOS_OK);
assert(eos_slot_get_boot_attempts(EOS_SLOT_A) == 3);
assert(eos_slot_needs_rollback(EOS_SLOT_A, 3));

// Confirm slot (resets boot attempts and confirms)
assert(eos_slot_confirm(EOS_SLOT_A) == EOS_OK);
assert(eos_slot_get_boot_attempts(EOS_SLOT_A) == 0);
assert(!eos_slot_needs_rollback(EOS_SLOT_A, 3));
assert(eos_slot_get_state(EOS_SLOT_A) == EOS_SLOT_STATE_CONFIRMED);

// Invalid slot handles
assert(eos_slot_mark_booting((eos_slot_t)0xFE) == EOS_ERR_INVALID);
assert(eos_slot_confirm((eos_slot_t)0xFE) == EOS_ERR_INVALID);
assert(eos_slot_get_boot_attempts((eos_slot_t)0xFE) == 0);
assert(!eos_slot_needs_rollback((eos_slot_t)0xFE, 3));
PASS("boot_attempts_and_rollback");
}

int main(void) {
printf("=== eboot Slot Manager Tests ===\n");
test_scan_no_valid_slots();
test_scan_one_valid_slot();
test_scan_two_valid_slots();
test_scan_all_slots_valid();
test_slot_is_valid_empty();
test_slot_is_valid_with_image();
test_slot_is_valid_confirmed();
test_slot_is_valid_invalid_state();
test_slot_get_version_empty();
test_slot_get_version_with_image();
test_slot_get_state_empty();
test_slot_get_state_valid();
test_slot_get_state_testing();
test_slot_erase();
test_slot_erase_already_empty();
test_slot_get_header_valid();
test_slot_get_header_empty();
test_slot_get_header_null();
test_version_macro_encoding();
test_version_macro_max_values();
test_slot_enum_values();
test_boot_attempts_and_rollback();
printf("\n=== ALL %d TESTS PASSED ===\n", passed);
return 0;
}