diff --git a/CMakeLists.txt b/CMakeLists.txt index fabbfcf..796359f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,7 +77,11 @@ if(ENABLE_COVERAGE) target_link_options(${MODULE_NAME} PRIVATE --coverage) endif() -create_library_makefile(${MODULE_NAME}) +# Only run create_library_makefile if not in CI environment +# The memory_analysis.cmake script path has issues in FetchContent context +if(NOT DEFINED ENV{CI} AND COMMAND create_library_makefile) + create_library_makefile(${MODULE_NAME}) +endif() # ====================================================================== # Tests diff --git a/README.md b/README.md index d56c679..7f1c4a9 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ DMOD provides a modular architecture that makes embedded systems more flexible, - **Simple key-value storage**: Store configuration parameters as name-value pairs - **Context-based management**: Create multiple isolated environment contexts - **Context inheritance**: Child contexts can inherit variables from parent contexts +- **Context stack**: Push and pop contexts for temporary context switching - **Thread-safe operations**: Uses DMOD's critical section mechanisms - **Prefix-based search**: Find all variables matching a specific prefix - **Efficient lookup**: Fast variable retrieval using linked list structure @@ -52,6 +53,8 @@ dmenv is ideal for storing application configuration, runtime parameters, and in - ✅ **Context-based management**: Create multiple isolated environment contexts - ✅ **Context inheritance**: Child contexts can inherit variables from parent contexts +- ✅ **Context stack**: Push and pop contexts for temporary context switching +- ✅ **Root context**: Set a global root context as the base fallback - ✅ **Key-value storage**: Store and retrieve environment variables by name - ✅ **Variable update**: Update existing variables without creating duplicates - ✅ **Variable removal**: Remove individual variables or clear all at once @@ -149,7 +152,8 @@ dmenv includes comprehensive test suites: - Integer operations (seti/geti) - Context inheritance - Context override behavior - - Default context management + - Root context management + - Context stack push/pop operations - Multiple variable stress tests ### Running Tests @@ -295,6 +299,44 @@ void reset_example(void) { } ``` +### Context Stack Example + +```c +#include "dmenv.h" + +void context_stack_example(void) { + // Create root and temporary contexts + dmenv_ctx_t root = dmenv_create(NULL); + dmenv_ctx_t temp = dmenv_create(NULL); + + // Set root context as the base + dmenv_set_root_context(root); + dmenv_set(root, "MODE", "production"); + + // Get current context (returns root) + dmenv_ctx_t current = dmenv_get_current_context(); + printf("Mode: %s\n", dmenv_get(current, "MODE")); // Prints: production + + // Push temporary context for testing + dmenv_push_context(temp); + dmenv_set(temp, "MODE", "testing"); + + // Get current context (returns temp) + current = dmenv_get_current_context(); + printf("Mode: %s\n", dmenv_get(current, "MODE")); // Prints: testing + + // Pop temporary context + dmenv_pop_context(); + + // Get current context (returns root again) + current = dmenv_get_current_context(); + printf("Mode: %s\n", dmenv_get(current, "MODE")); // Prints: production + + dmenv_destroy(temp); + dmenv_destroy(root); +} +``` + ### Integration Example ```c @@ -393,27 +435,62 @@ Check if a context is valid. - **Returns:** `true` if valid, `false` otherwise - **Thread-safe:** Yes -#### `dmenv_set_as_default` +#### `dmenv_set_root_context` + +```c +void dmenv_set_root_context(dmenv_ctx_t ctx); +``` + +Set the root context. The root context serves as the base context when no other contexts have been pushed onto the context stack. + +- **Parameters:** + - `ctx`: Context to set as root context +- **Thread-safe:** Yes + +#### `dmenv_get_root_context` + +```c +dmenv_ctx_t dmenv_get_root_context(void); +``` + +Get the root context. + +- **Returns:** Pointer to the root context, or NULL if not set +- **Thread-safe:** Yes + +#### `dmenv_push_context` ```c -void dmenv_set_as_default(dmenv_ctx_t ctx); +bool dmenv_push_context(dmenv_ctx_t ctx); ``` -Set the default context. +Push a context onto the context stack. The pushed context becomes the current context. - **Parameters:** - - `ctx`: Context to set as default + - `ctx`: Context to push onto the stack +- **Returns:** `true` if the context was pushed successfully, `false` otherwise (e.g., stack overflow or invalid context) +- **Thread-safe:** Yes + +#### `dmenv_pop_context` + +```c +dmenv_ctx_t dmenv_pop_context(void); +``` + +Pop the current context from the context stack. After popping, the previous context on the stack becomes the current context. If the stack is empty, the root context becomes the current context. + +- **Returns:** Pointer to the popped context, or NULL if the stack was empty - **Thread-safe:** Yes -#### `dmenv_get_default` +#### `dmenv_get_current_context` ```c -dmenv_ctx_t dmenv_get_default(void); +dmenv_ctx_t dmenv_get_current_context(void); ``` -Get the default context. +Get the current context. Returns the top context from the stack if any contexts have been pushed, otherwise returns the root context. -- **Returns:** Pointer to the default context, or NULL if not set +- **Returns:** Pointer to the current context, or NULL if no context is set - **Thread-safe:** Yes ### Variable Operations diff --git a/include/dmenv.h b/include/dmenv.h index 16af747..c014f1a 100644 --- a/include/dmenv.h +++ b/include/dmenv.h @@ -51,18 +51,55 @@ DMOD_BUILTIN_API(dmenv, 1.0, void, _destroy, (dmenv_ctx_t ctx)); DMOD_BUILTIN_API(dmenv, 1.0, bool, _is_valid, (dmenv_ctx_t ctx)); /** - * @brief Set the default context + * @brief Set the root context * - * @param ctx Context to set as default + * Sets the global root context. The root context serves as the base context + * when no other contexts have been pushed onto the context stack. + * + * @param ctx Context to set as root context + */ +DMOD_BUILTIN_API(dmenv, 1.0, void, _set_root_context, (dmenv_ctx_t ctx)); + +/** + * @brief Get the root context + * + * @return Pointer to the root context, or NULL if not set */ -DMOD_BUILTIN_API(dmenv, 1.0, void, _set_as_default, (dmenv_ctx_t ctx)); +DMOD_BUILTIN_API(dmenv, 1.0, dmenv_ctx_t, _get_root_context, (void)); /** - * @brief Get the default context + * @brief Push a context onto the context stack + * + * Pushes the specified context onto the context stack, making it the current + * context. When accessing variables through the current context, this context + * will be used instead of the root context. + * + * @param ctx Context to push onto the stack + * + * @return true if the context was pushed successfully, false otherwise (e.g., stack overflow or invalid context) + */ +DMOD_BUILTIN_API(dmenv, 1.0, bool, _push_context, (dmenv_ctx_t ctx)); + +/** + * @brief Pop the current context from the context stack + * + * Removes the top context from the context stack. After popping, the previous + * context on the stack becomes the current context. If the stack is empty, + * the root context becomes the current context. + * + * @return Pointer to the popped context, or NULL if the stack was empty + */ +DMOD_BUILTIN_API(dmenv, 1.0, dmenv_ctx_t, _pop_context, (void)); + +/** + * @brief Get the current context + * + * Returns the current active context. If contexts have been pushed onto the + * stack, returns the top context. Otherwise, returns the root context. * - * @return Pointer to the default context, or NULL if not set + * @return Pointer to the current context, or NULL if no context is set */ -DMOD_BUILTIN_API(dmenv, 1.0, dmenv_ctx_t, _get_default, (void)); +DMOD_BUILTIN_API(dmenv, 1.0, dmenv_ctx_t, _get_current_context, (void)); /** * @brief Set an environment variable (string value) diff --git a/src/dmenv.c b/src/dmenv.c index b86698f..8ce9f87 100644 --- a/src/dmenv.c +++ b/src/dmenv.c @@ -7,6 +7,10 @@ #define DMENV_MAGIC_NUMBER 0x444D454E // "DMEN" in hex #endif +#ifndef DMENV_CONTEXT_STACK_SIZE +#define DMENV_CONTEXT_STACK_SIZE 16 +#endif + /** * @brief Structure to hold an environment variable entry */ @@ -28,7 +32,16 @@ typedef struct dmenv_ctx size_t entry_count; } dmenv_ctx_internal_t; -static dmenv_ctx_t g_default_context = NULL; +/** + * @brief Global root context (previously named default context) + */ +static dmenv_ctx_t g_root_context = NULL; + +/** + * @brief Context stack for push/pop functionality + */ +static dmenv_ctx_t g_context_stack[DMENV_CONTEXT_STACK_SIZE]; +static size_t g_context_stack_top = 0; /** * @brief Helper function to find an entry by name in a context @@ -129,10 +142,25 @@ DMOD_INPUT_API_DECLARATION(dmenv, 1.0, void, _destroy, (dmenv_ctx_t ctx)) current = next; } - // If this is the default context, clear it - if (g_default_context == ctx) + // If this is the root context, clear it + if (g_root_context == ctx) { - g_default_context = NULL; + g_root_context = NULL; + } + + // Remove from context stack if present + for (size_t i = 0; i < g_context_stack_top; i++) + { + if (g_context_stack[i] == ctx) + { + // Shift remaining contexts down + for (size_t j = i; j < g_context_stack_top - 1; j++) + { + g_context_stack[j] = g_context_stack[j + 1]; + } + g_context_stack_top--; + break; + } } // Invalidate magic number @@ -153,7 +181,7 @@ DMOD_INPUT_API_DECLARATION(dmenv, 1.0, bool, _is_valid, (dmenv_ctx_t ctx)) return result; } -DMOD_INPUT_API_DECLARATION(dmenv, 1.0, void, _set_as_default, (dmenv_ctx_t ctx)) +DMOD_INPUT_API_DECLARATION(dmenv, 1.0, void, _set_root_context, (dmenv_ctx_t ctx)) { if (!dmenv_is_valid(ctx)) { @@ -162,14 +190,78 @@ DMOD_INPUT_API_DECLARATION(dmenv, 1.0, void, _set_as_default, (dmenv_ctx_t ctx)) } Dmod_EnterCritical(); - g_default_context = ctx; - DMOD_LOG_INFO("Set context %p as default\n", ctx); + g_root_context = ctx; + DMOD_LOG_INFO("Set context %p as root context\n", ctx); + Dmod_ExitCritical(); +} + +DMOD_INPUT_API_DECLARATION(dmenv, 1.0, dmenv_ctx_t, _get_root_context, (void)) +{ + return g_root_context; +} + +DMOD_INPUT_API_DECLARATION(dmenv, 1.0, bool, _push_context, (dmenv_ctx_t ctx)) +{ + if (!dmenv_is_valid(ctx)) + { + DMOD_LOG_ERROR("Invalid context\n"); + return false; + } + + Dmod_EnterCritical(); + + if (g_context_stack_top >= DMENV_CONTEXT_STACK_SIZE) + { + DMOD_LOG_ERROR("Context stack overflow\n"); + Dmod_ExitCritical(); + return false; + } + + g_context_stack[g_context_stack_top] = ctx; + g_context_stack_top++; + + DMOD_LOG_INFO("Pushed context %p onto stack (depth: %zu)\n", ctx, g_context_stack_top); + + Dmod_ExitCritical(); + return true; +} + +DMOD_INPUT_API_DECLARATION(dmenv, 1.0, dmenv_ctx_t, _pop_context, (void)) +{ + Dmod_EnterCritical(); + + if (g_context_stack_top == 0) + { + DMOD_LOG_INFO("Context stack is empty, nothing to pop\n"); + Dmod_ExitCritical(); + return NULL; + } + + g_context_stack_top--; + dmenv_ctx_t ctx = g_context_stack[g_context_stack_top]; + + DMOD_LOG_INFO("Popped context %p from stack (depth: %zu)\n", ctx, g_context_stack_top); + Dmod_ExitCritical(); + return ctx; } -DMOD_INPUT_API_DECLARATION(dmenv, 1.0, dmenv_ctx_t, _get_default, (void)) +DMOD_INPUT_API_DECLARATION(dmenv, 1.0, dmenv_ctx_t, _get_current_context, (void)) { - return g_default_context; + Dmod_EnterCritical(); + + dmenv_ctx_t ctx; + if (g_context_stack_top > 0) + { + ctx = g_context_stack[g_context_stack_top - 1]; + } + else + { + ctx = g_root_context; + } + + Dmod_ExitCritical(); + return ctx; } DMOD_INPUT_API_DECLARATION(dmenv, 1.0, bool, _set, (dmenv_ctx_t ctx, const char *name, const char *value)) @@ -463,7 +555,10 @@ DMOD_INPUT_API_DECLARATION(dmenv, 1.0, size_t, _count, (dmenv_ctx_t ctx)) #ifndef DMENV_DONT_IMPLEMENT_DMOD_API /** - * @brief Set an environment variable in the default context (DMOD API) + * @brief Set an environment variable in the current context (DMOD API) + * + * Uses the current context (top of stack if any contexts are pushed, + * otherwise the root context). * * @param Name Name of the environment variable * @param Value Value to set @@ -472,10 +567,10 @@ DMOD_INPUT_API_DECLARATION(dmenv, 1.0, size_t, _count, (dmenv_ctx_t ctx)) */ DMOD_INPUT_API_DECLARATION(Dmod, 1.0, int, _SetEnv, (const char *Name, const char *Value, int Overwrite)) { - dmenv_ctx_t ctx = dmenv_get_default(); + dmenv_ctx_t ctx = dmenv_get_current_context(); if (ctx == NULL) { - DMOD_LOG_ERROR("No default context set for Dmod_SetEnv\n"); + DMOD_LOG_ERROR("No context available for Dmod_SetEnv\n"); return -1; } @@ -496,17 +591,20 @@ DMOD_INPUT_API_DECLARATION(Dmod, 1.0, int, _SetEnv, (const char *Name, const cha } /** - * @brief Get an environment variable from the default context (DMOD API) + * @brief Get an environment variable from the current context (DMOD API) + * + * Uses the current context (top of stack if any contexts are pushed, + * otherwise the root context). * * @param Name Name of the environment variable * @return const char* Value if found, NULL otherwise */ DMOD_INPUT_API_DECLARATION(Dmod, 1.0, const char *, _GetEnv, (const char *Name)) { - dmenv_ctx_t ctx = dmenv_get_default(); + dmenv_ctx_t ctx = dmenv_get_current_context(); if (ctx == NULL) { - DMOD_LOG_ERROR("No default context set for Dmod_GetEnv\n"); + DMOD_LOG_ERROR("No context available for Dmod_GetEnv\n"); return NULL; } return dmenv_get(ctx, Name); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6bdb479..5eb665c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -22,19 +22,39 @@ target_include_directories(test_common INTERFACE ) target_link_libraries(test_common INTERFACE dmod_inc) +# Common linker options for all tests (only when not using coverage) +# The custom linker script is for embedded targets and interferes with coverage data generation +if(NOT ENABLE_COVERAGE) + set(TEST_LINK_OPTIONS -L ${DMOD_DIR}/scripts -T ${DMOD_DIR}/scripts/main.ld) +endif() + +# Coverage stubs - provides symbols normally defined by the linker script +if(ENABLE_COVERAGE) + set(COVERAGE_STUBS coverage_stubs.c) +endif() + # Test: Simple test -add_executable(test_simple test_simple.c) +add_executable(test_simple test_simple.c ${COVERAGE_STUBS}) target_link_libraries(test_simple PRIVATE dmenv unity test_common dmod_system) +if(TEST_LINK_OPTIONS) + target_link_options(test_simple PRIVATE ${TEST_LINK_OPTIONS}) +endif() add_test(NAME test_simple COMMAND test_simple) # Test: Unit tests -add_executable(test_dmenv_unit test_dmenv_unit.c) +add_executable(test_dmenv_unit test_dmenv_unit.c ${COVERAGE_STUBS}) target_link_libraries(test_dmenv_unit PRIVATE dmenv unity test_common dmod_system) +if(TEST_LINK_OPTIONS) + target_link_options(test_dmenv_unit PRIVATE ${TEST_LINK_OPTIONS}) +endif() add_test(NAME test_dmenv_unit COMMAND test_dmenv_unit) # Test: Minimal test -add_executable(test_minimal test_minimal.c) +add_executable(test_minimal test_minimal.c ${COVERAGE_STUBS}) target_link_libraries(test_minimal PRIVATE dmenv unity test_common dmod_system) +if(TEST_LINK_OPTIONS) + target_link_options(test_minimal PRIVATE ${TEST_LINK_OPTIONS}) +endif() add_test(NAME test_minimal COMMAND test_minimal) # Enable coverage for test executables if requested diff --git a/tests/coverage_stubs.c b/tests/coverage_stubs.c new file mode 100644 index 0000000..e9ad6a1 --- /dev/null +++ b/tests/coverage_stubs.c @@ -0,0 +1,15 @@ +/** + * @file coverage_stubs.c + * @brief Stub symbols for coverage builds without the embedded linker script. + * + * When building with coverage enabled, we skip the custom linker script that + * defines these symbols. This file provides stub definitions to satisfy the linker. + */ + +/* Stub sections for DMOD input/output APIs */ +char __dmod_inputs_start = 0; +char __dmod_inputs_end = 0; +char __dmod_inputs_size = 0; +char __dmod_outputs_start = 0; +char __dmod_outputs_end = 0; +char __dmod_outputs_size = 0; diff --git a/tests/test_dmenv_unit.c b/tests/test_dmenv_unit.c index 7d3ff2b..ed7b5da 100644 --- a/tests/test_dmenv_unit.c +++ b/tests/test_dmenv_unit.c @@ -211,16 +211,135 @@ void test_inheritance_override(void) { dmenv_destroy(parent); } -void test_default_context(void) { +void test_root_context(void) { dmenv_ctx_t ctx = dmenv_create(NULL); - dmenv_set_as_default(ctx); + dmenv_set_root_context(ctx); - dmenv_ctx_t retrieved = dmenv_get_default(); + dmenv_ctx_t retrieved = dmenv_get_root_context(); TEST_ASSERT_EQUAL_PTR(ctx, retrieved); dmenv_destroy(ctx); } +void test_push_pop_context(void) { + dmenv_ctx_t root = dmenv_create(NULL); + dmenv_ctx_t ctx1 = dmenv_create(NULL); + dmenv_ctx_t ctx2 = dmenv_create(NULL); + + // Set root context + dmenv_set_root_context(root); + + // Initially, current context should be root + TEST_ASSERT_EQUAL_PTR(root, dmenv_get_current_context()); + + // Push ctx1 + TEST_ASSERT_TRUE(dmenv_push_context(ctx1)); + TEST_ASSERT_EQUAL_PTR(ctx1, dmenv_get_current_context()); + + // Push ctx2 + TEST_ASSERT_TRUE(dmenv_push_context(ctx2)); + TEST_ASSERT_EQUAL_PTR(ctx2, dmenv_get_current_context()); + + // Pop ctx2 + dmenv_ctx_t popped = dmenv_pop_context(); + TEST_ASSERT_EQUAL_PTR(ctx2, popped); + TEST_ASSERT_EQUAL_PTR(ctx1, dmenv_get_current_context()); + + // Pop ctx1 + popped = dmenv_pop_context(); + TEST_ASSERT_EQUAL_PTR(ctx1, popped); + TEST_ASSERT_EQUAL_PTR(root, dmenv_get_current_context()); + + // Pop from empty stack should return NULL + popped = dmenv_pop_context(); + TEST_ASSERT_NULL(popped); + + // Current context should still be root + TEST_ASSERT_EQUAL_PTR(root, dmenv_get_current_context()); + + dmenv_destroy(ctx2); + dmenv_destroy(ctx1); + dmenv_destroy(root); +} + +void test_push_invalid_context(void) { + TEST_ASSERT_FALSE(dmenv_push_context(NULL)); +} + +void test_current_context_without_root(void) { + // Clear root context first + dmenv_set_root_context(NULL); + + // Without root context and empty stack, get_current_context should return NULL + TEST_ASSERT_NULL(dmenv_get_current_context()); + + // Create and push a context + dmenv_ctx_t ctx = dmenv_create(NULL); + TEST_ASSERT_TRUE(dmenv_push_context(ctx)); + TEST_ASSERT_EQUAL_PTR(ctx, dmenv_get_current_context()); + + // Pop it + dmenv_pop_context(); + TEST_ASSERT_NULL(dmenv_get_current_context()); + + dmenv_destroy(ctx); +} + +void test_context_stack_variables(void) { + dmenv_ctx_t root = dmenv_create(NULL); + dmenv_ctx_t child = dmenv_create(NULL); + + dmenv_set_root_context(root); + + // Set variable in root context + dmenv_set(root, "ROOT_VAR", "root_value"); + dmenv_set(root, "SHARED", "root_shared"); + + // Set variable in child context + dmenv_set(child, "CHILD_VAR", "child_value"); + dmenv_set(child, "SHARED", "child_shared"); + + // Initially current is root + TEST_ASSERT_EQUAL_STRING("root_value", dmenv_get(dmenv_get_current_context(), "ROOT_VAR")); + TEST_ASSERT_EQUAL_STRING("root_shared", dmenv_get(dmenv_get_current_context(), "SHARED")); + + // Push child context + dmenv_push_context(child); + + // Now current is child + TEST_ASSERT_EQUAL_STRING("child_value", dmenv_get(dmenv_get_current_context(), "CHILD_VAR")); + TEST_ASSERT_EQUAL_STRING("child_shared", dmenv_get(dmenv_get_current_context(), "SHARED")); + TEST_ASSERT_NULL(dmenv_get(dmenv_get_current_context(), "ROOT_VAR")); // Not in child's direct context + + // Pop child + dmenv_pop_context(); + + // Back to root + TEST_ASSERT_EQUAL_STRING("root_value", dmenv_get(dmenv_get_current_context(), "ROOT_VAR")); + TEST_ASSERT_EQUAL_STRING("root_shared", dmenv_get(dmenv_get_current_context(), "SHARED")); + + dmenv_destroy(child); + dmenv_destroy(root); +} + +void test_destroy_removes_from_stack(void) { + dmenv_ctx_t root = dmenv_create(NULL); + dmenv_ctx_t ctx = dmenv_create(NULL); + + dmenv_set_root_context(root); + dmenv_push_context(ctx); + + TEST_ASSERT_EQUAL_PTR(ctx, dmenv_get_current_context()); + + // Destroy the pushed context + dmenv_destroy(ctx); + + // Current should now be root since ctx was removed from stack + TEST_ASSERT_EQUAL_PTR(root, dmenv_get_current_context()); + + dmenv_destroy(root); +} + int main(void) { UNITY_BEGIN(); @@ -243,7 +362,12 @@ int main(void) { RUN_TEST(test_geti_decimal); RUN_TEST(test_inheritance); RUN_TEST(test_inheritance_override); - RUN_TEST(test_default_context); + RUN_TEST(test_root_context); + RUN_TEST(test_push_pop_context); + RUN_TEST(test_push_invalid_context); + RUN_TEST(test_current_context_without_root); + RUN_TEST(test_context_stack_variables); + RUN_TEST(test_destroy_removes_from_stack); return UNITY_END(); }