diff --git a/.gitignore b/.gitignore index 259148f..7a7235c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Build folders +build/ + # Prerequisites *.d diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..74f9b6e --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "ms-vscode.cmake-tools", + "ms-vscode.cpptools-extension-pack", + "vscode.cpptools", + "josetr.cmake-language-support-vscode" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e9fbe55 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,88 @@ +{ + "files.associations": { + "charconv": "cpp", + "chrono": "cpp", + "system_error": "cpp", + "xlocale": "cpp", + "limits": "cpp", + "tuple": "cpp", + "algorithm": "cpp", + "array": "cpp", + "atomic": "cpp", + "bit": "cpp", + "cctype": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "compare": "cpp", + "concepts": "cpp", + "csignal": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "exception": "cpp", + "format": "cpp", + "forward_list": "cpp", + "fstream": "cpp", + "functional": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "ios": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "iterator": "cpp", + "list": "cpp", + "locale": "cpp", + "map": "cpp", + "memory": "cpp", + "mutex": "cpp", + "new": "cpp", + "optional": "cpp", + "ostream": "cpp", + "ratio": "cpp", + "set": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "stop_token": "cpp", + "streambuf": "cpp", + "string": "cpp", + "thread": "cpp", + "type_traits": "cpp", + "typeinfo": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "utility": "cpp", + "vector": "cpp", + "xfacet": "cpp", + "xhash": "cpp", + "xiosbase": "cpp", + "xlocbuf": "cpp", + "xlocinfo": "cpp", + "xlocmes": "cpp", + "xlocmon": "cpp", + "xlocnum": "cpp", + "xloctime": "cpp", + "xmemory": "cpp", + "xstddef": "cpp", + "xstring": "cpp", + "xtr1common": "cpp", + "xtree": "cpp", + "xutility": "cpp" + }, + "editor.tabSize": 4, + "editor.detectIndentation": false, + "editor.insertSpaces": true, + "cSpell.words": [ + "funarg", + "INSTANTMEMORY", + "Pavlo", + "SUBCASE", + "Unstorable" + ], + "C_Cpp.errorSquiggles": "enabled", + "C_Cpp.loggingLevel": "Debug" +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..abde008 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,74 @@ +# This file is added just for convenience for those projects +# that already use CMake in their activities. +# +# This library is header only, so no need to build it +# and threre in NO REQUIREMENT to use CMake: +# one can just copy headers to their project +# and use any build system one likes. + + +cmake_minimum_required(VERSION 3.27) +project(InstantRTOS VERSION 0.1.0 LANGUAGES CXX) + +# Turn ALLOW_INSTANTRTOS_DEVELOPMENT ON once modifying InstantRTOS +# (remember this is cached variable, +# so it will stay the same until explicitly changed. +# For VSCode you can go F1 of Ctrl+Shift+P, then "CMake: Edit CMake Cache" +# or "CMake: Edit CMake Cache (UI)" and change the value there, +# or just delete CMakeCache.txt file in the build directory to start from scratch) +option( + ALLOW_INSTANTRTOS_DEVELOPMENT + "Used when developing/testing InstantRTOS" + ON +) + + +# The only purpose here is to populate include directories +add_library(${PROJECT_NAME} INTERFACE) +add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) +set_target_properties(${PROJECT_NAME} PROPERTIES + CXX_STANDARD 11 # This is the minimum requirement + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF +) +target_include_directories(${PROJECT_NAME} INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + + +if(ALLOW_INSTANTRTOS_DEVELOPMENT) + # While in VSCode, use recommended extentions from extensions.json, + # they add context menus and buttons into status bar. + # To see the list of targets and select the one to run + # of provide --config ConfigurationName in VSCode's + # click "Activity Bar" on the left, then go "CMake" icon. + # Find Build/Debug/Run buttons in the status bar below + # See also https://github.com/microsoft/vscode-cmake-tools/blob/main/docs/debug-launch.md + + if(MSVC) + # Remember _HAS_STATIC_RTTI=0 shall also do /GR- + + # uncomment below to have PDB in addition to execitable even in release + # set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "ProgramDatabase") + # add_link_options("/DEBUG:FULL") + endif() + + # Optionally it is possible to run tests + include(CTest) + if( BUILD_TESTING ) + # Go with doctest as the most natural choice for a C++ project )) + + # The only "external" dependency is when testing with doctest + include(FetchContent) + FetchContent_Declare( + doctest + GIT_REPOSITORY https://github.com/doctest/doctest.git + GIT_TAG master # or use a specific tag instead of master + ) + FetchContent_MakeAvailable(doctest) + + # Remember you can eiter run tests with CTest + # of just run (debug) the executable directly from the IDE + add_subdirectory(tests) + endif() +endif() diff --git a/examples/CoroutineGenerators/CoroutineGenerators.ino b/examples/CoroutineGenerators/CoroutineGenerators.ino new file mode 100644 index 0000000..7f870d4 --- /dev/null +++ b/examples/CoroutineGenerators/CoroutineGenerators.ino @@ -0,0 +1,62 @@ +#include "InstantCoroutine.h" + +// Create functor class named SequenceOfSquares for a coroutine +// producing squares starting from 0 +CoroutineDefine( SequenceOfSquares ) { + //coroutine variables (state persisting between CoroutineYield calls) + int i = 0; + + //specify body of the coroutine, int here is the type yielded + CoroutineBegin(int) + for ( ;; ++i){ + CoroutineYield( i*i ); + } + CoroutineEnd() +}; + +//Create functor class named Range for range producing coroutine +//(yes, it is possible for a coroutine to be template, it is just +// a class that can be instantiated multiple times!) +template +CoroutineDefine( Range ) { + T current, last; +public: + ///Constructor to establish initial coroutine state + Range(T beginFrom, T endWith) : current(beginFrom), last(endWith) {} + + //body of the coroutine that uses this state + CoroutineBegin(T) + for(; current < last; ++current){ + CoroutineYield( current ); + } + CoroutineStop(last); + CoroutineEnd() +}; + +//Instantiate squares generator +SequenceOfSquares sequenceOfSquares; + +void setup() { + Serial.begin(9600); +} + +void loop() { + //establish new range on each iteration + Range range(10, 20); + + while( range ){ + //"call" to coroutine resumes is till CoroutineYield or CoroutineStop + auto i = range(); + auto x = sequenceOfSquares(); + + Serial.print( i ); + Serial.print( ':' ); + Serial.println( x ); + delay( 200 ); + } + Serial.println(F("Iteration finished")); + + //NOTE: next iteration will start a new range again from scratch + // but sequenceOfSquares will continue where is was stopped previously + delay( 2000 ); +} diff --git a/examples/InstantCallback/InstantCallback.ino b/examples/InstantCallback/InstantCallback.ino new file mode 100644 index 0000000..9dca4a2 --- /dev/null +++ b/examples/InstantCallback/InstantCallback.ino @@ -0,0 +1,110 @@ +/* @file InstantCallback +* @brief Show how to turn C++ lambda into a "plain C" callback +*/ +#include "InstantCallback.h" + +void invoke_simple_callback( + unsigned (*simpleFunctionPointer)(unsigned arg) +){ + Serial.println(F("\nENTER invoke_simple_callback")); + unsigned res = simpleFunctionPointer(1000); + Serial.print(F("res=")); Serial.println(res); + Serial.println(F("LEAVE invoke_simple_callback\n")); +} + +void invoke_multiple( + unsigned (*simpleFunctionPointer)(unsigned arg) +){ + Serial.println(F("\nENTER invoke_multiple")); + unsigned res = simpleFunctionPointer(2000); + Serial.print(F("res=")); Serial.println(res); + res = simpleFunctionPointer(3000); + Serial.print(F("res=")); Serial.println(res); + res = simpleFunctionPointer(4000); + Serial.print(F("res=")); Serial.println(res); + Serial.println(F("LEAVE invoke_multiple\n")); +} + +/// Class for demo purposes (illustrate how lifetime is managed) +struct Wrap{ + unsigned val = 0; + + Wrap(){ + Serial.println(F("Wrap Default constructor")); + } + Wrap(unsigned initialVal): val(initialVal){ + Serial.print(F("Wrap Constructor for")); Println(); + } + ~Wrap(){ + Serial.print(F("Wrap Destructor for")); Println(); + } + + Wrap(const Wrap& other): val(other.val){ + Serial.print(F("Wrap Copy for")); Println(); + } + Wrap(Wrap&& other): val(other.val){ + other.val += 10000; //mark "other" as "moved from" + Serial.print(F("Wrap Move from ")); + Serial.print((unsigned)&other); + Serial.print(F(" to ")); + Println(); + } + Wrap& operator=(const Wrap& other){ + val = other.val; + Serial.print(F("Wrap Assignment for")); Println(); + return *this; + } + Wrap& operator=(Wrap&& other){ + val = other.val; + other.val += 20000; //mark "other" as "moved from" + Serial.print(F("Wrap Move assignment for")); Println(); + return *this; + } + void Println(){ + Serial.print(F(" val=")); Serial.print(val); + Serial.print(F(" at ")); Serial.println((unsigned)this); + } +}; + +void setup() { + Serial.begin(115200); + Serial.println(F("Start ===================================")); +} + +void loop() { + Serial.println(F("\nIteration ===============================")); + + unsigned var = rand() & 0xF; + Wrap wrap = rand() & 0xFF; + //demo for "single shot" callback + invoke_simple_callback(CallbackFrom<1>( + [=](unsigned arg){ + Serial.print(F("Lambda-1 called var=")); Serial.print(var); + Serial.print(F(", wrap=")); Serial.print(wrap.val); + Serial.print(F(", arg=")); Serial.println(arg); + return var + wrap.val + arg; + } + )); + + //refresh captured variables for new values + var = rand() & 0xF; + wrap.val = rand() & 0xFF; + //demo for multi shot callback + invoke_multiple(CallbackFrom<1>( + [=]( + CallbackExtendLifetime& lifetime, + unsigned arg + ){ + if( 4000 == arg ){ + //this will free memory after lambda exits + lifetime.Dispose(); + } + Serial.print(F("Lambda-2 called var=")); Serial.print(var); + Serial.print(F(", wrap=")); Serial.print(wrap.val); + Serial.print(F(", arg=")); Serial.println(arg); + return var + wrap.val + arg; + } + )); + + delay(1000); +} diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..faf6206 --- /dev/null +++ b/library.properties @@ -0,0 +1,9 @@ +name=InstantRTOS +version=0.1.0 +author=Pavlo M +maintainer=Pavlo M +sentence=A real-time operating system for Arduino (and other embedded platforms). +paragraph=InstantRTOS is a simple and efficient, lightweight real-time operating system (RTOS) for Arduino boards (and other embedded platforms). It allows you to create tasks, manage task scheduling etc... +category=Timing +url=https://github.com/olvap80/InstantRTOS +architectures=* \ No newline at end of file diff --git a/InstantArduino.h b/src/InstantArduino.h similarity index 100% rename from InstantArduino.h rename to src/InstantArduino.h diff --git a/InstantCallback.h b/src/InstantCallback.h similarity index 93% rename from InstantCallback.h rename to src/InstantCallback.h index 92d55c2..d5cbdc1 100644 --- a/InstantCallback.h +++ b/src/InstantCallback.h @@ -303,7 +303,7 @@ SOFTWARE. #ifndef InstantCallback_Panic # ifdef InstantRTOS_Panic -# define InstantCallback_Panic() InstantRTOS_Panic('T') +# define InstantCallback_Panic() InstantRTOS_Panic('B') # else # define InstantCallback_Panic() /* you can customize here! */ do{}while(true) # endif @@ -324,6 +324,34 @@ SOFTWARE. #endif +//______________________________________________________________________________ +// Handle C++ versions (just skip to "Classes for handling tasks" below)) + +#if defined(__cplusplus) +# if __cplusplus >= 201703L +# if __cplusplus >= 202002L +# define InstantCallbackNodiscard(explainWhy) [[nodiscard(explainWhy)]] +# else +# define InstantCallbackNodiscard(explainWhy) [[nodiscard]] +# endif +# else +# ifdef __GNUC__ +# define InstantCallbackNodiscard(explainWhy) __attribute__((warn_unused_result)) +# elif defined(_MSVC_LANG) && _MSVC_LANG >= 201703L +# if _MSVC_LANG >= 202002L +# define InstantCallbackNodiscard(explainWhy) [[nodiscard(explainWhy)]] +# else +# define InstantCallbackNodiscard(explainWhy) [[nodiscard]] +# endif +# endif +# endif +#endif + +#if !defined(InstantCallbackNodiscard) +# define InstantCallbackNodiscard(explainWhy) +#endif + + //______________________________________________________________________________ // Some "must have" internal stuff before declaring API (just skip that!!!)) @@ -355,6 +383,9 @@ In your usual case you pass only reservedCount to CallbackFrom See also https://en.cppreference.com/w/cpp/language/function_template section Template argument deduction */ template +InstantCallbackNodiscard( + "One shall use and call result of CallbackFrom or else memory is lost forever" +) auto CallbackFrom(LambdaType&& lambda) -> InternalTrampolineDetails::SimpleSignatureFromLambda*; @@ -756,6 +787,9 @@ namespace InternalTrampolineDetails{ template +InstantCallbackNodiscard( + "One shall use and call result of CallbackFrom or else memory is lost forever" +) auto CallbackFrom(LambdaType&& lambda) -> InternalTrampolineDetails::SimpleSignatureFromLambda* { diff --git a/InstantCoroutine.h b/src/InstantCoroutine.h similarity index 96% rename from InstantCoroutine.h rename to src/InstantCoroutine.h index c12c260..c411156 100644 --- a/InstantCoroutine.h +++ b/src/InstantCoroutine.h @@ -144,8 +144,8 @@ Additional sample inspired by https://www.chiark.greenend.org.uk/~sgtatham/corou } if (c == 0xFF) { // sign we met repetition CoroutineYield(); // wait for length - len = c; - CoroutineYield(); // wait for repeated char + len = c; //remember c is coroutine parameter here + CoroutineYield(); // wait for repeated char to c while (len--){ parser(c); } @@ -302,8 +302,8 @@ class CoroutineBase; * "Panic" is issued when trying to resume stopped coroutine. * Use LifetimeManager from InstantMemory.h * if you wish to have "restartable" coroutines */ -#define CoroutineStop(lastRVal) \ - do { cppCoroutine_State.current = CppCoroutine_State::Final; return lastRVal; } while (false) +#define CoroutineStop(...) \ + do { cppCoroutine_State.current = CppCoroutine_State::Final; return __VA_ARGS__; } while (false) /// End of the coroutine body (corresponds to CoroutineBegin() above) /** Remember, one shall not reach CoroutineEnd(), diff --git a/InstantDebounce.h b/src/InstantDebounce.h similarity index 86% rename from InstantDebounce.h rename to src/InstantDebounce.h index 89fef3f..8418c15 100644 --- a/InstantDebounce.h +++ b/src/InstantDebounce.h @@ -31,7 +31,7 @@ Simple demo for debouncing button with "Discover" approach control LED and beep) #define LED_OFF digitalWrite(LED_BUILTIN, LOW) //Pin with beeper - #define BEEP_PIN 4 + #define BEEP_PIN 7 namespace{ // For demo purposes generate frequency directly from the code @@ -50,8 +50,6 @@ Simple demo for debouncing button with "Discover" approach control LED and beep) pinMode(LED_BUILTIN, OUTPUT); pinMode(BEEP_PIN, OUTPUT); - //Note: timer_led is initially expired, so we will enter under if - beep_timer.StartPeriod(micros(), 500); } @@ -327,39 +325,29 @@ class SimpleDebounce{ * after the debounce cycle was performed * (returned value can be used to detect state change) */ bool Discover( - Ticks time, ///< Current time in the same units as in constructor + Ticks time, ///< Current time is measured in the same units + ///< as units in the constructor bool val ///< Current value corresponding to time ){ - bool sameValue = (val == currentDebouncedVal); - - if( simpleTimer.Discover(time) ){ - // We were in debounce state, and debouncing timer just expired! - if( !sameValue ){ - // new debounced value was detected - - //same as currentDebouncedVal = val; + if( simpleTimer.IsPending() ){ + //Debouncing is in progress right now + if( val == currentDebouncedVal ){ + /* Value is the same as before debounce again, + chattering continues, value is unstable */ + simpleTimer.Cancel(); + } + else if( simpleTimer.Discover(time) ){ + // We were in debounce state, and debouncing timer just expired! currentDebouncedVal = !currentDebouncedVal; - - return true; //DIFFERENT VALUE for sure! + return true; //We have detected DIFFERENT VALUE for sure! } - //else means button was released before debounce time! } - else{ - //here we are either counting debounce time or already debounced - - // Using != for booleans as logical XOR )) - if( sameValue != simpleTimer.IsExpired() ){ - /* here != means one of: - 1. same value while counting debounce interval - -> chattering continues - 2. different value when previous was debounced - -> need to debounce again - BOTH cases mean we start timing from scratch */ - simpleTimer.Start(time, debounceInterval); - } + else if( val != currentDebouncedVal ){ + //new value detected, start debouncing + simpleTimer.Start(time, debounceInterval); } - - return false; //value did not change (yet) + + return false; } ///Suppress any "incompatible" timings diff --git a/InstantDelegate.h b/src/InstantDelegate.h similarity index 96% rename from InstantDelegate.h rename to src/InstantDelegate.h index 139ecdc..14034fb 100644 --- a/InstantDelegate.h +++ b/src/InstantDelegate.h @@ -177,18 +177,18 @@ as a fast and compact C++ delegates for embedded platforms, like Arduino: test( Delegate::From(demoCallable5).Bind<&TestClass::test_method>() ); - //Ensure method pointer can be created with short syntax + //Ensure method pointer can be created TestClass demoCallable6{6}; test( Delegate::From(&demoCallable6).Bind<&TestClass::test_method>() ); - //Ensure method pointer can be created from reference + //Ensure method pointer can be created from function receiving reference TestClass demoCallable7{7}; test( Delegate::From(demoCallable7).Bind<&function_to_bind_receivingRef>() ); - //Ensure method pointer can be created with short syntax + //Ensure method pointer can be created from function receiving pointer TestClass demoCallable8{8}; test( Delegate::From(&demoCallable8).Bind<&function_to_bind_receivingPtr>() @@ -211,7 +211,7 @@ that can be treated as a type-erased callable reference but Delegate from InstantDelegate.h is also able to handle method pointers, and uses only two words to hold minimal closure! Generated code for trampolines contains called functions embedded -Or is produces proper tail calls https://godbolt.org/z/dW6eMdWb6 +or it produces proper tail calls https://godbolt.org/z/dW6eMdWb6 @code Delegate::MakeCallerForFunctor::apply(Delegate const*, int): # @"Delegate::MakeCallerForFunctor::apply(Delegate const*, int)" movl %esi, %eax @@ -221,10 +221,10 @@ Or is produces proper tail calls https://godbolt.org/z/dW6eMdWb6 retq Delegate::MakeCallerForConstMethod::apply(Delegate const*, int): # @Delegate::MakeCallerForConstMethod::apply(Delegate const*, int) movq 8(%rdi), %rdi - jmp TestClass::test_method(int) const # TAILCALL + jmp TestClass::test_method(int) const # TAIL CALL Delegate::MakeCallerForConstMethod::apply(Delegate const*, int): # @Delegate::MakeCallerForConstMethod::apply(Delegate const*, int) movq 8(%rdi), %rdi - jmp TestClass::test_method(int) const # TAILCALL + jmp TestClass::test_method(int) const # TAIL CALL @endcode See also https://godbolt.org/z/1ddGKs3PY for major compilers diff --git a/InstantId.h b/src/InstantId.h similarity index 100% rename from InstantId.h rename to src/InstantId.h diff --git a/InstantIntrusiveList.h b/src/InstantIntrusiveList.h similarity index 96% rename from InstantIntrusiveList.h rename to src/InstantIntrusiveList.h index ab3c722..216965e 100644 --- a/InstantIntrusiveList.h +++ b/src/InstantIntrusiveList.h @@ -420,7 +420,9 @@ class IntrusiveList{ inline ChainElement::~ChainElement(){ //assertion to ensure enclosing chain is not broken if( !IsChainElementSingle() ){ - //Destroying before removed from the chain + /* Destroying before removed from the chain + is likely an error (because those chain elements + usually have additional logic) */ InstantIntrusiveListPanic(); } } diff --git a/InstantMemory.h b/src/InstantMemory.h similarity index 79% rename from InstantMemory.h rename to src/InstantMemory.h index fc5110a..b208a23 100644 --- a/InstantMemory.h +++ b/src/InstantMemory.h @@ -5,8 +5,8 @@ (c) see https://github.com/olvap80/InstantRTOS Zero dependencies, works instantly by copy pasting to your project... -Inspired by memory management toolset available in various RTOSes, -and now available in C++ :) +Inspired by memory management tool set available in various RTOSes, +and now available in pure C++ :) The BlockPool is intended to be allocated statically and then used for memory allocation. @@ -182,8 +182,6 @@ SOFTWARE. //______________________________________________________________________________ // Portable configuration (just skip to "Classes for memory operations" below)) -#define InstantMemory_BlockPoolCountInMetadata - /* Keep promise to not use any standard libraries by default, but types from those header still must have */ #if defined(InstantRTOS_USE_STDLIB) || defined(__has_include) @@ -251,6 +249,10 @@ SOFTWARE. inline void* operator new(INSTANTMEMORY_SIZE_T, InstantMemoryPlaceholderHelper place) noexcept{ return place.ptr; } + /// Make compiler happy with explicit empty delete operator + inline void operator delete(void*, InstantMemoryPlaceholderHelper place) noexcept{ + //do nothing, as we do not allocate memory + } #endif @@ -276,28 +278,31 @@ SOFTWARE. # if defined(InstantRTOS_EnterCritical) && !defined(InstantMemory_SuppressEnterCritical) # define InstantMemory_EnterCritical InstantRTOS_EnterCritical # define InstantMemory_LeaveCritical InstantRTOS_LeaveCritical -# define InstantMemory_MutexObject InstantRTOS_MutexObject +# if defined(InstantRTOS_MutexObjectType) +# define InstantMemory_MutexObjectType InstantRTOS_MutexObjectType +# define InstantMemory_MutexObjectVariable InstantRTOS_MutexObjectVariable +# endif # else # define InstantMemory_EnterCritical # define InstantMemory_LeaveCritical -# define InstantMemory_MutexObject # endif #endif + //______________________________________________________________________________ -// Classes for memory operations - BlockPool and variations +// Classes for memory operations - CommonBlockPool and variations -///Optimized base for BlockPool (used for all kinds of BlockPools below) +///Optimized base used for all kinds of BlockPools below /** All various BlockPools reuse the same implementation * to save program space being spent on allocation/deallocation logic. - * Use SimpleBlockPool or SmartAllocator (TBD) below + * Use BlockPool, SharedAllocator, or SmartAllocator (TBD) below * (NOTE: single implementation to not duplicate code for each type) */ -class BlockPool{ +class CommonBlockPool{ public: //all the copying is banned - constexpr BlockPool(const BlockPool&) = delete; - BlockPool& operator =(const BlockPool&) = delete; + constexpr CommonBlockPool(const CommonBlockPool&) = delete; + CommonBlockPool& operator =(const CommonBlockPool&) = delete; //NOTE: for actual allocation API see corresponding derived classes @@ -310,80 +315,81 @@ class BlockPool{ /// How much bytes are available in custom part of block constexpr SizeType BlockSize() const; - /// Maximum number of blocks available for allocation constexpr SizeType TotalBlocks() const; - /// How many blocks are allocated so far constexpr SizeType BlocksAllocated() const; - ///"Raw" allocate of any free block, returned null if no more blocks available - /** Obtain raw uninitialized bytes */ + ///"Raw" allocate of any free block, returns null when no more blocks + /** Obtain raw uninitialized bytes (entire block), no constructor is called. + * One can use Make* API from derived classes for allocation */ void* AllocateRaw(); - /// Free raw bytes, do not issue any destructors! + /** Free entire block as "just bytes" without initialization, + * panic if block was not allocated by this CommonBlockPool */ static void FreeRaw(void* memoryPreviouslyAllocatedByBlockPool); + /// "Raw" deallocation with destructor, panic if not allocated here + /** Free block and call destructor of TBeingPlacedWhileAllocating, + * panic if block was not allocated by this CommonBlockPool. + * One can use Make* API from derived classes for allocation */ template static void Free(TBeingPlacedWhileAllocating* correspondingAllocatedObject); - - /// Use this for determining minimum alignment requirements - struct Metadata{ + + /// Metadata to be placed at the beginning of each block + /** Aso use this for determining minimum alignment requirements. + * Always goes just before custom part of the block, + * appending more metadata. + * It is up to derived class to ensure with EntireBlockSize(...) + * that entireBlockSizeUsed is large enough to hold metadata */ + class Metadata{ + private: + friend class CommonBlockPool; /// - struct IsPowerOfTwo { - static constexpr bool value = 0 != ((N >= 1) & !(N & (N - 1))); }; - static_assert( - IsPowerOfTwo::value, - "sizeof(Metadata) shall be power of two" - ); - - /// Calculate size of the block together with helper information (Metadata) - /** The EntireBlockSize takes into account place for Metadata - * and ensures Metadata alignment does not violate requestedAlignment. - * It is assumed that customBlockSizeRequested is already of - * size compatible with requestedAlignment, - * and that both requestedAlignment and sizeof(Metadata) are power of two */ - static constexpr SizeType EntireBlockSize( - SizeType customBlockSizeRequested, - SizeType requestedAlignment - ); protected: /// @Make gcc happy with C++ 11 - constexpr BlockPool() = delete; + constexpr CommonBlockPool() = delete; - ///Initialize BlockPool logic + ///Initialize CommonBlockPool logic /** One must be sure memoryArea contains at least * entireBlockSizeUsed*totalBlocksAvailable bytes! * Metadata is added to the end of */ - BlockPool( - ByteType* memoryArea, ///< BlockPool will place allocated blocks here, + CommonBlockPool( + ByteType* memoryArea, ///< CommonBlockPool will place allocated blocks here, ///< there shall be space enough to hold ///< entireBlockSizeUsed*totalBlocksAvailable bytes - SizeType customBlockSizeUsed, ///Custom part in the block + SizeType customBlockSizeUsed, ///Custom part in the block (without metadata) SizeType entireBlockSizeUsed, /// + struct IsPowerOfTwo{ + static constexpr bool value = 0 != ((N >= 1) & !(N & (N - 1))); }; + static_assert( IsPowerOfTwo::value, + "sizeof(Metadata) shall be power of two" ); + + /// Calculate size of the block together with helper information (Metadata) + /** The EntireBlockSize takes into account place for Metadata + * and ensures Metadata alignment does not violate requestedAlignment. + * It is assumed that customBlockSizeRequested is already of + * size compatible with requestedAlignment, + * and that both requestedAlignment and sizeof(Metadata) are power of two */ + static constexpr SizeType EntireBlockSize( + SizeType customBlockSizeRequested, SizeType requestedAlignment); + private: - /// Constant to mark BlockPool instances for debugging purposes + /// Constant to mark CommonBlockPool instances for debugging purposes static constexpr SizeType MarkToTest = 24991; - /// Mark BlockPool instances for debugging purposes + /// Mark CommonBlockPool instances for debugging purposes const SizeType mark = MarkToTest; /// Custom pa @@ -406,71 +412,53 @@ class BlockPool{ }; -/// Common allocation operations on BlockPool -template -class BlockPoolAllocator: public BlockPool{ +///Simple pool to allocate fixed size blocks +/** Allow allocation of raw pointer to fixed size memory blocks */ +template< + CommonBlockPool::SizeType SingleBlockSizeRequested, + CommonBlockPool::SizeType TotalNumBlocks, + class AlignAsType = CommonBlockPool::Metadata +> +class BlockPool: public CommonBlockPool{ + static_assert( + SingleBlockSizeRequested % alignof(AlignAsType) == 0, + "CommonBlockPool: ensure your blocks are of proper size allowing proper alignment" + ); public: - using BlockPool::BlockPool; + /// Create ready to use BlockPool + constexpr BlockPool(); ///Allocation with simultaneous construction (and static check for size)) + /** Use MakePtr to allocate and construct object of TBeingPlacedWhileAllocating + * in the block, panic if block is not available */ template - TBeingPlacedWhileAllocating* Allocate(Args&&... args); + TBeingPlacedWhileAllocating* MakePtr(Args&&... args); - /// RAII to enclose unique allocation from corresponding BlockPool - ///TODO: move this to BlockPool above - template - class UniqueAllocation{ - public: - private: - }; - /// RAII for unique allocation in BlockPool - template - UniqueAllocation AllocateUnique(Args&&... args); - - /// RAII to enclose unique allocation from corresponding BlockPool - ///TODO: move this to BlockPool above - ///TODO: two strategies of place for counter + /// RAII to enclose unique allocation from corresponding CommonBlockPool template - class SharedAllocation{ + class UniqueAllocation{ public: private: }; - /// RAII for shared allocation in BlockPool + /// RAII for unique allocation in CommonBlockPool template - SharedAllocation AllocateShared(Args&&... args); -}; - -///Simple pool to allocate fixed size blocks -/** Allow allocation of raw pointer to fixed size memory blocks */ -template< - BlockPool::SizeType SingleBlockSizeRequested, - BlockPool::SizeType TotalNumBlocks, - class AlignAsType = BlockPool::Metadata -> -class SimpleBlockPool: public BlockPoolAllocator{ - static_assert( - SingleBlockSizeRequested % alignof(AlignAsType) == 0, - "BlockPool: ensure your blocks are of proper size allowing proper alignment" - ); -public: - /// Create ready to use SimpleBlockPool - constexpr SimpleBlockPool(); + UniqueAllocation MakeUnique(Args&&... args); private: - static constexpr BlockPool::SizeType entireBlockSize = - BlockPool::EntireBlockSize(SingleBlockSizeRequested, alignof(AlignAsType)); + static constexpr CommonBlockPool::SizeType entireBlockSize = + CommonBlockPool::EntireBlockSize(SingleBlockSizeRequested, alignof(AlignAsType)); static_assert( - BlockPool::IsPowerOfTwo::value, + CommonBlockPool::IsPowerOfTwo::value, "alignof(AlignAsType) shall be power of two" ); /// Actual memory for blocks allocated as a single chunk /** Allocation info is not intermixed with allocated bytes, * thus there are no problems with alignments and strict aliasing */ - alignas(AlignAsType) BlockPool::ByteType memoryForBlocks[ + alignas(AlignAsType) CommonBlockPool::ByteType memoryForBlocks[ entireBlockSize*TotalNumBlocks ]; }; @@ -539,7 +527,7 @@ class LifetimeManager{ private: ///Actual location of that object /** See also https://en.cppreference.com/w/cpp/language/new#Placement_new */ - alignas(T) BlockPool::ByteType placeInMemory[ sizeof(T) ]; + alignas(T) CommonBlockPool::ByteType placeInMemory[ sizeof(T) ]; bool exists = false; }; @@ -570,6 +558,21 @@ class LifetimeManager{ /// TODO: SmartAllocator not yet ready! implement later #if 0 +class SharedAllocator{ +public: + /// RAII to enclose unique allocation from corresponding CommonBlockPool + ///TODO: two strategies of place for counter + template + class SharedAllocation{ + public: + private: + }; + + /// RAII for shared allocation in CommonBlockPool + template + SharedAllocation AllocateShared(Args&&... args); +}; + ///Specialization defines value of expected instance count for SmartAllocator /** Sample usage @code @@ -579,8 +582,8 @@ template class SmartAllocatorExpectedCount; ///Smart pool associated to type to allocate fixed size blocks -/** Uses SmartAllocatorExpectedCount to determine associated BlockPool capacity - * nested Allocate API returns smart pointer that keeps allocated instance alive */ +/** Uses SmartAllocatorExpectedCount to determine associated CommonBlockPool capacity + * nested MakeShared API returns smart pointer that keeps allocated instance alive */ template class SmartAllocator{ public: @@ -589,7 +592,7 @@ class SmartAllocator{ class Ptr{}; template - Ptr Allocate(Args... args); + Ptr MakeShared(Args... args); private: @@ -608,9 +611,9 @@ class SmartAllocator{ //______________________________________________________________________________ -// Implementing BlockPool +// Implementing CommonBlockPool -inline constexpr BlockPool::SizeType BlockPool::entireAlignedBlockSize( +inline constexpr CommonBlockPool::SizeType CommonBlockPool::entireAlignedBlockSize( SizeType customBlockSizeRequested, SizeType alignmentWithMetadata ){ @@ -623,20 +626,20 @@ inline constexpr BlockPool::SizeType BlockPool::entireAlignedBlockSize( ) * alignmentWithMetadata; } -inline constexpr BlockPool::SizeType BlockPool::BlockSize() const{ +inline constexpr CommonBlockPool::SizeType CommonBlockPool::BlockSize() const{ return customBlockSize; } -inline constexpr BlockPool::SizeType BlockPool::TotalBlocks() const{ +inline constexpr CommonBlockPool::SizeType CommonBlockPool::TotalBlocks() const{ return totalBlocks; } -inline constexpr BlockPool::SizeType BlockPool::BlocksAllocated() const{ +inline constexpr CommonBlockPool::SizeType CommonBlockPool::BlocksAllocated() const{ return blocksAllocated; } -inline void* BlockPool::AllocateRaw(){ +inline void* CommonBlockPool::AllocateRaw(){ ByteType* res = firstFree; if( res ){ auto metadata = reinterpret_cast(res - sizeof(Metadata)); @@ -651,12 +654,12 @@ inline void* BlockPool::AllocateRaw(){ return nullptr; } -inline void BlockPool::FreeRaw(void* memoryPreviouslyAllocatedByBlockPool){ +inline void CommonBlockPool::FreeRaw(void* memoryPreviouslyAllocatedByBlockPool){ if( nullptr != memoryPreviouslyAllocatedByBlockPool ){ ByteType* ptr = reinterpret_cast(memoryPreviouslyAllocatedByBlockPool); auto metadata = reinterpret_cast(ptr - sizeof(Metadata)); - BlockPool* owner = metadata->owner; + CommonBlockPool* owner = metadata->owner; if( owner->mark == MarkToTest ){ // valid block, almost for sure)) metadata->next = owner->firstFree; @@ -671,7 +674,7 @@ inline void BlockPool::FreeRaw(void* memoryPreviouslyAllocatedByBlockPool){ } template -void BlockPool::Free(TBeingPlacedWhileAllocating* correspondingAllocatedObject) +void CommonBlockPool::Free(TBeingPlacedWhileAllocating* correspondingAllocatedObject) { if( nullptr == correspondingAllocatedObject ){ return; @@ -682,7 +685,7 @@ void BlockPool::Free(TBeingPlacedWhileAllocating* correspondingAllocatedObject) -inline BlockPool::BlockPool( +inline CommonBlockPool::CommonBlockPool( ByteType* memoryArea, SizeType customBlockSizeUsed, SizeType entireBlockSizeUsed, @@ -711,7 +714,7 @@ inline BlockPool::BlockPool( )->next = nullptr; } -inline constexpr BlockPool::SizeType BlockPool::EntireBlockSize( +inline constexpr CommonBlockPool::SizeType CommonBlockPool::EntireBlockSize( SizeType customBlockSizeRequested, SizeType requestedAlignment ){ @@ -725,12 +728,31 @@ inline constexpr BlockPool::SizeType BlockPool::EntireBlockSize( //______________________________________________________________________________ -// Implementing BlockPoolAllocator +// Implementing SimpleBlockPool -template +template< + CommonBlockPool::SizeType SingleBlockSizeRequested, + CommonBlockPool::SizeType TotalNumBlocks, + class AlignAsType +> +constexpr BlockPool +::BlockPool() +: CommonBlockPool( + memoryForBlocks, + SingleBlockSizeRequested, + entireBlockSize, + TotalNumBlocks + ) +{} + +template< + CommonBlockPool::SizeType SingleBlockSizeRequested, + CommonBlockPool::SizeType TotalNumBlocks, + class AlignAsType +> template TBeingPlacedWhileAllocating* -BlockPoolAllocator::Allocate(Args&&... args){ +BlockPool::MakePtr(Args&&... args){ //see https://eli.thegreenplace.net/2014/perfect-forwarding-and-universal-references-in-c static_assert( sizeof(TBeingPlacedWhileAllocating) <= SingleBlockSizeRequested, @@ -742,26 +764,9 @@ BlockPoolAllocator::Allocate(Args&&... args){ return new( InstantMemoryPlaceholderHelper(rawMemory) ) TBeingPlacedWhileAllocating(static_cast(args)...); } - return nullptr; + InstantMemory_Panic(); } - -//______________________________________________________________________________ -// Implementing SimpleBlockPool - -template< - BlockPool::SizeType SingleBlockSizeRequested, - BlockPool::SizeType TotalNumBlocks, - class AlignAsType -> -constexpr SimpleBlockPool -::SimpleBlockPool() -: BlockPoolAllocator( - memoryForBlocks, SingleBlockSizeRequested, entireBlockSize, TotalNumBlocks - ) -{} - - //______________________________________________________________________________ // Implementing LifetimeManager diff --git a/InstantQueue.h b/src/InstantQueue.h similarity index 93% rename from InstantQueue.h rename to src/InstantQueue.h index 6753277..09954d1 100644 --- a/InstantQueue.h +++ b/src/InstantQueue.h @@ -66,11 +66,13 @@ SOFTWARE. # if defined(InstantRTOS_EnterCritical) && !defined(InstantQueue_SuppressEnterCritical) # define InstantQueue_EnterCritical InstantRTOS_EnterCritical # define InstantQueue_LeaveCritical InstantRTOS_LeaveCritical -# define InstantQueue_MutexObject InstantRTOS_MutexObject +# if defined(InstantRTOS_MutexObjectType) +# define InstantQueue_MutexObjectType InstantRTOS_MutexObjectType +# define InstantQueue_MutexObjectVariable InstantRTOS_MutexObjectVariable +# endif # else # define InstantQueue_EnterCritical # define InstantQueue_LeaveCritical -# define InstantQueue_MutexObject # endif #endif diff --git a/InstantRTOS.Config.CPU.h b/src/InstantRTOS.Config.CPU.h similarity index 79% rename from InstantRTOS.Config.CPU.h rename to src/InstantRTOS.Config.CPU.h index fc62186..c5a4419 100644 --- a/InstantRTOS.Config.CPU.h +++ b/src/InstantRTOS.Config.CPU.h @@ -17,27 +17,30 @@ Sample for AVR (Like Arduino UNO or Leonardo) @code #define InstantRTOS_EnterCritical { uint8_t oldSREG_value = SREG; cli(); #define InstantRTOS_LeaveCritical SREG = oldSREG_value} - #define InstantScheduler_MutexObject @endcode OPTION2: @code #define InstantRTOS_EnterCritical ATOMIC_BLOCK(ATOMIC_RESTORESTATE){ #define InstantRTOS_LeaveCritical } - #define InstantScheduler_MutexObject @endcode -Please note that InstantScheduler_MutexObject is "defined to nothing", -because we "disable all interrupts" for specific platform. -The InstantRTOS_MutexObject could be useful on those plaforms, where -there is no "disable interrupts" operation to use but mutex is present +The InstantRTOS_MutexObjectType and InstantRTOS_MutexObjectVariable could be +useful on those plaforms, where there is no "disable interrupts" operation +to use but mutex is present. The InstantRTOS takes care to place macro +InstantRTOS_MutexObjectType and InstantRTOS_MutexObjectVariable in the right +place in the code, so it will be visible to Enter/LeaveCritical macros. @code - //TODO: sample with mutex here + TODO: sample here + #define InstantRTOS_EnterCritical + #define InstantRTOS_LeaveCritical + #define InstantRTOS_MutexObjectType + #define InstantRTOS_MutexObjectVariable @endcode -NOTE: definitely you CANNOT obtain mutex from interrupt, +NOTE: definitely you CANNOT obtain mutex from interrupt, sample above is only to illustrate concept - for using InstantRTOS together with other with other RT(OS) - and is + for using InstantRTOS together with other with other (RT)OS + where only mutex is available. TODO: what about scoped macro? TODO: move this to corresponding file diff --git a/InstantRTOS.Config.h b/src/InstantRTOS.Config.h similarity index 83% rename from InstantRTOS.Config.h rename to src/InstantRTOS.Config.h index be50aca..170e48f 100644 --- a/InstantRTOS.Config.h +++ b/src/InstantRTOS.Config.h @@ -1,14 +1,14 @@ /** @file InstantRTOS.Config.h - @brief General configuration to include before files being configured + @brief General configuration is included before files being configured There are following "configurations" for InstantRTOS -(InstantRTOS works perfectly without them if you do not use corresponding - feature, so you do not need to always define them) +(NOTE: InstantRTOS works perfectly without them if you do not use corresponding + feature, so you do not need to always define them) -REMEMBER: using only cooperative threads without involving interrupts and/or - without coexisting with other RTOS - means you do not have to configure any Enter/LeaveCritical stuff, - just leave corresponding macros undefined :) +REMEMBER: once using only cooperative threads (without involving interrupts + and/or without coexisting with other RTOS) + means you do not have to configure any Enter/LeaveCritical stuff, + just leave corresponding macros undefined :) # general configurations: @@ -35,7 +35,8 @@ those that are not intended to happen but do happen. By default each module passes own letter to InstantRTOS_Panic, like InstantRTOS_Panic('Q') for queues. (You can easy find all those modules with searching for string - InstantRTOS_Panic(' via Ctrl+Shift+F/Find in Files feature :)) + InstantRTOS_Panic(' + via Ctrl+Shift+F/Find in Files feature :)) # file specific configurations @@ -47,14 +48,14 @@ but leave InstantScheduler_EnterCritical, InstantScheduler_LeaveCritical empty. This will alter only the module where this behavior is needed. -FileNameWithoutExtension+_EnterCritical -FileNameWithoutExtension+_LeaveCritical +FileNameWithoutExtension + _EnterCritical +FileNameWithoutExtension + _LeaveCritical ... etc... Also one can alter other InstantRTOS_Something stuff on per file basis -FileNameWithoutExtension+_Panic +FileNameWithoutExtension + _Panic etc... TODO: https://stackoverflow.com/questions/36381932/c-decrementing-an-element-of-a-single-byte-volatile-array-is-not-atomic-why @@ -106,7 +107,7 @@ SOFTWARE. // Uncomment below to allow InstantRTOS_Enter/LeaveCritical -//#define InstantRTOS_TryAllowCritical +#define InstantRTOS_TryAllowCritical /* Make the best effort to take CPU specifics into account InstantRTOS_TryAllowCritical etc are important here */ diff --git a/InstantRTOS.h b/src/InstantRTOS.h similarity index 100% rename from InstantRTOS.h rename to src/InstantRTOS.h diff --git a/InstantScheduler.h b/src/InstantScheduler.h similarity index 96% rename from InstantScheduler.h rename to src/InstantScheduler.h index 6db9693..773b4f2 100644 --- a/InstantScheduler.h +++ b/src/InstantScheduler.h @@ -89,11 +89,13 @@ SOFTWARE. # if defined(InstantRTOS_EnterCritical) && !defined(InstantScheduler_SuppressEnterCritical) # define InstantScheduler_EnterCritical InstantRTOS_EnterCritical # define InstantScheduler_LeaveCritical InstantRTOS_LeaveCritical -# define InstantScheduler_MutexObject InstantRTOS_MutexObject +# if defined(InstantRTOS_MutexObjectType) +# define InstantScheduler_MutexObjectType InstantRTOS_MutexObjectType +# define InstantScheduler_MutexObjectVariable InstantRTOS_MutexObjectVariable +# endif # else # define InstantScheduler_EnterCritical # define InstantScheduler_LeaveCritical -# define InstantScheduler_MutexObject # endif #endif diff --git a/InstantSignals.h b/src/InstantSignals.h similarity index 100% rename from InstantSignals.h rename to src/InstantSignals.h diff --git a/InstantTask.h b/src/InstantTask.h similarity index 96% rename from InstantTask.h rename to src/InstantTask.h index 5ac001f..4d951af 100644 --- a/InstantTask.h +++ b/src/InstantTask.h @@ -223,7 +223,10 @@ SOFTWARE. # if defined(InstantRTOS_EnterCritical) && !defined(InstantTask_SuppressEnterCritical) # define InstantTask_EnterCritical InstantRTOS_EnterCritical # define InstantTask_LeaveCritical InstantRTOS_LeaveCritical -# define InstantTask_MutexObject InstantRTOS_MutexObject +# if defined(InstantRTOS_MutexObjectType) +# define InstantTask_MutexObjectType InstantRTOS_MutexObjectType +# define InstantTask_MutexObjectVariable InstantRTOS_MutexObjectVariable +# endif # else # define InstantTask_EnterCritical # define InstantTask_LeaveCritical @@ -245,6 +248,12 @@ SOFTWARE. # else # ifdef __GNUC__ # define TaskNodiscard(explainWhy) __attribute__((warn_unused_result)) +# elif defined(_MSVC_LANG) && _MSVC_LANG >= 201703L +# if _MSVC_LANG >= 202002L +# define TaskNodiscard(explainWhy) [[nodiscard(explainWhy)]] +# else +# define TaskNodiscard(explainWhy) [[nodiscard]] +# endif # endif # endif #endif diff --git a/InstantThenable.h b/src/InstantThenable.h similarity index 94% rename from InstantThenable.h rename to src/InstantThenable.h index 1666c2f..8ed2c6c 100644 --- a/InstantThenable.h +++ b/src/InstantThenable.h @@ -81,10 +81,13 @@ SOFTWARE. //we have access from interrupts and/or multithreading # define InstantThenable_EnterCritical InstantRTOS_EnterCritical # define InstantThenable_LeaveCritical InstantRTOS_LeaveCritical -# define InstantThenable_MutexObject InstantRTOS_MutexObject +# if defined(InstantRTOS_MutexObjectType) +# define InstantThenable_MutexObjectType InstantRTOS_MutexObjectType +# define InstantThenable_MutexObjectVariable InstantRTOS_MutexObjectVariable +# endif # else # if 1 - //no interrupts/multithreading, so optimize it out + //no interrupts/multithreading, so optimize it out where possible # define InstantThenable_NoMultithreadingProtection //do not change in this branch, activate next branch to tune manually # define InstantThenable_EnterCritical @@ -94,7 +97,6 @@ SOFTWARE. //(usually not needed to get here, is mutually ) # define InstantThenable_EnterCritical # define InstantThenable_LeaveCritical -# define InstantThenable_MutexObject # endif # endif #endif @@ -216,8 +218,8 @@ class Thenable: private Delegate< void(const T& result) >{ /// Store value for the time when there is no subscription LifetimeManager storedResult; -#ifndef InstantThenable_NoMultithreadingProtection - InstantThenable_MutexObject; +#if !defined(InstantThenable_NoMultithreadingProtection) && defined(InstantThenable_MutexObjectType) + InstantThenable_MutexObjectType InstantThenable_MutexObjectVariable; #endif }; @@ -325,8 +327,8 @@ class Thenable: private Delegate{ /// Special helper for ExplicitlyIgnore static void doNothing(){} -#ifndef InstantThenable_NoMultithreadingProtection - InstantThenable_MutexObject; +#if !defined(InstantThenable_NoMultithreadingProtection) && defined(InstantThenable_MutexObjectType) + InstantThenable_MutexObjectType InstantThenable_MutexObjectVariable; #endif }; @@ -515,7 +517,7 @@ void ThenableToResolve::operator()(const T& result){ ){ copy = *static_cast(this); - Callback::state.correspondingCaller = markerForThenableWithoutSubscription; + Callback::state.correspondingCaller = Thenable::markerForThenableWithoutSubscription; Callback::state.untrackedEventsCount = 0; } else{ diff --git a/InstantTimer.h b/src/InstantTimer.h similarity index 84% rename from InstantTimer.h rename to src/InstantTimer.h index 26a8e52..5f0d4b7 100644 --- a/InstantTimer.h +++ b/src/InstantTimer.h @@ -1,7 +1,7 @@ /** @file InstantTimer.h @brief Simple timing classes to track timings in platform independent way -Simple platform independent time counting, without any dependencies, +Simple platform independent handy time counting, without any dependencies, straight forward implementation to use directly from program loop, follows absolute minimalism to be cheap by memory and functionality. (Note: consider InstantScheduler.h when you need some more complex scheduling)) @@ -32,8 +32,6 @@ Ideal for single treaded environment, see sample below for details. pinMode(LED_BUILTIN, OUTPUT); pinMode(SIGNAL_PIN, OUTPUT); - - //Note: timer_led is initially expired, so we will enter under if auto us = micros(); timer_led.Start(us, 1000000); @@ -121,8 +119,8 @@ SOFTWARE. class SimpleTimer{ public: /// Initial timer - /** Initially timer is marked as already expired (does not count!), - * One has to call Start to make it counting (non expired) */ + /** Initially timer does not count (IsPending() returns false), + * One has to call Start to make it counting */ SimpleTimer() = default; @@ -141,61 +139,61 @@ class SimpleTimer{ static constexpr Ticks DeltaMax = ~Ticks(0) / 2; - /// Mark timer as "non expired" and remember expiration moment - /** Remembers "expected" time for the timer to expire, - * from now all calls to IsExpired() return false, + /// Mark timer as "pending" and remember next trigger moment + /** Remembers "expected" time for the timer to trigger, + * from now all calls to IsPending() return true, * until delta ticks expires. */ void Start(Ticks currentTicks, Ticks delta){ expectedAbsoluteTicks = currentTicks + delta; //start counting - expired = false; + isPending = true; } - /// Update and check for the "edge" as signal transitions to expired + /// Update and check for the "edge" when timer triggers /** Returns true only when delta ticks expires since call to Start API * this means new "expiration state" detected ("discovered"), * then all other subsequent calls will return false again. - * Use IsExpired() API to check if timer "fired" in past. + * Use IsPending() API to check if timer still counting. * Call Start() to "recharge" the timer again. * * It is assumed that Discover() is called more often then DeltaMax, * (actually more often then expected time precision!) * thus we will not miss the moment once timer expires! */ bool Discover(Ticks currentTicks){ - if( !expired ){ + if( isPending ){ //compare unsigned values assuming two's complement if( (currentTicks - expectedAbsoluteTicks) > DeltaMax ){ //current time is still before the desired time return false; } // We have just detected time passed - expired = true; - //expiration detection is the only moment when true is returned + isPending = false; + //trigger detection is the only moment when true is returned return true; } return false; } - /// Test timer is expired without altering state (time is not checked!) + /// Test timer is pending without altering state (time is not checked!) /** Reads existing state (as calculated by the most recent Discover() call) - * @returns false only between Start and expiration moment, - * true is returned before Start() of after Expire. + * @returns true only between Start and expiration moment, + * false is returned before Start() of after expire. * * Remember one must periodically call Discover() API to detect expiration! * Discover() returns true only as "edge" of expiration moment is detected, - * After Discover() has detected expiration, IsExpired() always returns true + * After Discover() has detected expiration, IsPending() always returns false * till Start() is called to "recharge" */ - bool IsExpired() const { - return expired; + bool IsPending() const { + return isPending; } - /// Force timer to be marked as expired - /** Once marked as expired, Discover() API will have no effect + /// Cancel timer without expiration (IsPending() becomes false) + /** Once Cancel-ed, Discover() API will have no effect * and return false (as a sign that "nothing more was discovered"), - * but all subsequent calls to IsExpired() will return true from now! + * but all subsequent calls to IsPending() will return false from now! * Call Start to "recharge" timer again. */ - void ForceExpire(){ - expired = true; + void Cancel(){ + isPending = false; } @@ -210,9 +208,9 @@ class SimpleTimer{ bool Discover(OtherTicks currentTicks) = delete; private: - /// True if timer is expired (not waiting for the next) - bool expired = true; - ///Time when Timer will expire (Discover() API shall detect expiration) + /// True if timer is between Start and expire + bool isPending = false; + ///Time when Timer will trigger/expire (Discover() API shall detect expiration) /** In this way we remember only the single value to compare with! */ Ticks expectedAbsoluteTicks = 0; }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..269fe3a --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,26 @@ +# Tests to ensure the correctness of the library + +add_executable(InstantRTOS_tests + test_main.cpp + test_InstantTimer.cpp + test_InstantCallback.cpp + test_InstantCoroutine.cpp + test_InstantDebounce.cpp + test_InstantDelegate.cpp + test_InstantIntrusiveList.cpp + test_InstantMemory.cpp +) +set_target_properties(InstantRTOS_tests PROPERTIES + CXX_STANDARD 11 # This is the minimum requirement + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF +) +target_link_libraries(InstantRTOS_tests + PRIVATE + InstantRTOS # the library to be tested + doctest::doctest # use doctest as the testing framework +) + +# Ensure CTest will discover and run the tests +include(${doctest_SOURCE_DIR}/scripts/cmake/doctest.cmake) +doctest_discover_tests(InstantRTOS_tests) diff --git a/tests/test_InstantCallback.cpp b/tests/test_InstantCallback.cpp new file mode 100644 index 0000000..a0bb48e --- /dev/null +++ b/tests/test_InstantCallback.cpp @@ -0,0 +1,179 @@ +/** @file tests/test_InstantCallback.cpp + @brief Unit tests for InstantCallback.h +*/ + +#include +/// Custom exception for testing InstantCallback_Panic +class TestInstantCallbackPanicException: public std::exception{ + const char* what() const noexcept override{ + return "TestInstantCallbackPanicException"; + } +}; +//Header will see this definition +#define InstantCallback_Panic() throw TestInstantCallbackPanicException() +#include "InstantCallback.h" + + +#include "doctest/doctest.h" +#include +#include + +unsigned invoke_simple_callback( + unsigned (*simpleFunctionPointer)(unsigned arg) +){ + return simpleFunctionPointer(1000); +} + +std::tuple invoke_multiple( + unsigned (*simpleFunctionPointer)(unsigned arg) +){ + return { + simpleFunctionPointer(2000), + simpleFunctionPointer(3000), + simpleFunctionPointer(4000) + }; +} + +TEST_CASE("InstantCallback normal usage"){ + unsigned capture1 = rand() & 0xFF; + unsigned capture2 = rand() & 0x3FF; + SUBCASE("single shot"){ + //demo for "single shot" callback + auto res = invoke_simple_callback(CallbackFrom<1>( + [=](unsigned arg){ + return capture1*2 + capture2 + arg; + } + )); + CHECK(res == capture1*2 + capture2 + 1000); + } + SUBCASE("multiple shot"){ + //demo for "multiple shot" callback + auto res = invoke_multiple(CallbackFrom<1>( + [=]( + CallbackExtendLifetime& lifetime, + unsigned arg + ){ + if( 4000 == arg ){ + //this will free memory after lambda exits + lifetime.Dispose(); + } + return capture1*3 + capture2 + arg; + } + )); + CHECK(std::get<0>(res) == capture1*3 + capture2 + 2000); + CHECK(std::get<1>(res) == capture1*3 + capture2 + 3000); + CHECK(std::get<2>(res) == capture1*3 + capture2 + 4000); + } +} + +TEST_CASE("CallbackFrom allocation and InstantCallback_Panic"){ + SUBCASE("Iterative CallbackFrom with the same lambda shall not cause panic"){ + //this is to test if the memory is correctly freed + //when the lambda is destroyed + for(int i=0; i<100; i++){ + auto res = invoke_simple_callback(CallbackFrom<1>( + [=](unsigned arg){ + return arg; + } + )); + CHECK(res == 1000); + } + } + SUBCASE("Sequential CallbackFrom with different lambda shall not cause panic"){ + auto cb1 = CallbackFrom<1>([=](unsigned arg){ return arg; }); + + //assert that such type is "C style" unsigned(*)(unsigned) + static_assert( + std::is_same::value, + "cb1 should be of type unsigned(*)(unsigned)" + ); + + //this works because each lambda has different type and memory + auto cb2 = CallbackFrom<1>([=](unsigned arg){ return arg; }); + auto cb3 = CallbackFrom<1>([=](unsigned arg){ return arg; }); + + //but types of "C style callback" are the same + static_assert( + std::is_same::value, + "cb1 and cb2 should have the same types" + ); + static_assert( + std::is_same::value, + "cb2 and cb3 should have the same types" + ); + + //assert that the memory is correctly freed + auto res = invoke_simple_callback(cb1); + CHECK(res == 1000); + res = invoke_simple_callback(cb2); + CHECK(res == 1000); + res = invoke_simple_callback(cb3); + CHECK(res == 1000); + + #if defined(__cplusplus) && __cplusplus >= 202002L || defined(_MSVC_LANG) && _MSVC_LANG >= 202002L + //NOTE: a lambda can only appear in an unevaluated context with '/std:c++20' or later + + //assert other kinds of callbacks map to C style callbacks correctly + static_assert( + std::is_same< + decltype(CallbackFrom<1>([=](unsigned arg){})), + void(*)(unsigned) + >::value, + "cb1 should be of type void(*)(unsigned)" + ); + static_assert( + std::is_same< + decltype(CallbackFrom<1>([=](unsigned arg, double arg2){})), + void(*)(unsigned, double) + >::value, + "cb1 should be of type void(*)(unsigned, double)" + ); + static_assert( + std::is_same< + decltype(CallbackFrom<1>([=](unsigned arg, double arg2, char arg3){})), + void(*)(unsigned, double, char) + >::value, + "cb1 should be of type void(*)(unsigned, double, char)" + ); + #endif + } + SUBCASE("Allocating extra lambda call shall cause panic (one item allowed)"){ + constexpr int NumCallbacksAllowed = 1; + int iterations = 0; + CHECK_THROWS_AS( + [&]{ + constexpr int NumCallbacksAllowedNoCapture = 1; //make compiler happy + + for( ; iterations < NumCallbacksAllowedNoCapture+1; ++iterations){ + (void)CallbackFrom( + [=](unsigned arg){ + return arg; + } + ); + } + }(), + TestInstantCallbackPanicException + ); + CHECK(iterations == NumCallbacksAllowed); + } + SUBCASE("Allocating extra lambda call shall cause panic (ten items allowed)"){ + constexpr int NumCallbacksAllowed = 10; + int iterations = 0; + CHECK_THROWS_AS( + [&]{ + constexpr int NumCallbacksAllowedNoCapture = 10; //make compiler happy + + for( ; iterations < NumCallbacksAllowedNoCapture+1; ++iterations){ + (void)CallbackFrom( + [=](unsigned arg){ + (void)arg; + } + ); + } + }(), + TestInstantCallbackPanicException + ); + CHECK(iterations == NumCallbacksAllowed); + } +} + diff --git a/tests/test_InstantCoroutine.cpp b/tests/test_InstantCoroutine.cpp new file mode 100644 index 0000000..c081406 --- /dev/null +++ b/tests/test_InstantCoroutine.cpp @@ -0,0 +1,215 @@ +/** @file tests/test_InstantCoroutine.cpp + * @brief Unit tests for InstantCoroutine.h + */ + +#include +/// Custom exception for testing InstantCoroutine_Panic +class TestInstantCoroutinePanicException: public std::exception{ + const char* what() const noexcept override{ + return "TestInstantCoroutinePanicException"; + } +}; +//Header will see this definition +#define InstantCoroutine_Panic() throw TestInstantCoroutinePanicException() +#include "InstantCoroutine.h" + +#include "doctest/doctest.h" +#include +#include + + +namespace{ + + // Create functor class named SequenceOfSquares for a coroutine + // producing squares starting from 0 + CoroutineDefine( SequenceOfSquares ) { + //coroutine variables (state persisting between CoroutineYield calls) + int i = 0; + + //specify body of the coroutine, int here is the type yielded + CoroutineBegin(int) + for ( ;; ++i){ + CoroutineYield( i*i ); + } + CoroutineEnd() + }; + +} //namespace + +TEST_CASE("InstantCoroutine normal usage SequenceOfSquares"){ + //Instantiate squares generator + SequenceOfSquares sequenceOfSquares; + + //first 10 squares + for(int i = 0; i < 10; ++i){ + auto x = sequenceOfSquares(); + CHECK( x == i*i ); + + //Such coroutine "never ends" + CHECK( sequenceOfSquares ); + } + + //next 10 squares + for(int i = 10; i < 20; ++i){ + auto x = sequenceOfSquares(); + CHECK( x == i*i ); + + //Such coroutine "never ends" + CHECK( sequenceOfSquares ); + } +} + + +namespace{ + + //Create functor class named Range for range producing coroutine + //(yes, it is possible for a coroutine to be a template, it is just + // a class that can be instantiated multiple times!) + template + CoroutineDefine( Range ) { + T current, last; + public: + ///Constructor to establish initial coroutine state + Range(T beginFrom, T endWith) : current(beginFrom), last(endWith) {} + + //body of the coroutine that uses this state + CoroutineBegin(T) + for(; current < last; ++current){ + CoroutineYield( current ); + } + CoroutineStop(last); + CoroutineEnd() + }; + +} //namespace + +TEST_CASE("InstantCoroutine normal usage Range"){ + //Instantiate squares generator + Range range(10, 20); + + SUBCASE("first 10 starting from 10"){ + for(int i = 10; i < 20; ++i){ + auto x = range(); + CHECK( x == i ); + } + //the last value is returned by CoroutineStop + CHECK( range() == 20 ); + + //Such coroutine that reached CoroutineStop shall be marked as "ended" + CHECK( !range ); + //Once "ended" such coroutine will rise an exception if called + CHECK_THROWS_AS( + [&]{ range(); }(), + TestInstantCoroutinePanicException + ); + } + SUBCASE("Range as iterator"){ + int pos = 10; + while( range ){ + auto x = range(); + CHECK( x == pos++ ); + } + /*the last value returned by the coroutine was 20 + (that was what CoroutineStop returns) */ + CHECK( pos == 21 ); + + //Such coroutine that reached CoroutineStop shall be marked as "ended" + CHECK( !range ); + //Once "ended" such coroutine will rise an exception if called + CHECK_THROWS_AS( + [&]{ range(); }(), + TestInstantCoroutinePanicException + ); + } +} + + +TEST_CASE("InstantCoroutine two simultaneously"){ + //Range and SequenceOfSquares shall go in sync + Range range(0, 17); + SequenceOfSquares sequenceOfSquares; + while( range ){ + auto x = range(); + CHECK( x*x == sequenceOfSquares() ); + } +} + + +namespace{ +//Additional sample inspired by https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html + + CoroutineDefine( Parser ) { + public: + std::string accumulator; + + CoroutineBegin(void, int c) + for (;;) { + if( std::isalpha(c) ){ + do { + accumulator.push_back(char(c)); + CoroutineYield(); //get next char here + } while( std::isalpha(c) ); + accumulator += "[WORD DETECTED]"; + } + accumulator.push_back(char(c)); + accumulator += "[PUNCT DETECTED]"; + CoroutineYield(); //wait for more chars + } + CoroutineEnd() + } parser; + + CoroutineDefine( Decompressor ) { + unsigned len = 0; + CoroutineBegin(void, int c) + for (;;) { + if( c == EOF ){ + CoroutineStop(); + } + if( c == 0xFF ) { // sign we met repetition + CoroutineYield(); // wait for length + len = c; //remember c is coroutine parameter here + CoroutineYield(); // wait for repeated char to c + while (len--){ + parser(c); + } + } + else{ + parser(c); + } + CoroutineYield(); //wait for more chars + } + CoroutineEnd() + } decompressor; + +} //namespace + +TEST_CASE("InstantCoroutine normal usage Parser"){ + static const unsigned char testArray[] = "abc def \xFF\x07r d\xFF\x03Ref"; + + for(int position = 0; ; ++position ){ + CHECK( decompressor ); + + unsigned char current = testArray[position]; + if( !current ){ + decompressor(EOF); + break; + } + else{ + decompressor(current & 0xFF); + } + } + + CHECK( + parser.accumulator == + "abc[WORD DETECTED] [PUNCT DETECTED]def[WORD DETECTED] " + "[PUNCT DETECTED]rrrrrrr[WORD DETECTED] [PUNCT DETECTED]dRRRef" + ); + + //Decompressor did CoroutineStop, so it is marked as "ended" + CHECK( !decompressor ); + //Once "ended" such coroutine will rise an exception if called + CHECK_THROWS_AS( + [&]{ decompressor('a'); }(), + TestInstantCoroutinePanicException + ); +} diff --git a/tests/test_InstantDebounce.cpp b/tests/test_InstantDebounce.cpp new file mode 100644 index 0000000..edab36b --- /dev/null +++ b/tests/test_InstantDebounce.cpp @@ -0,0 +1,107 @@ +/** @file tests/test_InstantDebounce.cpp + @brief Unit tests for InstantDebounce.h +*/ + +#include "InstantDebounce.h" +#include "doctest/doctest.h" + +TEST_CASE("InstantDebounce: SimpleDebounce") { + // assume we use millis for debouncing + SimpleDebounce button(false, 50); + + //Initial state is false + CHECK(button.Value() == false); + + SUBCASE("Debouncing when no chatter") { + button.Discover(SimpleDebounce::Ticks(1000), true); + CHECK( !button.Value() ); //still false, counts + button.Discover(SimpleDebounce::Ticks(1003), true); + CHECK( !button.Value() ); //still false, counts + button.Discover(SimpleDebounce::Ticks(1049), true); + CHECK( !button.Value() ); //still false, counts + + button.Discover(SimpleDebounce::Ticks(1050), true); + CHECK( button.Value() ); //debounced, now true + + button.Discover(SimpleDebounce::Ticks(1051), true); + CHECK( button.Value() ); //still true + + button.Discover(SimpleDebounce::Ticks(1100), false); + CHECK( button.Value() ); //still true, counts + button.Discover(SimpleDebounce::Ticks(1125), false); + CHECK( button.Value() ); //still true, counts + button.Discover(SimpleDebounce::Ticks(1149), false); + CHECK( button.Value() ); //still true, counts + + button.Discover(SimpleDebounce::Ticks(1150), false); + CHECK( !button.Value() ); //debounced, now false + + button.Discover(SimpleDebounce::Ticks(1151), false); + CHECK( !button.Value() ); //still false + } + SUBCASE("Debouncing when single spike") { + //chatter that causes timer to start, + button.Discover(SimpleDebounce::Ticks(1152), true); + CHECK( !button.Value() ); //still false + button.Discover(SimpleDebounce::Ticks(1153), false); //timer counts from scratch + CHECK( !button.Value() ); //still false + + button.Discover(SimpleDebounce::Ticks(1175), false); //timer starts counting + CHECK( !button.Value() ); //still false + + //Assume there were no other measurements, timer should expire + button.Discover(SimpleDebounce::Ticks(1203), false); + CHECK( !button.Value() ); //still false + + /*Assume there were no other measurements, timer should expire + unless we continue pressing and releasing again */ + button.Discover(SimpleDebounce::Ticks(1303), true); + CHECK( !button.Value() ); //still false + } + + SUBCASE("Debouncing with chatter") { + button.Discover(SimpleDebounce::Ticks(1000), true); + CHECK( !button.Value() ); //still false, counts + button.Discover(SimpleDebounce::Ticks(1003), true); + CHECK( !button.Value() ); //still false, counts + button.Discover(SimpleDebounce::Ticks(1049), true); + CHECK( !button.Value() ); //still false, counts + + //chatter in the last moment + button.Discover(SimpleDebounce::Ticks(1050), false); + CHECK( !button.Value() ); //still false, counts from start + button.Discover(SimpleDebounce::Ticks(1051), true); + CHECK( !button.Value() ); //still false, continues counting + button.Discover(SimpleDebounce::Ticks(1090), true); + CHECK( !button.Value() ); //still false, continues counting + button.Discover(SimpleDebounce::Ticks(1100), true); + CHECK( !button.Value() ); //still false, continues counting + button.Discover(SimpleDebounce::Ticks(1101), true); + CHECK( button.Value() ); //debounced! + + + + button.Discover(SimpleDebounce::Ticks(1151), true); + CHECK( button.Value() ); //still true + + //go back to false + button.Discover(SimpleDebounce::Ticks(1200), false); + CHECK( button.Value() ); //still true, counts + button.Discover(SimpleDebounce::Ticks(1225), false); + CHECK( button.Value() ); //still true, counts + button.Discover(SimpleDebounce::Ticks(1249), false); + CHECK( button.Value() ); //still true, counts + + //chatter in the last moment + button.Discover(SimpleDebounce::Ticks(1250), true); + CHECK( button.Value() ); //still false, counts from start + button.Discover(SimpleDebounce::Ticks(1251), false); + CHECK( button.Value() ); //still false, continues counting + button.Discover(SimpleDebounce::Ticks(1290), false); + CHECK( button.Value() ); //still false, continues counting + button.Discover(SimpleDebounce::Ticks(1300), false); + CHECK( button.Value() ); //still false, continues counting + button.Discover(SimpleDebounce::Ticks(1301), false); + CHECK( !button.Value() ); //debounced! + } +} diff --git a/tests/test_InstantDelegate.cpp b/tests/test_InstantDelegate.cpp new file mode 100644 index 0000000..ac39c5b --- /dev/null +++ b/tests/test_InstantDelegate.cpp @@ -0,0 +1,213 @@ +/** @file tests/test_InstantDelegate.cpp + @brief Unit tests for InstantDelegate.h +*/ + +#include "InstantDelegate.h" +#include "doctest/doctest.h" +#include + + +namespace{ + using std::int32_t; + + //shorthand to not repeat the same signature every time + using MyCallback = Delegate; + + ///using callback by some custom API + int32_t CustomAPI(const MyCallback& callbackNeeded, int32_t some_int_argument){ + return callbackNeeded(2 * some_int_argument); + } + + ///"Plain" function with compatible signature + int32_t ordinary_function_with_compatible_signature(int32_t val){ + return val + 84; + } + + ///Some class with method to be called + class SomeClass{ + public: + /// Have some state in the object + SomeClass(int32_t val): some_private_field(val){} + + /// Method to be called via Delegate + int32_t some_method(int32_t val){ + return val + 142 + some_private_field; + } + private: + int32_t some_private_field; + }; + + /// callable thing having compatible operator() can also be referenced + class SomeFunctor{ + public: + /// Have some state in the functor + SomeFunctor(int32_t val): some_private_field(val){} + + /// Make SomeFunctor callable + int32_t operator()(int32_t val) { + return (val + 342) / some_private_field; + } + private: + int32_t some_private_field; + }; + +} //namespace + +TEST_CASE("InstantDelegate simple"){ + //various ways for creating the callback from lambdas, objects, methods + + //one can make callback directly from C++ lambda + CHECK( + 2042 == CustomAPI( + MyCallback([](int val){ return val + 42;}), + 1000 + ) + ); + + //and from "simple" function (implicit conversion is supported) + CHECK( + 4084 == CustomAPI( + ordinary_function_with_compatible_signature, 2000 + ) + ); + + //also one can create Delegate for calling method of some object + SomeClass targetObject{10000}; + CHECK( + 16142 == CustomAPI( + MyCallback::From(targetObject).Bind<&SomeClass::some_method>(), 3000 + ) + ); + + + SomeFunctor customCallable{2}; + CHECK( 4171 == CustomAPI(customCallable, 4000) ); + + //assume CustomAPI does not store Delegate, so it is safe to use temporary + CHECK( 4171 == CustomAPI(SomeFunctor{2}, 4000) ); +} + + +namespace{ + ///Sample API for receiving "callable" Delegate as parameter + int test(const Delegate& delegateToCall){ + return 20000 + delegateToCall(10); + } + + ///Sample "unbound" function to be called via Delegate + int function_to_pass_to_Delegate(int val){ + return val + 200; + } + + ///Sample class (to demonstrate functor and method calls) + class TestClass{ + public: + TestClass(int addToVal): addTo(addToVal) {} + + ///Demo functor to be tied with delegate + int operator()(int val) const { + return val + addTo; + } + + ///Demo method to be tied with delegate (object, method pair) + int test_method(int val) const { + return val + addTo + 1000; + } + int test_method2(int val) const { + return val + addTo + 100; + } + + ///Demo method to demonstrate usage from free function + int valueGet() const{ + return addTo; + } + + private: + int addTo; + }; + + //one can also create Delegate by binging objects to free functions + //(this may surprize you, but the word class is needed below, + // to make such functions statically bindable at compile time) + int function_to_bind_receivingRef(const class TestClass& t, int val){ + return t.valueGet() + 100 + val; + } + int function_to_bind_receivingPtr(class TestClass* t, int val){ + return t->valueGet() + 1000 + val; + } +} //namespace + + +TEST_CASE("InstantDelegate advanced"){ + //demo of "something callable" being passed to test as Delegate + TestClass demoCallable{-2}; + CHECK( 20008 == test(demoCallable) ); + + //demo variable to be captured by lambda + int captured = 1; + //C++ "long lasting" lambda to be referenced by the Delegate + auto lambdaAsVariable = [&](int val){ + return val + captured; + }; + //one can easy call any functor/callable via delegate + //assuming callable functor lives as long as delegate is called + CHECK( 20011 == test(lambdaAsVariable) ); + + //Function pointer can be called (implicit conversion, direct constructor is called) + CHECK( 20210 == test(function_to_pass_to_Delegate) ); + + + //inline "non capturing" lambda can be explicitly forced to be Delegate + CHECK( + 20052 == test( + Delegate([](int val){ + return val + 42; + }) + ) + ); + + + captured = 4; + //call with inlined lambda is also possible to force with Unstorable + //(we can do this because we do not save that temporary!) + CHECK( + 20014 == test(Delegate::Unstorable([&](int val){ + return val + captured; + })) + ); + + //method pointer for existing object can be created + const TestClass demoCallable5{5}; + //Note: direct call without delegate is demoCallable5.test_method(42); + CHECK( + 21015 == test( + Delegate::From(demoCallable5).Bind<&TestClass::test_method>() + ) + ); + + //Ensure other method pointer can be created + TestClass demoCallable6{6}; + CHECK( + 20116 == test( + Delegate::From(&demoCallable6).Bind<&TestClass::test_method2>() + ) + ); + + + //Ensure method pointer can be created from function receiving reference + TestClass demoCallable7{7}; + CHECK( + 20117 == test( + Delegate::From(demoCallable7) + .Bind<&function_to_bind_receivingRef>() + ) + ); + //Ensure method pointer can be created from function receiving pointer + TestClass demoCallable8{8}; + CHECK( + 21018 == test( + Delegate::From(&demoCallable8) + .Bind<&function_to_bind_receivingPtr>() + ) + ); +} diff --git a/tests/test_InstantIntrusiveList.cpp b/tests/test_InstantIntrusiveList.cpp new file mode 100644 index 0000000..40e7421 --- /dev/null +++ b/tests/test_InstantIntrusiveList.cpp @@ -0,0 +1,139 @@ +/** @file tests/test_InstantIntrusiveList.cpp + @brief Unit tests for InstantIntrusiveList.h +*/ + +#include +/// Custom exception for testing InstantIntrusiveListPanic +class TestInstantIntrusiveListException: public std::exception{ + const char* what() const noexcept override{ + return "TestInstantIntrusiveListException"; + } +}; +//Header will see this definition +#define InstantIntrusiveListPanic() throw TestInstantIntrusiveListException() +#include "InstantIntrusiveList.h" + +#include "doctest/doctest.h" +#include + +namespace{ + /// Class for testing purposes + class MyTestItem: public IntrusiveList::Node{ + public: + /// Constructor for testing purposes + MyTestItem(int value) : storedValue(value){} + + /// Method for testing purposes + int Value(){ + return storedValue; + } + private: + int storedValue; + }; + +} //namespace + +TEST_CASE("InstantIntrusiveList"){ + // put your main code here, to run repeatedly: + IntrusiveList il; + + MyTestItem ti1(11); + MyTestItem ti2(22); + MyTestItem ti3(33); + + il.InsertAtFront(&ti2); + il.InsertAtFront(&ti1); + il.InsertAtBack(&ti3); + { + static constexpr auto expected = {11, 22, 33}; + + int i = 0; + auto expected_it = expected.begin(); + for(auto& item : il){ + CHECK(item.Value() == *expected_it); + ++i; + ++expected_it; + } + CHECK( i == expected.size() ); + } + + + MyTestItem ti4(444); + ti2.InsertPrevChainElement(&ti4); + { + static constexpr auto expected = {11, 444, 22, 33}; + + int i = 0; + auto expected_it = expected.begin(); + for(auto& item : il){ + CHECK(item.Value() == *expected_it); + ++i; + ++expected_it; + } + CHECK( i == expected.size() ); + } + + + ti2.InsertNextChainElement(&ti4); + { + static constexpr auto expected = {11, 22, 444, 33}; + + int i = 0; + auto expected_it = expected.begin(); + for(auto& item : il){ + CHECK(item.Value() == *expected_it); + ++i; + ++expected_it; + } + CHECK( i == expected.size() ); + } + + + ti4.RemoveFromChain(); + { + static constexpr auto expected = {11, 22, 33}; + + int i = 0; + auto expected_it = expected.begin(); + for(auto& item : il){ + CHECK(item.Value() == *expected_it); + ++i; + ++expected_it; + } + CHECK( i == expected.size() ); + } + + + il.RemoveAtFront(); + { + static constexpr auto expected = {22, 33}; + + int i = 0; + auto expected_it = expected.begin(); + for(auto& item : il){ + CHECK(item.Value() == *expected_it); + ++i; + ++expected_it; + } + CHECK( i == expected.size() ); + } + + + il.RemoveAtEnd(); + { + static constexpr auto expected = {22}; + + int i = 0; + auto expected_it = expected.begin(); + for(auto& item : il){ + CHECK(item.Value() == *expected_it); + ++i; + ++expected_it; + } + CHECK( i == expected.size() ); + } + + + ti2.RemoveFromChain(); + CHECK( il.IsEmpty() ); +} diff --git a/tests/test_InstantMemory.cpp b/tests/test_InstantMemory.cpp new file mode 100644 index 0000000..1f895a1 --- /dev/null +++ b/tests/test_InstantMemory.cpp @@ -0,0 +1,328 @@ +/** @file tests/test_InstantMemory.cpp + @brief Unit tests for InstantMemory.h +*/ + +#include +/// Custom exception for testing InstantMemoryPanic +class TestInstantMemoryException: public std::exception{ + const char* what() const noexcept override{ + return "TestInstantIntrusiveListException"; + } +}; +//Header will see this definition +#define InstantMemory_Panic() throw TestInstantMemoryException() +#include "InstantMemory.h" +#include "doctest/doctest.h" + + +#if 0 + //actual block pool (place is allocated statically) + BlockPool< + sizeof(SomeClass), //size of single block + 10 //maximum number of blocks + > blockPool; + + ... + + void loop() { + ... + + auto ptr = blockPool.Allocate(); + ... + blockPool.Free(ptr); + ... + } + + + //============================================================================== + LifetimeManager manualManagement; + LifetimeManager useAsSingleton; + + ... + + void loop() { + ... + + manualManagement.Emplace("Some parameter", 42); //lifetime started + ... + manualManagement->SomeAction(); //continues + ... + manualManagement.Destroy(); //ended + + ... + useAsSingleton.Singleton().DoSomethingElse(); //create if not exists + } + +#endif + +namespace{ + int instancesOfSomeClass = 0; + class SomeClass{ + public: + SomeClass(int value): value(value){ + ++instancesOfSomeClass; + } + ~SomeClass(){ + --instancesOfSomeClass; + } + int Value() const { return value; } + private: + int value; + }; +} //namespace + +TEST_CASE("InstantMemory BlockPool"){ + constexpr CommonBlockPool::SizeType NumBlocks = 10; + BlockPool blocks; + + CHECK(blocks.BlockSize() == sizeof(SomeClass)); + CHECK(blocks.TotalBlocks() == NumBlocks); + CHECK(blocks.BlocksAllocated() == 0); + + SUBCASE("BlockPool simple"){ + auto p1 = blocks.MakePtr(42); + CHECK(instancesOfSomeClass == 1); + CHECK(blocks.BlocksAllocated() == 1); + CHECK(p1->Value() == 42); + + auto p2 = blocks.MakePtr(43); + CHECK(instancesOfSomeClass == 2); + CHECK(blocks.BlocksAllocated() == 2); + CHECK(p2->Value() == 43); + + blocks.Free(p1); + CHECK(instancesOfSomeClass == 1); + CHECK(blocks.BlocksAllocated() == 1); + + auto p3 = blocks.MakePtr(44); + CHECK(instancesOfSomeClass == 2); + CHECK(blocks.BlocksAllocated() == 2); + CHECK(p3->Value() == 44); + + blocks.Free(p2); + blocks.Free(p3); + CHECK(instancesOfSomeClass == 0); + CHECK(blocks.BlocksAllocated() == 0); + + //====================================================================== + auto p4 = blocks.MakePtr(45); + CHECK(instancesOfSomeClass == 1); + CHECK(blocks.BlocksAllocated() == 1); + CHECK(p4->Value() == 45); + + { + constexpr int MaxAllocationsLeft = NumBlocks - 1; + + SomeClass* ptrs[MaxAllocationsLeft]; + for(CommonBlockPool::SizeType i = 0; i < MaxAllocationsLeft; ++i){ + ptrs[i] = blocks.MakePtr(i); + CHECK(instancesOfSomeClass == i + 2); + CHECK(blocks.BlocksAllocated() == i + 2); + } + + CHECK(blocks.BlocksAllocated() == NumBlocks); + CHECK(blocks.AllocateRaw() == nullptr); + CHECK_THROWS_AS(blocks.MakePtr(NumBlocks), TestInstantMemoryException); + + for(CommonBlockPool::SizeType i = 0; i < MaxAllocationsLeft; ++i){ + blocks.Free(ptrs[i]); + CHECK(instancesOfSomeClass == MaxAllocationsLeft - i); + CHECK(blocks.BlocksAllocated() == MaxAllocationsLeft - i); + } + + CHECK(blocks.BlocksAllocated() == 1); + for(CommonBlockPool::SizeType i = 0; i < MaxAllocationsLeft; ++i){ + ptrs[i] = blocks.MakePtr(i); + CHECK(instancesOfSomeClass == i + 2); + CHECK(blocks.BlocksAllocated() == i + 2); + } + } + } +} + + +#if 0 +// Class to demonstrate using of the BlockPool +class AllocationTest{ +public: + AllocationTest(){ + Serial.print(F("CREATED:")); + printState(); + } + AllocationTest(char cInit) :c(cInit) { + Serial.print(F("CREATED:")); + printState(); + } + AllocationTest(char cInit, int iInit, double dInit) + :c(cInit), i(iInit), d(dInit) + { + Serial.print(F("CREATED:")); + printState(); + } + + ~AllocationTest(){ + Serial.print(F("DESTROYING:")); + printState(); + //mark as "unused" (for debugging)) + c = '_'; + i = 0; + d = -1.1; + Serial.print(F("DESTROYED:")); + printState(); + } + + void printState() const{ + if( c != '_' ){ + Serial.print(F("AllocationTest instance at ")); + Serial.print(reinterpret_cast(this)); + Serial.print('/'); + Serial.print(c); + Serial.print('/'); + Serial.print(i); + Serial.print('/'); + Serial.println(d); + } + else{ + Serial.println(F("ALREADY DESTRUCTED")); + } + } + +private: + char c = 'A'; + int i = 42; + double d = 42.42; +}; + +const unsigned n = sizeof(AllocationTest); + +SimpleBlockPool< + sizeof(AllocationTest), + 5 +> blockPool; + +LifetimeManager lifetimeManager; + +void setup() { + Serial.begin(9600); + + Serial.println(sizeof(AllocationTest)); + Serial.println(sizeof(float)); + Serial.println(sizeof(double)); + Serial.println(alignof(double)); + + Serial.println(F("----------------------------------")); + + Serial.println(blockPool.BlockSize()); + Serial.println(blockPool.TotalBlocks()); + Serial.println(blockPool.BlocksAllocated()); +} + +void loop() { + // put your main code here, to run repeatedly: + Serial.println(F("\n------------- Iteraton -------------")); + + AllocationTest* p1 = blockPool.Allocate(); + AllocationTest* p2 = blockPool.Allocate('B'); + //auto also turns to pointer + auto p3 = blockPool.Allocate('C', 30, 3.3); + auto p4 = blockPool.Allocate('D', 40, 4.4); + auto p5 = blockPool.Allocate('E', 45, 4.5); + + //this one will return nullptr because there is no more space + // since we have only 5 blocks in blockPool + auto p6 = blockPool.Allocate('F'); + + Serial.println(blockPool.BlocksAllocated()); + + Serial.println(reinterpret_cast(p1)); + Serial.println(reinterpret_cast(p2)); + Serial.println(reinterpret_cast(p3)); + Serial.println(reinterpret_cast(p4)); + Serial.println(reinterpret_cast(p5)); + Serial.println(reinterpret_cast(p6)); + + Serial.println(F("--Now free some items and allocate again--")); + + BlockPool::Free(p2); + BlockPool::Free(p4); + + Serial.println(blockPool.BlocksAllocated()); + + //those new items will take place of previ + auto p7 = blockPool.Allocate('G', 50, 5.5); + auto p8 = blockPool.Allocate('H'); + BlockPool::Free(p7); + auto p9 = blockPool.Allocate('I', 50, 5.5); + + //this one will return nullptr because there is no more space + auto p10 = blockPool.Allocate('J', 50, 5.5); + Serial.println(reinterpret_cast(p10)); + + Serial.println(blockPool.BlocksAllocated()); + Serial.println(F("-- Now free everyshing --")); + BlockPool::Free(p3); + BlockPool::Free(p1); + Serial.println(blockPool.BlocksAllocated()); + BlockPool::Free(p5); + BlockPool::Free(p9); + //remember this was already destructed BlockPoolBase::Free(p7); + BlockPool::Free(p8); + + Serial.println(F("-- Everyshing freed --")); + Serial.println(blockPool.BlocksAllocated()); + Serial.println(F("-- Allocate again --")); + + p1 = blockPool.Allocate('K'); + auto r1 = blockPool.AllocateRaw(); + p2 = blockPool.Allocate('L', 700, 7.7); + auto r2 = blockPool.AllocateRaw(); + auto r3 = blockPool.AllocateRaw(); + auto r4 = blockPool.AllocateRaw(); + + Serial.println(blockPool.BlocksAllocated()); + + Serial.println(reinterpret_cast(p1)); + Serial.println(reinterpret_cast(r1)); + Serial.println(reinterpret_cast(p2)); + Serial.println(reinterpret_cast(r2)); + Serial.println(reinterpret_cast(r3)); + Serial.println(reinterpret_cast(r4)); + + Serial.println(blockPool.BlocksAllocated()); + + BlockPool::Free(p1); + BlockPool::FreeRaw(r1); + BlockPool::Free(p2); + BlockPool::FreeRaw(r3); + BlockPool::FreeRaw(r2); + + Serial.println(blockPool.BlocksAllocated()); + + Serial.println(F("-- Testing lifetimeManager 1 --")); + + auto& ref = lifetimeManager.Emplace('R'); + + ref.printState(); + lifetimeManager->printState(); + (*lifetimeManager).printState(); + + lifetimeManager.Destroy(); + + //object is destroyed, but use ref for demo purposes + ref.printState(); + + Serial.println(F("-- Testing lifetimeManager 2 (LifetimeManagerScope) --")); + + LifetimeManagerScope(lifetimeManager, 'S', 1233, 12.34){ + + lifetimeManager->printState(); + (*lifetimeManager).printState(); + + lifetimeManager.Destroy(); + + //ref is still valid, use it for demo purposes + ref.printState(); + } + delay(14200); +} +#endif \ No newline at end of file diff --git a/tests/test_InstantTimer.cpp b/tests/test_InstantTimer.cpp new file mode 100644 index 0000000..4669e91 --- /dev/null +++ b/tests/test_InstantTimer.cpp @@ -0,0 +1,252 @@ +/** @file tests/test_InstantTimer.cpp + @brief Unit tests for InstantTimer.h +*/ + +#include "InstantTimer.h" +#include "doctest/doctest.h" +#include + +TEST_CASE("InstantTimer: SimpleTimer") { + //Remember, we provide a time, timer just tracks it + SimpleTimer simpleTimer; + + //Never expired before starting + CHECK( !simpleTimer.IsPending() ); + + + SUBCASE("Normal expiration") { + /*We provide a time, timer just tracks it, + test starting from "some moment" */ + simpleTimer.Start(SimpleTimer::Ticks(10000), 1000); + //Never expired after starting + CHECK( simpleTimer.IsPending() ); + + //All values before expiration time do not trigger + CHECK( !simpleTimer.Discover(SimpleTimer::Ticks(10010)) ); + CHECK( simpleTimer.IsPending() ); + CHECK( !simpleTimer.Discover(SimpleTimer::Ticks(10100)) ); + CHECK( simpleTimer.IsPending() ); + CHECK( !simpleTimer.Discover(SimpleTimer::Ticks(10999)) ); + CHECK( simpleTimer.IsPending() ); + + //Expiration time triggers, timer triggering shall be discovered + CHECK( simpleTimer.Discover(SimpleTimer::Ticks(11000)) ); + //After expiration, timer is not pending + CHECK( !simpleTimer.IsPending() ); + + //Timer expires only once + CHECK( !simpleTimer.Discover(SimpleTimer::Ticks(11000)) ); + CHECK( !simpleTimer.Discover(SimpleTimer::Ticks(11001)) ); + CHECK( !simpleTimer.IsPending() ); + + //Timer expires only once + CHECK( !simpleTimer.Discover(SimpleTimer::Ticks(12001)) ); + CHECK( !simpleTimer.IsPending() ); + + //But can be started again + + simpleTimer.Start(SimpleTimer::Ticks(20000), 1000); + //Never expired after starting + CHECK( simpleTimer.IsPending() ); + + //All values before expiration time do not trigger + CHECK( !simpleTimer.Discover(SimpleTimer::Ticks(20010)) ); + CHECK( simpleTimer.IsPending() ); + CHECK( !simpleTimer.Discover(SimpleTimer::Ticks(20100)) ); + CHECK( simpleTimer.IsPending() ); + CHECK( !simpleTimer.Discover(SimpleTimer::Ticks(20999)) ); + CHECK( simpleTimer.IsPending() ); + + //Time after expiration also triggers + CHECK( simpleTimer.Discover(SimpleTimer::Ticks(21500)) ); + //After expiration, timer is not pending + CHECK( !simpleTimer.IsPending() ); + + //Timer expires only once + CHECK( !simpleTimer.Discover(SimpleTimer::Ticks(21501)) ); + CHECK( !simpleTimer.IsPending() ); + + //Also can cross unsigned bounds once scheduled near boundary + constexpr SimpleTimer::Ticks newStartFrom + = std::numeric_limits::max() - 500; + + simpleTimer.Start(newStartFrom, 1000); + CHECK( simpleTimer.IsPending() ); + + CHECK( !simpleTimer.Discover(newStartFrom + 1) ); + CHECK( simpleTimer.IsPending() ); + + CHECK( !simpleTimer.Discover(newStartFrom + 999) ); + CHECK( simpleTimer.IsPending() ); + + //Wrapping around the boundary is not a problem + CHECK( simpleTimer.Discover(newStartFrom + 1000) ); + CHECK( !simpleTimer.IsPending() ); + + CHECK( !simpleTimer.Discover(newStartFrom + 1001) ); + CHECK( !simpleTimer.IsPending() ); + } + SUBCASE("Start again (restart) without expiration") { + //We provide a time, timer just tracks it + simpleTimer.Start(SimpleTimer::Ticks(0), 1000); + //Never expired after starting + CHECK( simpleTimer.IsPending() ); + + //We provide a time, timer just tracks it + simpleTimer.Start(SimpleTimer::Ticks(1000), 1000); + //Never expired after starting + CHECK( simpleTimer.IsPending() ); + + //All values before expiration time do not trigger + CHECK( !simpleTimer.Discover(SimpleTimer::Ticks(1010)) ); + CHECK( simpleTimer.IsPending() ); + CHECK( !simpleTimer.Discover(SimpleTimer::Ticks(1100)) ); + CHECK( simpleTimer.IsPending() ); + CHECK( !simpleTimer.Discover(SimpleTimer::Ticks(1999)) ); + CHECK( simpleTimer.IsPending() ); + + //Expiration time triggers, timer triggering shall be discovered + CHECK( simpleTimer.Discover(SimpleTimer::Ticks(2000)) ); + //After expiration, timer is not pending + CHECK( !simpleTimer.IsPending() ); + + //Timer expires only once + CHECK( !simpleTimer.Discover(SimpleTimer::Ticks(11001)) ); + CHECK( !simpleTimer.IsPending() ); + } + SUBCASE("Corner case - 0 time") { + simpleTimer.Start(SimpleTimer::Ticks(0), 0); + + //Never expired after starting + CHECK( simpleTimer.IsPending() ); + + //Discovering the time triggers the timer + CHECK( simpleTimer.Discover(SimpleTimer::Ticks(0)) ); + //After expiration, timer is not pending + CHECK( !simpleTimer.IsPending() ); + } + SUBCASE("Cancel") { + simpleTimer.Start(SimpleTimer::Ticks(0), 0); + //Never expired after starting + CHECK( simpleTimer.IsPending() ); + + //Force expiration + simpleTimer.Cancel(); + //After expiration, timer is not pending + CHECK( !simpleTimer.IsPending() ); + + //Timer expires only once + CHECK( !simpleTimer.Discover(SimpleTimer::Ticks(1)) ); + } + SUBCASE("Cancel after expiration") { + simpleTimer.Start(SimpleTimer::Ticks(0), 0); + //Never expired after starting + CHECK( simpleTimer.IsPending() ); + + //Discovering the time triggers the timer + CHECK( simpleTimer.Discover(SimpleTimer::Ticks(0)) ); + //After expiration, timer is not pending + CHECK( !simpleTimer.IsPending() ); + + //Force expiration + simpleTimer.Cancel(); + //After expiration, timer is not pending + CHECK( !simpleTimer.IsPending() ); + + //Timer expires only once + CHECK( !simpleTimer.Discover(SimpleTimer::Ticks(1)) ); + } + SUBCASE("Is never 'Discovered' without starting") { + //Discovering the time triggers the timer + CHECK( !simpleTimer.Discover(SimpleTimer::Ticks(0)) ); + //After expiration, timer is not pending + CHECK( !simpleTimer.IsPending() ); + } +} + +TEST_CASE("PeriodicTimer"){ + PeriodicTimer periodicTimer; + + SUBCASE("Normal periods") { + periodicTimer.StartPeriod(SimpleTimer::Ticks(10000), 1000); + + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(10010)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(10500)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(10999)) ); + + CHECK( periodicTimer.Discover(SimpleTimer::Ticks(11000)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(11001)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(11200)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(11999)) ); + + CHECK( periodicTimer.Discover(SimpleTimer::Ticks(12000)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(12000)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(12001)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(12999)) ); + + CHECK( periodicTimer.Discover(SimpleTimer::Ticks(13100)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(13999)) ); + + CHECK( periodicTimer.Discover(SimpleTimer::Ticks(14000)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(14001)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(14999)) ); + } + SUBCASE("Changing periods") { + periodicTimer.StartPeriod(SimpleTimer::Ticks(10000), 1000); + + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(10010)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(10500)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(10999)) ); + + CHECK( periodicTimer.Discover(SimpleTimer::Ticks(11000)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(11001)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(11200)) ); + + periodicTimer.StartPeriod(SimpleTimer::Ticks(11500), 2000); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(11999)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(12000)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(12500)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(13499)) ); + + CHECK( periodicTimer.Discover(SimpleTimer::Ticks(13500)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(13999)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(14999)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(15499)) ); + + CHECK( periodicTimer.Discover(SimpleTimer::Ticks(15510)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(17499)) ); + + CHECK( periodicTimer.Discover(SimpleTimer::Ticks(17500)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(17501)) ); + + } + SUBCASE("Deactivate") { + periodicTimer.StartPeriod(SimpleTimer::Ticks(10000), 1000); + + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(10010)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(10500)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(10999)) ); + + CHECK( periodicTimer.Discover(SimpleTimer::Ticks(11000)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(11001)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(11200)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(11999)) ); + + periodicTimer.Deactivate(); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(12000)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(13000)) ); + } + SUBCASE("Deactivate immediately after starting") { + periodicTimer.StartPeriod(SimpleTimer::Ticks(10000), 1000); + periodicTimer.Deactivate(); + + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(11000)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(11001)) ); + } + SUBCASE("Periods with 0 period are already \"deactivated\" (no period)") { + periodicTimer.StartPeriod(SimpleTimer::Ticks(10000), 0); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(11000)) ); + CHECK( !periodicTimer.Discover(SimpleTimer::Ticks(21001)) ); + } + +} diff --git a/tests/test_main.cpp b/tests/test_main.cpp new file mode 100644 index 0000000..3e43be3 --- /dev/null +++ b/tests/test_main.cpp @@ -0,0 +1,4 @@ +//Include the doctest main file to run the tests + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest/doctest.h"