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/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/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..3a256aba 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 ) ); @@ -124,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_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_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/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/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..394e74b4 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; @@ -396,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 new file mode 100644 index 00000000..c9efc982 --- /dev/null +++ b/src/module/dmod_test_main.c @@ -0,0 +1,69 @@ +/** + * @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 registers three special .dmod.inputs entries so the + * system-side Dmod_RunTests() can drive the test lifecycle without accessing + * module internals directly: + * + * __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 + * + * 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.h" + +//============================================================================== +// TEST STATE +//============================================================================== + +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) +//============================================================================== + +DMOD_WEAK_SYMBOL void dmod_test_setup(void) {} +DMOD_WEAK_SYMBOL void dmod_test_teardown(void) {} + +/* 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") = +{ + .Function = (void*)dmod_test_setup, + .Signature = DMOD_MAKE_TEST_SIGNATURE(__setup__) +}; + +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 +//============================================================================== + +int main( int argc, char* argv[] ) +{ + 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 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..9f52a248 --- /dev/null +++ b/templates/module/test/@DMOD_MODULE_NAME@_test.c @@ -0,0 +1,32 @@ +/** + * @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. + * + * 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" + +void dmod_test_setup(void) +{ + /* initialise fixtures before every step */ +} + +void dmod_test_teardown(void) +{ + /* clean up after every step */ +} + +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