From 7705b6a05ac7fcac961eed0cafaf0657bbf0e787 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:13:55 +0000 Subject: [PATCH 1/4] Initial plan From 5d9e10b474edb6cd72bb49e3e0385de2c6ac2b32 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:40:26 +0000 Subject: [PATCH 2/4] Add dmod unit test framework (dmod_test.h, dmod_add_test, DMOD_TEST_STEP) --- docs/cmake-functions.md | 75 ++++++++ inc/dmod_defs.h | 31 ++++ inc/dmod_test.h | 160 ++++++++++++++++++ scripts/CMakeLists.txt | 23 +++ src/common/dmod_common.c | 1 + src/module/dmod_test_main.c | 130 ++++++++++++++ .../module/test/@DMOD_MODULE_NAME@_test.c | 28 +++ templates/module/test/CMakeLists.txt | 23 +++ templates/module/test/Makefile | 44 +++++ templates/module/test/README.md | 58 +++++++ tools-cfg.cmake | 86 ++++++++++ 11 files changed, 659 insertions(+) create mode 100644 inc/dmod_test.h create mode 100644 src/module/dmod_test_main.c create mode 100644 templates/module/test/@DMOD_MODULE_NAME@_test.c create mode 100644 templates/module/test/CMakeLists.txt create mode 100644 templates/module/test/Makefile create mode 100644 templates/module/test/README.md create mode 100644 tools-cfg.cmake diff --git a/docs/cmake-functions.md b/docs/cmake-functions.md index b341e3e6..e97345b7 100644 --- a/docs/cmake-functions.md +++ b/docs/cmake-functions.md @@ -59,6 +59,81 @@ build/ └── my_lib.zip ``` +### `dmod_add_test(moduleName version sources...)` + +Creates a DMOD test module. + +The test runner `main()` is provided automatically by the framework — do **not** +define `main()` in your test sources. Test steps are registered with the +`DMOD_TEST_STEP()` macro from `dmod_test.h` and are discovered and executed +automatically at runtime. + +The exit code of the resulting binary equals the number of failed steps, making +it suitable for use in CI pipelines. + +**Parameters:** +- `moduleName` - Name of the test module +- `version` - Module version (e.g., "1.0") +- `sources...` - List of test source files (must **not** define `main()`) + +**Example:** +```cmake +set(DMOD_MODULE_NAME my_module_tests) +set(DMOD_MODULE_VERSION "1.0") +set(DMOD_AUTHOR_NAME "Jane Smith") +set(DMOD_STACK_SIZE 2048) + +dmod_add_test(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} + test_feature_a.c + test_feature_b.c +) +``` + +**Test source example:** +```c +#include "dmod_test.h" + +/* Optional lifecycle hooks */ +void dmod_test_setup(void) { /* reset state */ } +void dmod_test_teardown(void) { /* cleanup */ } + +DMOD_TEST_STEP(addition_works) +{ + DMOD_TEST_EXPECT_EQ(1 + 1, 2); +} + +DMOD_TEST_STEP(null_pointer_check) +{ + void* ptr = get_something(); + DMOD_TEST_EXPECT_NOT_NULL(ptr); +} +``` + +**Output example:** +``` +=== DMOD Test Runner === +[ RUN ] addition_works +[ OK ] addition_works +[ RUN ] null_pointer_check +[ OK ] null_pointer_check + +=== Results: 2/2 passed === +``` + +**Available assertion macros (from `dmod_test.h`):** + +| Macro | Description | +|-------|-------------| +| `DMOD_TEST_EXPECT(cond)` | Fail if condition is false | +| `DMOD_TEST_EXPECT_TRUE(cond)` | Alias for `DMOD_TEST_EXPECT` | +| `DMOD_TEST_EXPECT_FALSE(cond)` | Fail if condition is true | +| `DMOD_TEST_EXPECT_EQ(a, b)` | Fail if `a != b` | +| `DMOD_TEST_EXPECT_NE(a, b)` | Fail if `a == b` | +| `DMOD_TEST_EXPECT_NULL(ptr)` | Fail if `ptr != NULL` | +| `DMOD_TEST_EXPECT_NOT_NULL(ptr)` | Fail if `ptr == NULL` | +| `DMOD_TEST_FAIL()` | Unconditionally fail | +| `DMOD_TEST_FAIL_MSG(msg, ...)` | Unconditionally fail with message | + ## Dependency Management Functions ### `dmod_link_modules(targetName [PRIVATE|PUBLIC|INTERFACE] modules...)` diff --git a/inc/dmod_defs.h b/inc/dmod_defs.h index 104469c4..64ba5082 100644 --- a/inc/dmod_defs.h +++ b/inc/dmod_defs.h @@ -102,12 +102,14 @@ extern "C" { #define DMOD_IRQ_SIGNATURE_PREFIX "\021DIRQ\022" #define DMOD_MAL_SIGNATURE_PREFIX "\021DMAL\022" #define DMOD_DIF_SIGNATURE_PREFIX "\021DDIF\022" +#define DMOD_TEST_SIGNATURE_PREFIX "\021DTST\022" #define DMOD_SIGNATURE_SUFFIX "\0" #define DMOD_MAKE_SIGNATURE( MODULE, VERSION, NAME ) DMOD_SIGNATURE_PREFIX #NAME "@" #MODULE ":" #VERSION DMOD_SIGNATURE_SUFFIX #define DMOD_MAKE_BUILTIN_SIGNATURE( MODULE, VERSION, NAME ) DMOD_BUILTIN_SIGNATURE_PREFIX #NAME "@" #MODULE ":" #VERSION DMOD_SIGNATURE_SUFFIX #define DMOD_MAKE_IRQ_SIGNATURE( NAME ) DMOD_IRQ_SIGNATURE_PREFIX #NAME #define DMOD_MAKE_MAL_SIGNATURE( MODULE, VERSION, NAME ) DMOD_MAL_SIGNATURE_PREFIX #NAME "@" #MODULE ":" #VERSION DMOD_SIGNATURE_SUFFIX #define DMOD_MAKE_DIF_SIGNATURE( MODULE, VERSION, NAME ) DMOD_DIF_SIGNATURE_PREFIX #NAME "@" #MODULE ":" #VERSION DMOD_SIGNATURE_SUFFIX +#define DMOD_MAKE_TEST_SIGNATURE( NAME ) DMOD_TEST_SIGNATURE_PREFIX #NAME DMOD_SIGNATURE_SUFFIX #define DMOD_MAKE_VERSION(API_VERSION, MODULE_VERSION) API_VERSION/MODULE_VERSION //============================================================================== @@ -231,6 +233,8 @@ extern "C" { #define DMOD_IRQ_MAKE_HANDLER_NAME(IRQ_NUMBER) __irq_##IRQ_NUMBER #define DMOD_IRQ_MAKE_REG_NAME(IRQ_NUMBER) __irq_##IRQ_NUMBER##_registration #define DMOD_IRQ_SIGNATURE_BUFFER_SIZE ( sizeof(DMOD_IRQ_SIGNATURE_PREFIX) + 20 ) +#define DMOD_TEST_MAKE_STEP_NAME(NAME) dmod_test_step_##NAME +#define DMOD_TEST_MAKE_REG_NAME(NAME) dmod_test_step_##NAME##_registration #define DMOD_MAL_CONNECT( MODULE, NAME, FUNCTION_NAME ) \ DMOD_FUNCTION_REDEFINITION( DMOD_MAKE_MAL_API_FUNCTION_NAME(MODULE,NAME), FUNCTION_NAME ) @@ -260,6 +264,33 @@ extern "C" { };\ static void DMOD_IRQ_MAKE_HANDLER_NAME(IRQ_NUMBER)(void) +/** + * @brief Defines a test step function and registers it for automatic discovery. + * + * Use this macro to define a test step function. All test steps are automatically + * discovered by the test runner provided by dmod_add_test. + * + * @param NAME Name of the test step (must be a valid C identifier) + * + * @note Include dmod_test.h to use assertion macros (DMOD_TEST_EXPECT_*) inside steps. + * + * Example: + * @code + * DMOD_TEST_STEP(my_step) + * { + * DMOD_TEST_EXPECT_EQ(1 + 1, 2); + * } + * @endcode + */ +#define DMOD_TEST_STEP( NAME ) \ + static void DMOD_TEST_MAKE_STEP_NAME(NAME)(void);\ + volatile const Dmod_ApiRegistration_t DMOD_TEST_MAKE_REG_NAME(NAME) DMOD_USED_SECTION(".dmod.inputs") = \ + { \ + .Function = (void*)DMOD_TEST_MAKE_STEP_NAME(NAME), \ + .Signature = DMOD_MAKE_TEST_SIGNATURE(NAME) \ + };\ + static void DMOD_TEST_MAKE_STEP_NAME(NAME)(void) + #ifdef DOXYGEN /** * @brief Defines Builtin API diff --git a/inc/dmod_test.h b/inc/dmod_test.h new file mode 100644 index 00000000..496257bb --- /dev/null +++ b/inc/dmod_test.h @@ -0,0 +1,160 @@ +/** + * @file dmod_test.h + * @brief DMOD Unit Test Framework + * + * Provides assertion macros and setup/teardown hooks for writing DMOD module + * unit tests. Include this header in your test source files. + * + * Test steps are defined with the DMOD_TEST_STEP() macro and are automatically + * discovered and executed by the test runner that is compiled in by + * dmod_add_test() in CMake. + * + * Basic usage: + * @code + * #include "dmod_test.h" + * + * DMOD_TEST_STEP(addition_works) + * { + * DMOD_TEST_EXPECT_EQ(1 + 1, 2); + * } + * @endcode + * + * Optional per-test lifecycle hooks: + * @code + * void dmod_test_setup(void) { // runs before every test step } + * void dmod_test_teardown(void) { // runs after every test step } + * @endcode + */ +#ifndef INC_DMOD_TEST_H_ +#define INC_DMOD_TEST_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "dmod.h" + +//============================================================================== +// TEST STATE +//============================================================================== + +/** + * @brief Flag indicating whether the current test step has failed. + * + * Set to non-zero by EXPECT macros on failure. + * Reset to zero before each test step by the test runner. + * Defined in dmod_test_main.c (compiled in by dmod_add_test). + */ +extern volatile int dmod_test_step_failed; + +//============================================================================== +// SETUP / TEARDOWN HOOKS +//============================================================================== + +/** + * @brief Called by the test runner before each test step. + * + * A default empty implementation is provided as a weak symbol. + * Define this function in your test module to add common setup logic. + */ +void dmod_test_setup(void); + +/** + * @brief Called by the test runner after each test step. + * + * A default empty implementation is provided as a weak symbol. + * Define this function in your test module to add common teardown logic. + */ +void dmod_test_teardown(void); + +//============================================================================== +// ASSERTION MACROS +//============================================================================== + +/** + * @brief Expect that @p condition is true. + * + * Records a failure and prints a diagnostic if the condition is false. + * The test step continues executing after the failure. + */ +#define DMOD_TEST_EXPECT( condition ) \ + do { \ + if ( !(condition) ) { \ + Dmod_Printf( "\033[31;1m FAILED\033[0m " __FILE__ ":%d: EXPECT( " #condition " )\n", __LINE__ ); \ + dmod_test_step_failed = 1; \ + } \ + } while( 0 ) + +/** @brief Alias for DMOD_TEST_EXPECT(). */ +#define DMOD_TEST_EXPECT_TRUE( condition ) DMOD_TEST_EXPECT( condition ) + +/** @brief Expect that @p condition is false. */ +#define DMOD_TEST_EXPECT_FALSE( condition ) DMOD_TEST_EXPECT( !(condition) ) + +/** + * @brief Expect that @p a equals @p b. + * + * Uses the == operator. For floating-point comparisons prefer an explicit + * tolerance check with DMOD_TEST_EXPECT(). + */ +#define DMOD_TEST_EXPECT_EQ( a, b ) \ + do { \ + if ( (a) != (b) ) { \ + Dmod_Printf( "\033[31;1m FAILED\033[0m " __FILE__ ":%d: EXPECT_EQ( " #a ", " #b " )\n", __LINE__ ); \ + dmod_test_step_failed = 1; \ + } \ + } while( 0 ) + +/** @brief Expect that @p a does not equal @p b. */ +#define DMOD_TEST_EXPECT_NE( a, b ) \ + do { \ + if ( (a) == (b) ) { \ + Dmod_Printf( "\033[31;1m FAILED\033[0m " __FILE__ ":%d: EXPECT_NE( " #a ", " #b " )\n", __LINE__ ); \ + dmod_test_step_failed = 1; \ + } \ + } while( 0 ) + +/** @brief Expect that @p ptr is NULL. */ +#define DMOD_TEST_EXPECT_NULL( ptr ) \ + do { \ + if ( (ptr) != NULL ) { \ + Dmod_Printf( "\033[31;1m FAILED\033[0m " __FILE__ ":%d: EXPECT_NULL( " #ptr " )\n", __LINE__ ); \ + dmod_test_step_failed = 1; \ + } \ + } while( 0 ) + +/** @brief Expect that @p ptr is not NULL. */ +#define DMOD_TEST_EXPECT_NOT_NULL( ptr ) \ + do { \ + if ( (ptr) == NULL ) { \ + Dmod_Printf( "\033[31;1m FAILED\033[0m " __FILE__ ":%d: EXPECT_NOT_NULL( " #ptr " )\n", __LINE__ ); \ + dmod_test_step_failed = 1; \ + } \ + } while( 0 ) + +/** + * @brief Unconditionally fail the current test step. + */ +#define DMOD_TEST_FAIL() \ + do { \ + Dmod_Printf( "\033[31;1m FAILED\033[0m " __FILE__ ":%d: FAIL()\n", __LINE__ ); \ + dmod_test_step_failed = 1; \ + } while( 0 ) + +/** + * @brief Unconditionally fail the current test step with a formatted message. + * + * @param msg printf-style format string (without trailing newline) + * @param ... optional format arguments + */ +#define DMOD_TEST_FAIL_MSG( msg, ... ) \ + do { \ + Dmod_Printf( "\033[31;1m FAILED\033[0m " __FILE__ ":%d: " msg "\n", __LINE__, ##__VA_ARGS__ ); \ + dmod_test_step_failed = 1; \ + } while( 0 ) + +#ifdef __cplusplus +} +#endif + +#endif /* INC_DMOD_TEST_H_ */ diff --git a/scripts/CMakeLists.txt b/scripts/CMakeLists.txt index cde1cda9..aed334db 100644 --- a/scripts/CMakeLists.txt +++ b/scripts/CMakeLists.txt @@ -526,6 +526,29 @@ function(dmod_add_library moduleName version) dmod_create_module(${moduleName} ${version} Library ${SOURCES}) endfunction() +# +# Function to create a DMOD test module +# +# Usage: +# dmod_add_test(moduleName version sources...) +# +# Creates an Application-type module with a pre-provided main() that +# automatically discovers and runs all test steps registered via +# DMOD_TEST_STEP(). Do NOT define main() yourself in the test sources. +# +# Lifecycle hooks dmod_test_setup() and dmod_test_teardown() are called +# before/after each step; define them in your test sources to override +# the default empty implementations. +# +function(dmod_add_test moduleName version) + set(SOURCES ${ARGN}) + + # Inject the pre-built test runner as an additional source file + list(APPEND SOURCES ${DMOD_SRC_DIR}/module/dmod_test_main.c) + + dmod_create_module(${moduleName} ${version} Application ${SOURCES}) +endfunction() + # # Function to link external modules and download their headers # diff --git a/src/common/dmod_common.c b/src/common/dmod_common.c index 97c4a401..5ad8e772 100644 --- a/src/common/dmod_common.c +++ b/src/common/dmod_common.c @@ -74,6 +74,7 @@ bool Dmod_ApiSignature_IsValid( const char* Signature ) && strncmp( Signature, DMOD_IRQ_SIGNATURE_PREFIX, sizeof( DMOD_IRQ_SIGNATURE_PREFIX ) - 1 ) != 0 && strncmp( Signature, DMOD_MAL_SIGNATURE_PREFIX, sizeof( DMOD_MAL_SIGNATURE_PREFIX ) - 1 ) != 0 && strncmp( Signature, DMOD_DIF_SIGNATURE_PREFIX, sizeof( DMOD_DIF_SIGNATURE_PREFIX ) - 1 ) != 0 + && strncmp( Signature, DMOD_TEST_SIGNATURE_PREFIX, sizeof( DMOD_TEST_SIGNATURE_PREFIX ) - 1 ) != 0 ) { return false; diff --git a/src/module/dmod_test_main.c b/src/module/dmod_test_main.c new file mode 100644 index 00000000..3d7837f0 --- /dev/null +++ b/src/module/dmod_test_main.c @@ -0,0 +1,130 @@ +/** + * @file dmod_test_main.c + * @brief DMOD unit-test runner + * + * This file provides the main() entry point for test modules created with + * dmod_add_test(). It automatically discovers all test steps that were + * registered via the DMOD_TEST_STEP() macro (stored in the .dmod.inputs + * section) and executes them one by one, calling dmod_test_setup() before + * each step and dmod_test_teardown() after each step. + * + * Return value of main() equals the number of failed test steps, so the + * test binary can be used directly in CI pipelines. + */ + +#include "dmod_test.h" + +#include +#include +#include + +/* Defined in the module's generated _header.c - points to ModuleHeader which + * is at offset 0 in the binary, i.e. equals Context->Data (binary base). */ +extern volatile const Dmod_ModuleHeader_t* DMOD_Header; + +//============================================================================== +// TEST STATE +//============================================================================== + +volatile int dmod_test_step_failed = 0; + +//============================================================================== +// DEFAULT SETUP / TEARDOWN (weak) +//============================================================================== + +DMOD_WEAK_SYMBOL void dmod_test_setup(void) {} +DMOD_WEAK_SYMBOL void dmod_test_teardown(void) {} + +//============================================================================== +// INTERNAL HELPERS +//============================================================================== + +typedef void (*DmodTestStepFn)(void); + +/* Return 1 if sig starts with the test signature prefix, 0 otherwise. */ +static int IsTestSignature( const char* sig ) +{ + const char prefix[] = DMOD_TEST_SIGNATURE_PREFIX; + size_t i; + + for( i = 0; i < sizeof( prefix ) - 1; i++ ) + { + if( sig[i] != prefix[i] ) + { + return 0; + } + } + return 1; +} + +/* Return the step name embedded in the signature (the part after the prefix). */ +static const char* GetTestName( const char* sig ) +{ + return sig + sizeof( DMOD_TEST_SIGNATURE_PREFIX ) - 1; +} + +//============================================================================== +// MAIN +//============================================================================== + +int main( int argc, char* argv[] ) +{ + (void)argc; + (void)argv; + + /* Obtain the footer via the module header. + * DMOD_Header == binary base address (ModuleHeader is at offset 0). + * After loading, Footer.Ptr is the relocated pointer to __footer_start. */ + Dmod_ModuleFooter_t* footer = (Dmod_ModuleFooter_t*)DMOD_Header->Footer.Ptr; + uint8_t* base = (uint8_t*)(uintptr_t)DMOD_Header; + + Dmod_ApiRegistration_t* entries = + (Dmod_ApiRegistration_t*)( base + footer->Inputs.SectionStart ); + uint32_t count = footer->Inputs.SectionSize / sizeof( Dmod_ApiRegistration_t ); + + int total_steps = 0; + int failed_steps = 0; + + Dmod_Printf( "=== DMOD Test Runner ===\n" ); + + for( uint32_t i = 0; i < count; i++ ) + { + if( entries[i].Function == NULL ) { continue; } + if( entries[i].Signature == NULL ) { continue; } + if( !IsTestSignature( entries[i].Signature ) ) { continue; } + + const char* name = GetTestName( entries[i].Signature ); + DmodTestStepFn fn = (DmodTestStepFn)entries[i].Function; + + Dmod_Printf( "[ RUN ] %s\n", name ); + + dmod_test_step_failed = 0; + dmod_test_setup(); + fn(); + dmod_test_teardown(); + + total_steps++; + + if( dmod_test_step_failed ) + { + Dmod_Printf( "[FAILED] %s\n", name ); + failed_steps++; + } + else + { + Dmod_Printf( "[ OK ] %s\n", name ); + } + } + + if( total_steps == 0 ) + { + Dmod_Printf( "\nNo test steps found.\n" ); + } + else + { + Dmod_Printf( "\n=== Results: %d/%d passed ===\n", + total_steps - failed_steps, total_steps ); + } + + return failed_steps; +} diff --git a/templates/module/test/@DMOD_MODULE_NAME@_test.c b/templates/module/test/@DMOD_MODULE_NAME@_test.c new file mode 100644 index 00000000..c45aa67e --- /dev/null +++ b/templates/module/test/@DMOD_MODULE_NAME@_test.c @@ -0,0 +1,28 @@ +/** + * @file @DMOD_MODULE_NAME@_test.c + * @brief Test steps for @DMOD_MODULE_NAME@ + * + * Each DMOD_TEST_STEP defines one test step. Steps are discovered + * automatically at runtime and executed by the test runner. + * + * Optionally define dmod_test_setup() / dmod_test_teardown() to run code + * before / after every step. + */ +#include "dmod_test.h" + +/* Optional: runs before every test step */ +/* void dmod_test_setup(void) {} */ + +/* Optional: runs after every test step */ +/* void dmod_test_teardown(void) {} */ + +DMOD_TEST_STEP(example_pass) +{ + DMOD_TEST_EXPECT_EQ( 1 + 1, 2 ); +} + +DMOD_TEST_STEP(example_fail) +{ + /* Remove or replace this step - it always fails to demonstrate output */ + DMOD_TEST_FAIL_MSG( "This step intentionally fails - replace with real tests" ); +} diff --git a/templates/module/test/CMakeLists.txt b/templates/module/test/CMakeLists.txt new file mode 100644 index 00000000..fe6cac76 --- /dev/null +++ b/templates/module/test/CMakeLists.txt @@ -0,0 +1,23 @@ +# Name of the module +set(DMOD_MODULE_NAME @DMOD_MODULE_NAME@) + +# Version (should be string in format "Major.Minor") +set(DMOD_MODULE_VERSION "0.1") + +# Author (should be string) +set(DMOD_AUTHOR_NAME @DMOD_AUTHOR_NAME@) + +# Stack size for the module (should be integer) +set(DMOD_STACK_SIZE @DMOD_STACK_SIZE@) + +# +# dmod_add_test - create a test module +# +# The test runner main() is provided automatically. Do NOT define main() +# in your source files. Register test steps with the DMOD_TEST_STEP() +# macro from dmod_test.h. +# +dmod_add_test(${DMOD_MODULE_NAME} "0.1" + # List of test source files - can include C and C++ files + @DMOD_MODULE_NAME@_test.c +) diff --git a/templates/module/test/Makefile b/templates/module/test/Makefile new file mode 100644 index 00000000..051d2c86 --- /dev/null +++ b/templates/module/test/Makefile @@ -0,0 +1,44 @@ +# ############################################################################# +# +# This is an example of a DMOD test module. +# +# ############################################################################# +DMOD_DIR=../../.. + +# ----------------------------------------------------------------------------- +# Paths initialization +# ----------------------------------------------------------------------------- +include $(DMOD_DIR)/paths.mk + +# ----------------------------------------------------------------------------- +# Module configuration +# ----------------------------------------------------------------------------- + +# The name of the module +DMOD_MODULE_NAME=@MODULE_NAME@ + +# The version of the module +DMOD_MODULE_VERSION=0.1 + +# The name of the author +DMOD_AUTHOR_NAME=John Doe + +# The list of C sources (do NOT add main.c - the test runner provides main) +DMOD_CSOURCES=@MODULE_NAME@_test.c + +# The list of C++ sources +DMOD_CXXSOURCES= + +# The list of include directories +DMOD_INC_DIRS= + +# The list of libraries to link +DMOD_LIBS= + +# The list of definitions +DMOD_DEFINITIONS= + +# ----------------------------------------------------------------------------- +# Include the dmod app makefile (test modules are Application-type) +# ----------------------------------------------------------------------------- +include $(DMOD_DMF_APP_FILE_PATH) diff --git a/templates/module/test/README.md b/templates/module/test/README.md new file mode 100644 index 00000000..2f82ddbc --- /dev/null +++ b/templates/module/test/README.md @@ -0,0 +1,58 @@ +# DMOD Test Module Template + +This template creates a DMOD test module using the built-in unit test framework. + +## Quick Start + +1. Copy this directory to your module directory. +2. Rename `@DMOD_MODULE_NAME@_test.c` to `_test.c` and adapt it. +3. Update `CMakeLists.txt` (or `Makefile`) with your module name and test sources. +4. Build and run the resulting `.dmf` with `dmod_loader`. + +## Usage + +Test steps are defined with the `DMOD_TEST_STEP()` macro from `dmod_test.h`: + +```c +#include "dmod_test.h" + +DMOD_TEST_STEP(my_feature_works) +{ + DMOD_TEST_EXPECT_EQ(compute_result(), EXPECTED_VALUE); + DMOD_TEST_EXPECT_NOT_NULL(some_pointer); +} +``` + +The test runner provided by `dmod_add_test` discovers all steps automatically +and prints a summary. The exit code equals the number of failed steps. + +## Lifecycle Hooks + +Define `dmod_test_setup()` and/or `dmod_test_teardown()` in any source file +to run code before/after **every** test step: + +```c +void dmod_test_setup(void) +{ + // reset state, open resources, ... +} + +void dmod_test_teardown(void) +{ + // cleanup, close resources, ... +} +``` + +## Assertion Macros + +| Macro | Description | +|-------|-------------| +| `DMOD_TEST_EXPECT(cond)` | Fail if condition is false | +| `DMOD_TEST_EXPECT_TRUE(cond)` | Alias for DMOD_TEST_EXPECT | +| `DMOD_TEST_EXPECT_FALSE(cond)` | Fail if condition is true | +| `DMOD_TEST_EXPECT_EQ(a, b)` | Fail if a != b | +| `DMOD_TEST_EXPECT_NE(a, b)` | Fail if a == b | +| `DMOD_TEST_EXPECT_NULL(ptr)` | Fail if ptr != NULL | +| `DMOD_TEST_EXPECT_NOT_NULL(ptr)` | Fail if ptr == NULL | +| `DMOD_TEST_FAIL()` | Unconditionally fail | +| `DMOD_TEST_FAIL_MSG(msg, ...)` | Unconditionally fail with message | diff --git a/tools-cfg.cmake b/tools-cfg.cmake new file mode 100644 index 00000000..1ee55e8a --- /dev/null +++ b/tools-cfg.cmake @@ -0,0 +1,86 @@ +#================================================================================================================================ +# Default tools configuration +#================================================================================================================================ + +# +# Default configuration options +# +set(DMOD_USE_STDLIB ON ) +set(DMOD_USE_STDIO ON ) +set(DMOD_USE_ASSERT ON ) +set(DMOD_USE_PTHREAD ON ) +set(DMOD_USE_MMAN ON ) +set(DMOD_BUILD_TESTS ON ) +set(DMOD_BUILD_EXAMPLES ON ) +set(DMOD_BUILD_TOOLS ON ) + +# +# Toolchain configuration +# +if(NOT DEFINED COMPILER_PATH) + set(COMPILER_PATH "") +endif() +if(NOT DEFINED CROSS_COMPILE) + set(CROSS_COMPILE "") +endif() + + + +# +# Toolchain configuration +# +if(NOT DEFINED CROSS_COMPILE) + set(CROSS_COMPILE "") +endif() + +find_program(GCC ${CROSS_COMPILE}gcc) +if(NOT GCC) + message(FATAL_ERROR "GCC compiler not found") +endif() + +find_program(GXX ${CROSS_COMPILE}g++) +if(NOT GXX) + message(FATAL_ERROR "G++ compiler not found") +endif() + +find_program(LD ${CROSS_COMPILE}ld) +if(NOT LD) + message(FATAL_ERROR "Linker not found") +endif() + +find_program(OBJDUMP ${CROSS_COMPILE}objdump) +if(NOT OBJDUMP) + message(FATAL_ERROR "objdump not found") +endif() + +find_program(OBJCOPY ${CROSS_COMPILE}objcopy) +if(NOT OBJCOPY) + message(FATAL_ERROR "objcopy not found") +endif() + +find_program(AR ${CROSS_COMPILE}ar) +if(NOT AR) + message(FATAL_ERROR "ar not found") +endif() + +find_program(SIZE ${CROSS_COMPILE}size) +if(NOT SIZE) + message(FATAL_ERROR "size not found") +endif() + +# ============================================================================== +# CMake Configuration +# ============================================================================== +set(CMAKE_C_COMPILER "${GCC}" CACHE STRING "C compiler") +set(CMAKE_CXX_COMPILER "${GXX}" CACHE STRING "C++ compiler") +set(CMAKE_LINKER "${LD}" CACHE STRING "Linker") +set(CMAKE_OBJDUMP "${OBJDUMP}" CACHE STRING "Objdump") +set(CMAKE_OBJCOPY "${OBJCOPY}" CACHE STRING "Objcopy") +set(CMAKE_SIZE "${SIZE}" CACHE STRING "Size") +set(CMAKE_AR "${AR}" CACHE STRING "Archiver") +set(MAKE make CACHE STRING "Make") +set(MKDIR mkdir CACHE STRING "Mkdir") +set(RM rm CACHE STRING "Rm") +set(CMAKE_C_FLAGS "-Wall -Werror -std=c11 ${CPUCONFIG_CFLAGS}") +set(CMAKE_CXX_FLAGS "-Wall -Werror -std=c++17 ${CPUCONFIG_CXXFLAGS}") +set(CMAKE_LFLAGS "${CPUCONFIG_LDFLAGS}") \ No newline at end of file From b22a178e8eff31e9b11ce1236ae7e170c25acec5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:59:11 +0000 Subject: [PATCH 3/4] Refactor test runner: move helpers to dmod, add CLI filtering, add example CI test - Add Dmod_ApiSignature_IsTest() to dmod_common.c / dmod.h alongside IsMal() and IsBuiltin() - Add Dmod_Module_GetInputs() to dmod_module.c / dmod_module.h to encapsulate footer/section pointer arithmetic - Rewrite dmod_test_main.c to use the new proper APIs instead of inline magic; remove IsTestSignature() / GetTestName() helpers; add command-line step-name filtering (argv[1..] as allowlist) - Add setup/teardown stubs (uncommented) to the test module template - Add examples/module/test_example/ as a self-contained dmod_add_test example with arithmetic, boolean, and null-check steps - Add CI step in .github/workflows/ci.yml to build and run the example test module via dmod_loader --- .github/workflows/ci.yml | 6 ++ examples/module/CMakeLists.txt | 3 +- examples/module/test_example/CMakeLists.txt | 7 +++ .../module/test_example/test_example_test.c | 38 +++++++++++++ inc/dmod.h | 1 + inc/dmod_module.h | 16 ++++++ src/common/dmod_common.c | 12 ++++ src/module/dmod_module.c | 36 ++++++++++++ src/module/dmod_test_main.c | 56 ++++++++----------- .../module/test/@DMOD_MODULE_NAME@_test.c | 16 ++++-- 10 files changed, 152 insertions(+), 39 deletions(-) create mode 100644 examples/module/test_example/CMakeLists.txt create mode 100644 examples/module/test_example/test_example_test.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0eb9d867..eaae1b5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,6 +77,12 @@ jobs: ./examples/system/dmod_loader/dmod_loader ./dmf/log_step.dmf echo "log_step module loaded and executed successfully" + - name: Run example dmod_add_test module + working-directory: build + run: | + ./examples/system/dmod_loader/dmod_loader ./dmf/test_example.dmf + echo "test_example module passed all test steps" + - name: Run manifest library tests working-directory: build run: ./tests/tests_dmod_manifest diff --git a/examples/module/CMakeLists.txt b/examples/module/CMakeLists.txt index c880568d..c7ca329c 100644 --- a/examples/module/CMakeLists.txt +++ b/examples/module/CMakeLists.txt @@ -1,3 +1,4 @@ add_subdirectory(library) add_subdirectory(application) -add_subdirectory(log_step) \ No newline at end of file +add_subdirectory(log_step) +add_subdirectory(test_example) \ No newline at end of file diff --git a/examples/module/test_example/CMakeLists.txt b/examples/module/test_example/CMakeLists.txt new file mode 100644 index 00000000..9ecc8d25 --- /dev/null +++ b/examples/module/test_example/CMakeLists.txt @@ -0,0 +1,7 @@ +set(DMOD_MODULE_NAME test_example) +set(DMOD_MODULE_VERSION "1.0") +set(DMOD_AUTHOR_NAME "DMOD") + +dmod_add_test(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} + test_example_test.c +) diff --git a/examples/module/test_example/test_example_test.c b/examples/module/test_example/test_example_test.c new file mode 100644 index 00000000..9e1c1c58 --- /dev/null +++ b/examples/module/test_example/test_example_test.c @@ -0,0 +1,38 @@ +/** + * @file test_example_test.c + * @brief Example DMOD test module + * + * Demonstrates the dmod_add_test / DMOD_TEST_STEP framework. + * All steps here must pass so the CI job exits with 0. + */ +#include "dmod_test.h" + +void dmod_test_setup(void) +{ + /* runs before every step */ +} + +void dmod_test_teardown(void) +{ + /* runs after every step */ +} + +DMOD_TEST_STEP(arithmetic) +{ + DMOD_TEST_EXPECT_EQ( 1 + 1, 2 ); + DMOD_TEST_EXPECT_NE( 1 + 1, 3 ); +} + +DMOD_TEST_STEP(boolean) +{ + DMOD_TEST_EXPECT_TRUE( 1 ); + DMOD_TEST_EXPECT_FALSE( 0 ); +} + +DMOD_TEST_STEP(null_checks) +{ + void* p = (void*)0; + DMOD_TEST_EXPECT_NULL( p ); + p = (void*)1; + DMOD_TEST_EXPECT_NOT_NULL( p ); +} diff --git a/inc/dmod.h b/inc/dmod.h index c5a6fcac..244ae9b2 100644 --- a/inc/dmod.h +++ b/inc/dmod.h @@ -93,6 +93,7 @@ extern bool Dmod_ApiSignature_ReadModuleVersion( const char* Signature, extern bool Dmod_ApiSignature_AreCompatible( const char* Signature1, const char* Signature2 ); extern bool Dmod_ApiSignature_IsMal( const char* Signature ); extern bool Dmod_ApiSignature_IsBuiltin( const char* Signature ); +extern bool Dmod_ApiSignature_IsTest( const char* Signature ); DMOD_BUILTIN_API( Dmod, 1.0, void , _SetLogLevel, ( Dmod_LogLevel_t LogLevel ) ); DMOD_BUILTIN_API( Dmod, 1.0, void , _SetModuleLogLevel, ( const char* ModuleName, Dmod_LogLevel_t LogLevel ) ); diff --git a/inc/dmod_module.h b/inc/dmod_module.h index ce61a080..2d601a17 100644 --- a/inc/dmod_module.h +++ b/inc/dmod_module.h @@ -20,6 +20,22 @@ extern "C" { */ extern Dmod_LogLevel_t Dmod_GetLogLevel(void); +/** + * @brief Get the input API registrations of the running module. + * + * Reads the module footer to locate the .dmod.inputs section and returns + * a pointer to its entries together with the entry count. This is the + * canonical way for module-side code to iterate its own registrations; + * it avoids open-coding the footer pointer arithmetic in multiple places. + * + * @param outEntries Set to the first Dmod_ApiRegistration_t in the section. + * Must not be NULL. + * @param outCount Set to the number of entries in the section. + * Must not be NULL. + * @return true on success, false if the module header or footer is unavailable. + */ +extern bool Dmod_Module_GetInputs( Dmod_ApiRegistration_t** outEntries, uint32_t* outCount ); + #ifdef __cplusplus } #endif diff --git a/src/common/dmod_common.c b/src/common/dmod_common.c index 5ad8e772..394e74b4 100644 --- a/src/common/dmod_common.c +++ b/src/common/dmod_common.c @@ -397,6 +397,18 @@ bool Dmod_ApiSignature_IsBuiltin( const char* Signature ) return strncmp( Signature, DMOD_BUILTIN_SIGNATURE_PREFIX, sizeof(DMOD_BUILTIN_SIGNATURE_PREFIX) - 1 ) == 0; } +/** + * @brief Check if API signature is a test step + * + * @param Signature API signature + * + * @return true if signature is a test step, false otherwise + */ +bool Dmod_ApiSignature_IsTest( const char* Signature ) +{ + return strncmp( Signature, DMOD_TEST_SIGNATURE_PREFIX, sizeof(DMOD_TEST_SIGNATURE_PREFIX) - 1 ) == 0; +} + //============================================================================== // LOCAL FUNCTIONS IMPLEMENTATIONS //============================================================================== diff --git a/src/module/dmod_module.c b/src/module/dmod_module.c index 96cc08d1..3d7e7549 100644 --- a/src/module/dmod_module.c +++ b/src/module/dmod_module.c @@ -1,5 +1,8 @@ #include "dmod.h" +/* Every module binary defines DMOD_Header in its generated _header.c file. */ +extern volatile const Dmod_ModuleHeader_t* DMOD_Header; + /** * @brief Get the effective log level for the current module * @@ -25,3 +28,36 @@ Dmod_LogLevel_t Dmod_GetLogLevel(void) return Dmod_GetModuleLogLevel(s_ctx); } +/** + * @brief Get the input API registrations of the running module. + * + * Reads the module footer to locate the .dmod.inputs section and returns + * a pointer to the first Dmod_ApiRegistration_t entry together with the + * number of entries. This encapsulates the footer pointer arithmetic so + * callers do not need to duplicate it. + * + * @param outEntries Set to the first entry in the .dmod.inputs section. + * @param outCount Set to the number of entries. + * @return true on success, false if a required pointer is NULL. + */ +bool Dmod_Module_GetInputs( Dmod_ApiRegistration_t** outEntries, uint32_t* outCount ) +{ + if( outEntries == NULL || outCount == NULL ) + { + return false; + } + + if( DMOD_Header == NULL || DMOD_Header->Footer.Ptr == NULL ) + { + return false; + } + + Dmod_ModuleFooter_t* footer = (Dmod_ModuleFooter_t*)DMOD_Header->Footer.Ptr; + uint8_t* base = (uint8_t*)(uintptr_t)DMOD_Header; + + *outEntries = (Dmod_ApiRegistration_t*)( base + footer->Inputs.SectionStart ); + *outCount = footer->Inputs.SectionSize / sizeof( Dmod_ApiRegistration_t ); + + return true; +} + diff --git a/src/module/dmod_test_main.c b/src/module/dmod_test_main.c index 3d7837f0..3b94ffac 100644 --- a/src/module/dmod_test_main.c +++ b/src/module/dmod_test_main.c @@ -8,20 +8,19 @@ * section) and executes them one by one, calling dmod_test_setup() before * each step and dmod_test_teardown() after each step. * + * Optional command-line filtering: pass one or more step names as arguments + * to run only those steps. With no arguments all steps are executed. + * * Return value of main() equals the number of failed test steps, so the * test binary can be used directly in CI pipelines. */ #include "dmod_test.h" +#include "dmod_module.h" #include -#include #include -/* Defined in the module's generated _header.c - points to ModuleHeader which - * is at offset 0 in the binary, i.e. equals Context->Data (binary base). */ -extern volatile const Dmod_ModuleHeader_t* DMOD_Header; - //============================================================================== // TEST STATE //============================================================================== @@ -41,26 +40,25 @@ DMOD_WEAK_SYMBOL void dmod_test_teardown(void) {} typedef void (*DmodTestStepFn)(void); -/* Return 1 if sig starts with the test signature prefix, 0 otherwise. */ -static int IsTestSignature( const char* sig ) +/* Return 1 if this step should run given the argv filter, 0 otherwise. */ +static int ShouldRunStep( const char* name, int argc, char* argv[] ) { - const char prefix[] = DMOD_TEST_SIGNATURE_PREFIX; - size_t i; + int i; + + if( argc <= 1 ) + { + return 1; /* no filter: run everything */ + } - for( i = 0; i < sizeof( prefix ) - 1; i++ ) + for( i = 1; i < argc; i++ ) { - if( sig[i] != prefix[i] ) + if( strcmp( name, argv[i] ) == 0 ) { - return 0; + return 1; } } - return 1; -} -/* Return the step name embedded in the signature (the part after the prefix). */ -static const char* GetTestName( const char* sig ) -{ - return sig + sizeof( DMOD_TEST_SIGNATURE_PREFIX ) - 1; + return 0; } //============================================================================== @@ -69,33 +67,27 @@ static const char* GetTestName( const char* sig ) int main( int argc, char* argv[] ) { - (void)argc; - (void)argv; - - /* Obtain the footer via the module header. - * DMOD_Header == binary base address (ModuleHeader is at offset 0). - * After loading, Footer.Ptr is the relocated pointer to __footer_start. */ - Dmod_ModuleFooter_t* footer = (Dmod_ModuleFooter_t*)DMOD_Header->Footer.Ptr; - uint8_t* base = (uint8_t*)(uintptr_t)DMOD_Header; - - Dmod_ApiRegistration_t* entries = - (Dmod_ApiRegistration_t*)( base + footer->Inputs.SectionStart ); - uint32_t count = footer->Inputs.SectionSize / sizeof( Dmod_ApiRegistration_t ); + Dmod_ApiRegistration_t* entries = NULL; + uint32_t count = 0; int total_steps = 0; int failed_steps = 0; + Dmod_Module_GetInputs( &entries, &count ); + Dmod_Printf( "=== DMOD Test Runner ===\n" ); for( uint32_t i = 0; i < count; i++ ) { if( entries[i].Function == NULL ) { continue; } if( entries[i].Signature == NULL ) { continue; } - if( !IsTestSignature( entries[i].Signature ) ) { continue; } + if( !Dmod_ApiSignature_IsTest( entries[i].Signature ) ) { continue; } - const char* name = GetTestName( entries[i].Signature ); + const char* name = Dmod_ApiSignature_GetName( entries[i].Signature ); DmodTestStepFn fn = (DmodTestStepFn)entries[i].Function; + if( !ShouldRunStep( name, argc, argv ) ) { continue; } + Dmod_Printf( "[ RUN ] %s\n", name ); dmod_test_step_failed = 0; diff --git a/templates/module/test/@DMOD_MODULE_NAME@_test.c b/templates/module/test/@DMOD_MODULE_NAME@_test.c index c45aa67e..9f52a248 100644 --- a/templates/module/test/@DMOD_MODULE_NAME@_test.c +++ b/templates/module/test/@DMOD_MODULE_NAME@_test.c @@ -5,16 +5,20 @@ * Each DMOD_TEST_STEP defines one test step. Steps are discovered * automatically at runtime and executed by the test runner. * - * Optionally define dmod_test_setup() / dmod_test_teardown() to run code - * before / after every step. + * dmod_test_setup() runs before every step and dmod_test_teardown() + * runs after every step. Remove them if no common fixture is needed. */ #include "dmod_test.h" -/* Optional: runs before every test step */ -/* void dmod_test_setup(void) {} */ +void dmod_test_setup(void) +{ + /* initialise fixtures before every step */ +} -/* Optional: runs after every test step */ -/* void dmod_test_teardown(void) {} */ +void dmod_test_teardown(void) +{ + /* clean up after every step */ +} DMOD_TEST_STEP(example_pass) { From 0d5f5aa6d50f04fdf30b9cd524d0805326f5ac9e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:38:53 +0000 Subject: [PATCH 4/4] Add Dmod_RunTests/Dmod_RunModuleTests; refactor test_main to register fixtures in .dmod.inputs --- inc/dmod.h | 1 + inc/dmod_system.h | 2 + src/module/dmod_test_main.c | 123 +++++++++-------------------- src/system/dmod_system.c | 42 ++++++++++ src/system/public/dmod_dmf_cb.c | 132 ++++++++++++++++++++++++++++++++ 5 files changed, 212 insertions(+), 88 deletions(-) diff --git a/inc/dmod.h b/inc/dmod.h index 244ae9b2..3a256aba 100644 --- a/inc/dmod.h +++ b/inc/dmod.h @@ -125,6 +125,7 @@ DMOD_BUILTIN_API( Dmod, 1.0, bool , _IsFunctionConnected, (void* FunctionP DMOD_BUILTIN_API( Dmod, 1.0, Dmod_Context_t*, _GetNextDifModule, (const char* DifSignature, Dmod_Context_t* Previous) ); DMOD_BUILTIN_API( Dmod, 1.0, void* , _GetDifFunction, (Dmod_Context_t* Context, const char* DifSignature) ); DMOD_BUILTIN_API( Dmod, 1.0, const char*, _GetName, (Dmod_Context_t* Context) ); +DMOD_BUILTIN_API( Dmod, 1.0, int , _RunTests, (Dmod_Context_t* Context, int argc, char* argv[]) ); DMOD_BUILTIN_API( Dmod, 1.0, bool , _AddPackageBuffer, ( const void* Buffer, size_t Size, uint32_t* outIndex ) ); DMOD_BUILTIN_API( Dmod, 1.0, bool , _AddPackageFile, ( const char* FilePath, uint32_t* outIndex ) ); diff --git a/inc/dmod_system.h b/inc/dmod_system.h index ca75aa48..6d25c1c1 100644 --- a/inc/dmod_system.h +++ b/inc/dmod_system.h @@ -94,6 +94,8 @@ extern int Dmod_Deinit ( Dmod_Context_t* Context ); extern int Dmod_Signal ( Dmod_Context_t* Context, int SignalNumber ); extern int Dmod_Irq ( Dmod_Context_t* Context, int IrqNumber ); extern void Dmod_IrqAll ( int IrqNumber ); +extern int Dmod_RunTests ( Dmod_Context_t* Context, int argc, char* argv[] ); +extern int Dmod_RunModuleTests ( const char* ModuleName, int argc, char* argv[] ); // DMF Getters extern uint64_t Dmod_GetStackSize ( Dmod_Context_t* Context ); diff --git a/src/module/dmod_test_main.c b/src/module/dmod_test_main.c index 3b94ffac..c9efc982 100644 --- a/src/module/dmod_test_main.c +++ b/src/module/dmod_test_main.c @@ -3,23 +3,22 @@ * @brief DMOD unit-test runner * * This file provides the main() entry point for test modules created with - * dmod_add_test(). It automatically discovers all test steps that were - * registered via the DMOD_TEST_STEP() macro (stored in the .dmod.inputs - * section) and executes them one by one, calling dmod_test_setup() before - * each step and dmod_test_teardown() after each step. + * dmod_add_test(). It registers three special .dmod.inputs entries so the + * system-side Dmod_RunTests() can drive the test lifecycle without accessing + * module internals directly: * - * Optional command-line filtering: pass one or more step names as arguments - * to run only those steps. With no arguments all steps are executed. + * __step_failed__ — address of the dmod_test_step_failed flag + * __setup__ — address of the dmod_test_setup fixture hook + * __teardown__ — address of the dmod_test_teardown fixture hook * - * Return value of main() equals the number of failed test steps, so the - * test binary can be used directly in CI pipelines. + * Actual test steps are registered by the DMOD_TEST_STEP() macro. + * + * main() delegates entirely to Dmod_RunTests() and returns its result, so + * the exit code equals the number of failed steps (0 = all passed). */ #include "dmod_test.h" -#include "dmod_module.h" - -#include -#include +#include "dmod.h" //============================================================================== // TEST STATE @@ -27,6 +26,15 @@ volatile int dmod_test_step_failed = 0; +/* Register the address of dmod_test_step_failed so Dmod_RunTests() can reset + * and read it without accessing module internals directly. */ +volatile const Dmod_ApiRegistration_t dmod_test_step_failed_registration + DMOD_USED_SECTION(".dmod.inputs") = +{ + .Function = (void*)&dmod_test_step_failed, + .Signature = DMOD_MAKE_TEST_SIGNATURE(__step_failed__) +}; + //============================================================================== // DEFAULT SETUP / TEARDOWN (weak) //============================================================================== @@ -34,32 +42,22 @@ volatile int dmod_test_step_failed = 0; DMOD_WEAK_SYMBOL void dmod_test_setup(void) {} DMOD_WEAK_SYMBOL void dmod_test_teardown(void) {} -//============================================================================== -// INTERNAL HELPERS -//============================================================================== - -typedef void (*DmodTestStepFn)(void); - -/* Return 1 if this step should run given the argv filter, 0 otherwise. */ -static int ShouldRunStep( const char* name, int argc, char* argv[] ) +/* Register the setup and teardown hooks so Dmod_RunTests() can call them. + * The linker resolves these to the strong definitions if the user provides + * them, overriding the weak defaults above. */ +volatile const Dmod_ApiRegistration_t dmod_test_setup_registration + DMOD_USED_SECTION(".dmod.inputs") = { - int i; - - if( argc <= 1 ) - { - return 1; /* no filter: run everything */ - } + .Function = (void*)dmod_test_setup, + .Signature = DMOD_MAKE_TEST_SIGNATURE(__setup__) +}; - for( i = 1; i < argc; i++ ) - { - if( strcmp( name, argv[i] ) == 0 ) - { - return 1; - } - } - - return 0; -} +volatile const Dmod_ApiRegistration_t dmod_test_teardown_registration + DMOD_USED_SECTION(".dmod.inputs") = +{ + .Function = (void*)dmod_test_teardown, + .Signature = DMOD_MAKE_TEST_SIGNATURE(__teardown__) +}; //============================================================================== // MAIN @@ -67,56 +65,5 @@ static int ShouldRunStep( const char* name, int argc, char* argv[] ) int main( int argc, char* argv[] ) { - Dmod_ApiRegistration_t* entries = NULL; - uint32_t count = 0; - - int total_steps = 0; - int failed_steps = 0; - - Dmod_Module_GetInputs( &entries, &count ); - - Dmod_Printf( "=== DMOD Test Runner ===\n" ); - - for( uint32_t i = 0; i < count; i++ ) - { - if( entries[i].Function == NULL ) { continue; } - if( entries[i].Signature == NULL ) { continue; } - if( !Dmod_ApiSignature_IsTest( entries[i].Signature ) ) { continue; } - - const char* name = Dmod_ApiSignature_GetName( entries[i].Signature ); - DmodTestStepFn fn = (DmodTestStepFn)entries[i].Function; - - if( !ShouldRunStep( name, argc, argv ) ) { continue; } - - Dmod_Printf( "[ RUN ] %s\n", name ); - - dmod_test_step_failed = 0; - dmod_test_setup(); - fn(); - dmod_test_teardown(); - - total_steps++; - - if( dmod_test_step_failed ) - { - Dmod_Printf( "[FAILED] %s\n", name ); - failed_steps++; - } - else - { - Dmod_Printf( "[ OK ] %s\n", name ); - } - } - - if( total_steps == 0 ) - { - Dmod_Printf( "\nNo test steps found.\n" ); - } - else - { - Dmod_Printf( "\n=== Results: %d/%d passed ===\n", - total_steps - failed_steps, total_steps ); - } - - return failed_steps; + return Dmod_RunTests( Dmod_GetModuleContext( Dmod_GetCurrentModuleName() ), argc, argv ); } diff --git a/src/system/dmod_system.c b/src/system/dmod_system.c index b59b2db7..6e6524a1 100644 --- a/src/system/dmod_system.c +++ b/src/system/dmod_system.c @@ -1927,6 +1927,48 @@ int Dmod_RunModule(const char* Module, int argc, char *argv[]) return result; } +/** + * @brief Run all test steps registered in a named module + * + * Loads the module identified by @p ModuleName (or file path), connects its + * output APIs via the standard run sequence, executes the module's main() + * which in turn calls Dmod_RunTests(), then unloads the module. + * + * @param ModuleName Name of the module or path to the .dmf file + * @param argc Argument count forwarded to the test runner + * @param argv Argument vector forwarded to the test runner + * + * @return Return value of the module's main() (number of failed steps, 0 = all + * passed), or a negative errno value if the module could not be loaded. + */ +int Dmod_RunModuleTests( const char* ModuleName, int argc, char* argv[] ) +{ + if( ModuleName == NULL ) + { + DMOD_LOG_ERROR("Cannot run module tests - missing module name\n"); + return -EINVAL; + } + + Dmod_Context_t* context = NULL; + if( Dmod_FileAvailable( ModuleName ) ) + { + context = Dmod_LoadFile( ModuleName ); + } + else + { + context = Dmod_LoadModuleByName( ModuleName ); + } + if( context == NULL ) + { + DMOD_LOG_ERROR("Cannot run module tests - cannot load module: %s\n", ModuleName); + return -ENOENT; + } + + int result = Dmod_Run( context, argc, argv ); + Dmod_Unload( context, false ); + return result; +} + /** * @brief Spawn application in a new child process * diff --git a/src/system/public/dmod_dmf_cb.c b/src/system/public/dmod_dmf_cb.c index 426cf98a..9e70290e 100644 --- a/src/system/public/dmod_dmf_cb.c +++ b/src/system/public/dmod_dmf_cb.c @@ -175,4 +175,136 @@ int Dmod_Irq( Dmod_Context_t* Context, int IrqNumber ) } } return 0; +} + +/** + * @brief Run all test steps registered in a module + * + * Iterates the module's .dmod.inputs section, discovers all entries whose + * signature begins with DMOD_TEST_SIGNATURE_PREFIX, and executes them one + * by one. Three special reserved step names drive the fixture mechanism: + * + * __setup__ — called before every step (optional) + * __teardown__ — called after every step (optional) + * __step_failed__ — pointer to the module-side volatile int flag that + * assertion macros set on failure + * + * These reserved entries are registered automatically by the + * dmod_test_main.c translation unit compiled in by dmod_add_test(). + * + * @param Context Context of the loaded test module + * @param argc Argument count forwarded from main() + * @param argv Argument vector; argv[1..] are treated as a step-name + * allow-list — only the named steps run. Pass argc == 1 + * (or argc == 0) to run all steps. + * + * @return Number of failed test steps (0 = all passed) + */ +int Dmod_RunTests( Dmod_Context_t* Context, int argc, char* argv[] ) +{ + if( !Dmod_Context_IsValid( Context ) ) + { + DMOD_LOG_ERROR("Cannot run tests - invalid context\n"); + return -1; + } + + if( Context->Inputs.InputSection == NULL ) + { + DMOD_LOG_ERROR("Cannot run tests - no input section\n"); + return -1; + } + + size_t numberOfEntries = Dmod_Api_GetNumberOfEntries( &Context->Inputs ); + + /* Locate the optional fixture pointers registered by dmod_test_main.c */ + void (*setup_fn)(void) = NULL; + void (*teardown_fn)(void) = NULL; + volatile int *pStepFailed = NULL; + + for( size_t i = 0; i < numberOfEntries; i++ ) + { + const char* sig = Context->Inputs.InputSection->Entries[i].Signature; + void* fn = Context->Inputs.InputSection->Entries[i].Function; + + if( sig == NULL || fn == NULL ) { continue; } + if( !Dmod_ApiSignature_IsTest( sig ) ) { continue; } + + const char* name = Dmod_ApiSignature_GetName( sig ); + if( name == NULL ) { continue; } + + if( strcmp( name, "__setup__" ) == 0 ) { setup_fn = (void (*)(void))fn; } + else if( strcmp( name, "__teardown__" ) == 0 ) { teardown_fn = (void (*)(void))fn; } + else if( strcmp( name, "__step_failed__" ) == 0 ) { pStepFailed = (volatile int*)fn; } + } + + int total_steps = 0; + int failed_steps = 0; + + Dmod_Printf( "=== DMOD Test Runner ===\n" ); + + for( size_t i = 0; i < numberOfEntries; i++ ) + { + const char* sig = Context->Inputs.InputSection->Entries[i].Signature; + void* fn = Context->Inputs.InputSection->Entries[i].Function; + + if( sig == NULL || fn == NULL ) { continue; } + if( !Dmod_ApiSignature_IsTest( sig ) ) { continue; } + + const char* name = Dmod_ApiSignature_GetName( sig ); + if( name == NULL ) { continue; } + + /* Skip reserved fixture entries */ + if( strcmp( name, "__setup__" ) == 0 ) { continue; } + if( strcmp( name, "__teardown__" ) == 0 ) { continue; } + if( strcmp( name, "__step_failed__" ) == 0 ) { continue; } + + /* Apply optional step-name filter from argv */ + if( argc > 1 ) + { + int found = 0; + for( int j = 1; j < argc; j++ ) + { + if( argv[j] != NULL && strcmp( name, argv[j] ) == 0 ) + { + found = 1; + break; + } + } + if( !found ) { continue; } + } + + Dmod_Printf( "[ RUN ] %s\n", name ); + + if( pStepFailed != NULL ) { *pStepFailed = 0; } + + void (*stepFn)(void) = (void (*)(void))fn; + if( setup_fn != NULL ) { setup_fn(); } + stepFn(); + if( teardown_fn != NULL ) { teardown_fn(); } + + total_steps++; + + int step_failed = ( pStepFailed != NULL ) ? *pStepFailed : 0; + if( step_failed ) + { + Dmod_Printf( "[FAILED] %s\n", name ); + failed_steps++; + } + else + { + Dmod_Printf( "[ OK ] %s\n", name ); + } + } + + if( total_steps == 0 ) + { + Dmod_Printf( "\nNo test steps found.\n" ); + } + else + { + Dmod_Printf( "\n=== Results: %d/%d passed ===\n", + total_steps - failed_steps, total_steps ); + } + + return failed_steps; } \ No newline at end of file