From d968f169c994c3105493002253f20a3cb789da90 Mon Sep 17 00:00:00 2001 From: PolarGoose <35307286+PolarGoose@users.noreply.github.com> Date: Wed, 10 Jan 2018 20:51:29 +0300 Subject: [PATCH] Initial --- .clang-format | 6 + .editorconfig | 9 + .gitattributes | 1 + .../continuous-integration-workflow.yaml | 21 +++ .gitignore | 4 + CMakeLists.txt | 14 ++ LICENSE | 21 +++ README.md | 4 + build.ps1 | 48 +++++ src/test/bit_cast_test.cpp | 28 +++ src/test/circular_buffer_test.cpp | 53 ++++++ src/test/concurrency/dispatcher_test.cpp | 73 ++++++++ .../interruptible_condition_variable_test.cpp | 92 ++++++++++ src/test/concurrency/lock_guard_test.cpp | 24 +++ .../observable_atomic_variable_test.cpp | 115 ++++++++++++ src/test/concurrency/one_shot_timer_test.cpp | 31 ++++ .../concurrency/utils/time_passed_method.h | 12 ++ src/test/concurrency/waiting_queue_test.cpp | 169 ++++++++++++++++++ src/test/concurrency/watchdog_test.cpp | 53 ++++++ src/test/is_string_literal_test.cpp | 24 +++ src/test/logging/assert_with_message_test.cpp | 6 + src/test/logging/log_stream_test.cpp | 18 ++ src/test/logging/scoped_log_test.cpp | 33 ++++ .../non_copyable_and_non_movable_test.cpp | 10 ++ src/test/non_copyable_test.cpp | 9 + src/test/precompiled_header.h | 19 ++ src/test/string_conversion_test.cpp | 11 ++ src/test/struct_aliasing_disable_test.cpp | 15 ++ src/utils_h/bit_cast.h | 21 +++ src/utils_h/circular_buffer.h | 64 +++++++ src/utils_h/concurrency/dispatcher.h | 74 ++++++++ .../interruptible_condition_variable.h | 59 ++++++ src/utils_h/concurrency/lock_guard.h | 6 + .../concurrency/observable_atomic_variable.h | 52 ++++++ src/utils_h/concurrency/one_shot_timer.h | 43 +++++ src/utils_h/concurrency/waiting_queue.h | 64 +++++++ src/utils_h/concurrency/watchdog.h | 24 +++ src/utils_h/logging/assert_with_message.h | 9 + src/utils_h/logging/log_stream.h | 23 +++ src/utils_h/logging/scoped_log.h | 44 +++++ src/utils_h/non_copyable.h | 18 ++ src/utils_h/non_copyable_and_non_movable.h | 17 ++ src/utils_h/preprocessor.h | 20 +++ src/utils_h/string_conversion.h | 19 ++ src/utils_h/struct_aliasing_disable.h | 4 + src/utils_h/type_traits.h | 21 +++ tools/cmake/catch2_lib.cmake | 18 ++ tools/cmake/clang_format.cmake | 29 +++ tools/cmake/compiler_options.cmake | 19 ++ 49 files changed, 1571 insertions(+) create mode 100644 .clang-format create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/workflows/continuous-integration-workflow.yaml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build.ps1 create mode 100644 src/test/bit_cast_test.cpp create mode 100644 src/test/circular_buffer_test.cpp create mode 100644 src/test/concurrency/dispatcher_test.cpp create mode 100644 src/test/concurrency/interruptible_condition_variable_test.cpp create mode 100644 src/test/concurrency/lock_guard_test.cpp create mode 100644 src/test/concurrency/observable_atomic_variable_test.cpp create mode 100644 src/test/concurrency/one_shot_timer_test.cpp create mode 100644 src/test/concurrency/utils/time_passed_method.h create mode 100644 src/test/concurrency/waiting_queue_test.cpp create mode 100644 src/test/concurrency/watchdog_test.cpp create mode 100644 src/test/is_string_literal_test.cpp create mode 100644 src/test/logging/assert_with_message_test.cpp create mode 100644 src/test/logging/log_stream_test.cpp create mode 100644 src/test/logging/scoped_log_test.cpp create mode 100644 src/test/non_copyable_and_non_movable_test.cpp create mode 100644 src/test/non_copyable_test.cpp create mode 100644 src/test/precompiled_header.h create mode 100644 src/test/string_conversion_test.cpp create mode 100644 src/test/struct_aliasing_disable_test.cpp create mode 100644 src/utils_h/bit_cast.h create mode 100644 src/utils_h/circular_buffer.h create mode 100644 src/utils_h/concurrency/dispatcher.h create mode 100644 src/utils_h/concurrency/interruptible_condition_variable.h create mode 100644 src/utils_h/concurrency/lock_guard.h create mode 100644 src/utils_h/concurrency/observable_atomic_variable.h create mode 100644 src/utils_h/concurrency/one_shot_timer.h create mode 100644 src/utils_h/concurrency/waiting_queue.h create mode 100644 src/utils_h/concurrency/watchdog.h create mode 100644 src/utils_h/logging/assert_with_message.h create mode 100644 src/utils_h/logging/log_stream.h create mode 100644 src/utils_h/logging/scoped_log.h create mode 100644 src/utils_h/non_copyable.h create mode 100644 src/utils_h/non_copyable_and_non_movable.h create mode 100644 src/utils_h/preprocessor.h create mode 100644 src/utils_h/string_conversion.h create mode 100644 src/utils_h/struct_aliasing_disable.h create mode 100644 src/utils_h/type_traits.h create mode 100644 tools/cmake/catch2_lib.cmake create mode 100644 tools/cmake/clang_format.cmake create mode 100644 tools/cmake/compiler_options.cmake diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..9b22225 --- /dev/null +++ b/.clang-format @@ -0,0 +1,6 @@ +# https://clang.llvm.org/docs/ClangFormatStyleOptions.html +# https://zed0.co.uk/clang-format-configurator/ +BasedOnStyle: LLVM +CompactNamespaces: true +FixNamespaceComments: false +PointerAlignment: Left diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3c8f90a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/continuous-integration-workflow.yaml b/.github/workflows/continuous-integration-workflow.yaml new file mode 100644 index 0000000..69843be --- /dev/null +++ b/.github/workflows/continuous-integration-workflow.yaml @@ -0,0 +1,21 @@ +on: push + +jobs: + build: + runs-on: windows-2022 + steps: + - uses: actions/checkout@v3 + - uses: ilammy/msvc-dev-cmd@v1 + - run: ./build.ps1 + - uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + draft: true + files: out/publish/*.zip + fail_on_unmatched_files: true + - uses: actions/upload-artifact@v3 + with: + name: Build artifacts + path: out/publish/*.zip diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7117aa5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/out/ +.vs/ +.idea/ +CMakeSettings.json diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e306f15 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required (VERSION 3.17) +project(UtilsHLibrary LANGUAGES CXX) + +include(tools/cmake/compiler_options.cmake) +include(tools/cmake/clang_format.cmake) +include(tools/cmake/catch2_lib.cmake) + +file(GLOB_RECURSE src_files CONFIGURE_DEPENDS "src/*.h" "src/*.cpp") +add_executable(utils_h_test ${src_files}) +target_include_directories(utils_h_test PRIVATE "src") +target_precompile_headers(utils_h_test PRIVATE src/test/precompiled_header.h) +add_clang_format(utils_h_test) +add_catch2_lib(utils_h_test) +add_compiler_options_with_warnings(utils_h_test) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..10b8396 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 PolarGoose + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7e6fbf9 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +This project contains a small header only C++ library of helpful utilities. + +## How to use this library +As it is a header only library, you only need to add the headers from the "src/utils_h" folder to your project. diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..e71f6d2 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,48 @@ +Function Info($msg) { + Write-Host -ForegroundColor DarkGreen "`nINFO: $msg`n" +} + +Function Error($msg) { + Write-Host `n`n + Write-Error $msg + exit 1 +} + +Function CheckReturnCodeOfPreviousCommand($msg) { + if(-Not $?) { + Error "${msg}. Error code: $LastExitCode" + } +} + +Function CreateZipArchive($directory, $archiveFile) { + Info "Create a zip archive from `n '$directory' `n to `n '$archiveFile'" + New-Item $archiveFile -Force -ItemType File > $null + Compress-Archive -Force -Path $directory -DestinationPath $archiveFile +} + +Function ForceCopy($srcFile, $dstFile) { + Info "Copy `n '$srcFile' `n to `n '$dstFile'" + New-Item $dstFile -Force -ItemType File > $null + Copy-Item $srcFile -Destination $dstFile -Force +} + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$root = Resolve-Path $PSScriptRoot +$buildDir = "$root/out" +$publishDir = "$buildDir/publish" + +Info "Build the project using Cmake" +Info "Cmake generation phase" +cmake -S $root -B $buildDir -G Ninja -DCMAKE_BUILD_TYPE=Release +CheckReturnCodeOfPreviousCommand "Cmake generation phase failed" +Info "Cmake build phase" +cmake --build $buildDir +CheckReturnCodeOfPreviousCommand "Cmake building phase failed" + +Info "Run tests" +& $buildDir/utils_h_test.exe +CheckReturnCodeOfPreviousCommand "Tests failed" + +CreateZipArchive $root/src/utils_h $publishDir/utils_h.zip diff --git a/src/test/bit_cast_test.cpp b/src/test/bit_cast_test.cpp new file mode 100644 index 0000000..02a2e7e --- /dev/null +++ b/src/test/bit_cast_test.cpp @@ -0,0 +1,28 @@ +#include "utils_h/bit_cast.h" +#include "utils_h/struct_aliasing_disable.h" + +using namespace utils_h; + +template void bit_cast_test(const Source src) { + const auto dst = bit_cast(src); + REQUIRE(memcmp(&dst, &src, sizeof(src)) == 0); + + const auto new_src = bit_cast(dst); + REQUIRE(memcmp(&new_src, &src, sizeof(src)) == 0); +} + +UTILS_H_DISABLE_STRUCT_ALIASING_BEGIN +struct struct_of_64_bytes { + uint8_t a; + uint32_t b; + uint8_t c; + uint8_t d; + uint8_t e; +}; +UTILS_H_DISABLE_STRUCT_ALIASING_END + +TEST_CASE("bit_cast - Should convert data back and forth") { + bit_cast_test(1.2F); + bit_cast_test('a'); + bit_cast_test(0x12af43db4589a7c4); +} diff --git a/src/test/circular_buffer_test.cpp b/src/test/circular_buffer_test.cpp new file mode 100644 index 0000000..8aa6c21 --- /dev/null +++ b/src/test/circular_buffer_test.cpp @@ -0,0 +1,53 @@ +#include "utils_h/circular_buffer.h" + +using namespace utils_h; + +class circular_buffer_test { +protected: + void fill_buffer() { + for (size_t i{0}; i < m_capacity; i++) { + m_buffer.put(i); + } + } + + const size_t m_capacity{100}; + circular_buffer m_buffer{m_capacity}; +}; + +TEST_CASE_METHOD(circular_buffer_test, "Capacity should return correct value") { + REQUIRE(m_buffer.capacity() == m_capacity); +} + +TEST_CASE_METHOD( + circular_buffer_test, + "Get method should throw an exception when the buffer is empty") { + REQUIRE(m_buffer.is_empty() == true); + REQUIRE_THROWS_AS(m_buffer.get(), circular_buffer_is_empty_exception); +} + +TEST_CASE_METHOD( + circular_buffer_test, + "Buffer can contain the amount of elements equal to its capacity") { + fill_buffer(); + + REQUIRE(m_buffer.size() == m_capacity); + REQUIRE(m_buffer.is_full() == true); + + for (size_t i{0}; i < m_capacity; i++) { + REQUIRE(m_buffer.get() == i); + } + + REQUIRE(m_buffer.size() == 0); + REQUIRE(m_buffer.is_empty() == true); +} + +TEST_CASE_METHOD(circular_buffer_test, "Adding one element to a full buffer " + "should remove its last added element") { + fill_buffer(); + + m_buffer.put(m_capacity); + + for (size_t i{0}; i < m_capacity; i++) { + REQUIRE(m_buffer.get() == i + 1); + } +} diff --git a/src/test/concurrency/dispatcher_test.cpp b/src/test/concurrency/dispatcher_test.cpp new file mode 100644 index 0000000..8e70920 --- /dev/null +++ b/src/test/concurrency/dispatcher_test.cpp @@ -0,0 +1,73 @@ +#include "utils_h/concurrency/dispatcher.h" + +using namespace utils_h; +using namespace utils_h::concurrency; + +class dispatcher_test { +protected: + void wait_while_all_tasks_are_executed() { + _dispatcher.dispatch_and_wait([] {}); + } + + void unhandled_exception_handler() { + _unhandled_exception_handler_was_called = true; + } + + dispatcher _dispatcher{ + [&](auto& /* exception */) { unhandled_exception_handler(); }}; + bool _unhandled_exception_handler_was_called{false}; +}; + +TEST_CASE_METHOD( + dispatcher_test, + "dispatch_and_wait function should work with functions which return void") { + _dispatcher.dispatch_and_wait([&] {}); +} + +TEST_CASE_METHOD(dispatcher_test, "dispatch_and_wait function should work with " + "functions which return value") { + REQUIRE(_dispatcher.dispatch_and_wait([] { return std::string{"string"}; }) == + "string"); + REQUIRE(_dispatcher.dispatch_and_wait([] { return 1; }) == 1); +} + +TEST_CASE_METHOD(dispatcher_test, + "If a dispatched task throws an exception, an unhandled " + "exception handler should be called") { + _dispatcher.dispatch([] { throw std::runtime_error("error"); }); + wait_while_all_tasks_are_executed(); + + REQUIRE(_unhandled_exception_handler_was_called == true); +} + +TEST_CASE_METHOD(dispatcher_test, "Exception during dispatch_and_wait function " + "call should be propagated to the caller") { + REQUIRE_THROWS_AS( + _dispatcher.dispatch_and_wait([] { throw std::runtime_error("error"); }), + std::runtime_error); +} + +TEST_CASE_METHOD(dispatcher_test, + "is_dispatcher_thread function returns true if it is called " + "inside a dispatcher thread") { + REQUIRE(_dispatcher.is_dispatcher_thread() == false); + REQUIRE(_dispatcher.dispatch_and_wait( + [&] { return _dispatcher.is_dispatcher_thread(); }) == true); +} + +TEST_CASE_METHOD( + dispatcher_test, + "Tasks should be executed in the order in which they were dispatched") { + std::vector results; + + for (size_t i = 0; i < 100; i++) { + _dispatcher.dispatch([i, &results] { results.push_back(i); }); + } + + wait_while_all_tasks_are_executed(); + REQUIRE(results.size() == 100); + + for (size_t i = 0; i < 100; i++) { + REQUIRE(results[i] == i); + } +} diff --git a/src/test/concurrency/interruptible_condition_variable_test.cpp b/src/test/concurrency/interruptible_condition_variable_test.cpp new file mode 100644 index 0000000..00dd492 --- /dev/null +++ b/src/test/concurrency/interruptible_condition_variable_test.cpp @@ -0,0 +1,92 @@ +#include "utils/time_passed_method.h" +#include "utils_h/concurrency/interruptible_condition_variable.h" + +using namespace utils_h; +using namespace utils_h::concurrency; +using namespace std::chrono_literals; + +class interruptible_condition_variable_test : protected has_time_passed_method { +protected: + std::mutex _mutex; + interruptible_condition_variable _cond_var; +}; + +TEST_CASE_METHOD(interruptible_condition_variable_test, + "wait() method should throw an exception on time out") { + std::unique_lock lock{_mutex}; + + REQUIRE_THROWS_AS(_cond_var.wait_for( + lock, [] { return false; }, 10ms), + timed_out_exception); +} + +TEST_CASE_METHOD(interruptible_condition_variable_test, + "wait() method should throw an exception when interrupted") { + auto client_func{[&] { + try { + std::unique_lock lock{_mutex}; + _cond_var.wait_for( + lock, [] { return false; }, 1s); + } catch (const interrupted_exception&) { + return true; + } + return false; + }}; + + auto client1_is_interrupted{false}; + auto client2_is_interrupted{false}; + + std::thread client1{[&] { client1_is_interrupted = client_func(); }}; + std::thread client2{[&] { client2_is_interrupted = client_func(); }}; + + // sleep long enough to let client1 and client2 threads to get blocked on + // _cond_var.wait_for() call + std::this_thread::sleep_for(300ms); + + // interrupt client1 and client2 + { + std::unique_lock lock{_mutex}; + _cond_var.interrupt(lock); + } + + // wait until client1 and client2 threads get notified about the interruption + // and finished + client1.join(); + client2.join(); + + REQUIRE(client1_is_interrupted == true); + REQUIRE(client2_is_interrupted == true); +} + +TEST_CASE_METHOD( + interruptible_condition_variable_test, + "After being interrupted, all calls to wait() should throw an exception") { + std::unique_lock lock{_mutex}; + _cond_var.interrupt(lock); + + REQUIRE_THROWS_AS(_cond_var.wait_for(lock, [] { return false; }), + interrupted_exception); +} + +TEST_CASE_METHOD( + interruptible_condition_variable_test, + "wait() method should unblock when the condition becomes true") { + auto value_to_wait_for{false}; + + std::thread client{[&] { + std::unique_lock lock{_mutex}; + _cond_var.wait_for(lock, [&] { return value_to_wait_for; }); + }}; + + // wait long enough to let client thread to get blocked on wait_for() method + std::this_thread::sleep_for(300ms); + + // set the wait condition to true and wake up the client + { + std::unique_lock lock{_mutex}; + value_to_wait_for = true; + _cond_var.notify_all(); + } + + client.join(); +} diff --git a/src/test/concurrency/lock_guard_test.cpp b/src/test/concurrency/lock_guard_test.cpp new file mode 100644 index 0000000..b88eb87 --- /dev/null +++ b/src/test/concurrency/lock_guard_test.cpp @@ -0,0 +1,24 @@ +#include "utils_h/concurrency/lock_guard.h" + +struct lock_guard_test { + auto is_mutex_locked() { + const auto locked_successfully = _m.try_lock(); + if (locked_successfully) { + _m.unlock(); + } + return !locked_successfully; + } + + std::mutex _m; +}; + +TEST_CASE_METHOD(lock_guard_test, "Should locks a mutex") { + UTILS_H_LOCK_GUARD(_m); + REQUIRE(is_mutex_locked() == true); +} + +TEST_CASE_METHOD(lock_guard_test, + "Should unlock a mutex at the end of the scope") { + { UTILS_H_LOCK_GUARD(_m); } + REQUIRE(is_mutex_locked() == false); +} diff --git a/src/test/concurrency/observable_atomic_variable_test.cpp b/src/test/concurrency/observable_atomic_variable_test.cpp new file mode 100644 index 0000000..2a388a6 --- /dev/null +++ b/src/test/concurrency/observable_atomic_variable_test.cpp @@ -0,0 +1,115 @@ +#include "utils/time_passed_method.h" +#include "utils_h/concurrency/observable_atomic_variable.h" + +using namespace utils_h; +using namespace utils_h::concurrency; +using namespace std::chrono_literals; + +class observable_atomic_variable_test : protected has_time_passed_method { +protected: + void on_value_changed(const size_t& value) { + m_historyOfEvents.push_back(value); + } + + const size_t _initialValue = 1; + observable_atomic_variable m_observableAtomicVariable{ + _initialValue, [&](const auto& val) { on_value_changed(val); }}; + std::vector m_historyOfEvents; +}; + +TEST_CASE_METHOD(observable_atomic_variable_test, "constructed correctly") { + REQUIRE(m_observableAtomicVariable.get() == _initialValue); +} + +TEST_CASE_METHOD(observable_atomic_variable_test, + "When value is set, it should be saved and a client should be " + "notified with an event") { + const size_t numberOfElementsToSet = 100; + + for (size_t i = 0; i < numberOfElementsToSet; i++) { + m_observableAtomicVariable.set(i); + REQUIRE(m_observableAtomicVariable.get() == i); + } + + REQUIRE(m_historyOfEvents.size() == numberOfElementsToSet); + for (size_t i = 0; i < numberOfElementsToSet; i++) { + REQUIRE(m_historyOfEvents[i] == i); + } +} + +TEST_CASE_METHOD( + observable_atomic_variable_test, + "Setting the same value should not cause an event to be sent") { + m_observableAtomicVariable.set(_initialValue); + REQUIRE(m_historyOfEvents.empty() == true); +} + +TEST_CASE_METHOD(observable_atomic_variable_test, + "Several clients can call wait_for_value() method") { + const auto timeout = 1s; + const size_t valueToWaitFor = _initialValue + 1; + + const auto client1 = std::async(std::launch::async, [&] { + m_observableAtomicVariable.wait_for_value(valueToWaitFor, timeout); + }); + + const auto client2 = std::async(std::launch::async, [&] { + m_observableAtomicVariable.wait_for_value(valueToWaitFor, timeout); + }); + + m_observableAtomicVariable.set(valueToWaitFor); + + REQUIRE(client1.wait_for(timeout) != std::future_status::timeout); + REQUIRE(client2.wait_for(timeout) != std::future_status::timeout); +} + +TEST_CASE_METHOD( + observable_atomic_variable_test, + "Client should get an exception if waiting for a value is timed out") { + const auto timeout = 300ms; + + REQUIRE_THROWS_AS( + m_observableAtomicVariable.wait_for_value(_initialValue + 1, timeout), + timed_out_exception); + REQUIRE(time_passed() > timeout); +} + +TEST_CASE_METHOD(observable_atomic_variable_test, + "If wait() method is interrupted, a client should be " + "unblocked with an exception") { + const auto timeout = 2s; + + bool client1IsInterrupted{false}; + bool client2IsInterrupted{false}; + + auto waitCall{[&] { + try { + m_observableAtomicVariable.wait_for_value(_initialValue + 1, timeout); + } catch (const interrupted_exception&) { + return true; + } + return false; + }}; + + std::thread client1{[&] { client1IsInterrupted = waitCall(); }}; + std::thread client2{[&] { client2IsInterrupted = waitCall(); }}; + + std::this_thread::sleep_for(300ms); + m_observableAtomicVariable.interrupt(); + client1.join(); + client2.join(); + + REQUIRE(client1IsInterrupted == true); + REQUIRE(client2IsInterrupted == true); +} + +TEST_CASE_METHOD( + observable_atomic_variable_test, + "After interrupted, all wait calls should fail by throwing an exception") { + m_observableAtomicVariable.interrupt(); + + REQUIRE_THROWS_AS(m_observableAtomicVariable.wait_for_value(_initialValue), + interrupted_exception); + REQUIRE_THROWS_AS(m_observableAtomicVariable.wait_for_value(_initialValue), + interrupted_exception); +} diff --git a/src/test/concurrency/one_shot_timer_test.cpp b/src/test/concurrency/one_shot_timer_test.cpp new file mode 100644 index 0000000..d1dc9a8 --- /dev/null +++ b/src/test/concurrency/one_shot_timer_test.cpp @@ -0,0 +1,31 @@ +#include "utils/time_passed_method.h" +#include "utils_h/concurrency/observable_atomic_variable.h" +#include "utils_h/concurrency/one_shot_timer.h" + +using namespace utils_h; +using namespace utils_h::concurrency; +using namespace std::chrono_literals; + +class one_shot_timer_test : protected has_time_passed_method { +protected: + void wait_for_timer_callback_is_called() { + _timer_callback_was_called.wait_for_value(true, _time * 2); + } + + const std::chrono::milliseconds _time{300}; + observable_atomic_variable _timer_callback_was_called{false}; + std::unique_ptr _timer{std::make_unique( + _time, [&] { _timer_callback_was_called.set(true); })}; +}; + +TEST_CASE_METHOD(one_shot_timer_test, + "Timer should be started when it is constructed") { + REQUIRE_NOTHROW(wait_for_timer_callback_is_called()); + REQUIRE(time_passed() > _time); +} + +TEST_CASE_METHOD(one_shot_timer_test, + "Timer should be stopped when it is destructed") { + _timer.reset(); + REQUIRE_THROWS_AS(wait_for_timer_callback_is_called(), timed_out_exception); +} diff --git a/src/test/concurrency/utils/time_passed_method.h b/src/test/concurrency/utils/time_passed_method.h new file mode 100644 index 0000000..16fb2ca --- /dev/null +++ b/src/test/concurrency/utils/time_passed_method.h @@ -0,0 +1,12 @@ +#pragma once +#include + +class has_time_passed_method { +protected: + [[nodiscard]] auto time_passed() const { + return std::chrono::steady_clock::now() - m_startTime; + } + + const std::chrono::steady_clock::time_point m_startTime = + std::chrono::steady_clock::now(); +}; diff --git a/src/test/concurrency/waiting_queue_test.cpp b/src/test/concurrency/waiting_queue_test.cpp new file mode 100644 index 0000000..320592d --- /dev/null +++ b/src/test/concurrency/waiting_queue_test.cpp @@ -0,0 +1,169 @@ +#include "utils/time_passed_method.h" +#include "utils_h/concurrency/waiting_queue.h" + +using namespace utils_h; +using namespace utils_h::concurrency; +using namespace std::chrono_literals; + +class waiting_queue_test : protected has_time_passed_method { +protected: + void fill_waiting_queue() { + for (size_t i = 0; i < m_boundary; i++) { + m_bounded_waiting_queue.blocking_push(i); + } + } + + const size_t m_boundary{10}; + waiting_queue m_bounded_waiting_queue{m_boundary}; + waiting_queue m_unbounded_waiting_queue; +}; + +TEST_CASE_METHOD( + waiting_queue_test, + "Maximum size of unbounded queue should be equal to size_t::max") { + REQUIRE(m_unbounded_waiting_queue.max_size() == + std::numeric_limits::max()); +} + +TEST_CASE_METHOD(waiting_queue_test, "Queue should be empty by default") { + REQUIRE(m_unbounded_waiting_queue.empty() == true); +} + +TEST_CASE_METHOD(waiting_queue_test, + "User can put an unlimited amount of elements into an " + "unbounded queue and retrieve them back") { + const size_t numberOfElements{10 * 1000}; + + for (size_t i = 0; i < numberOfElements; i++) { + m_unbounded_waiting_queue.blocking_push(i); + } + REQUIRE(m_unbounded_waiting_queue.size() == numberOfElements); + + for (size_t i = 0; i < numberOfElements; i++) { + REQUIRE(m_unbounded_waiting_queue.blocking_pop() == i); + } + REQUIRE(m_unbounded_waiting_queue.empty() == true); +} + +TEST_CASE_METHOD( + waiting_queue_test, + "Reading from an empty queue should throw an exception after a time out") { + const auto timeout = 300ms; + REQUIRE_THROWS_AS(m_unbounded_waiting_queue.blocking_pop(timeout), + timed_out_exception); + REQUIRE(time_passed() > timeout); +} + +TEST_CASE_METHOD(waiting_queue_test, "Reading client should be unblocked when " + "an element is added to the queue") { + const auto timeout = 1s; + const auto time_to_wait_before_send = timeout / 2; + const auto data = 1; + + std::thread sender([&] { + std::this_thread::sleep_for(time_to_wait_before_send); + m_unbounded_waiting_queue.blocking_push(data); + }); + + REQUIRE(m_unbounded_waiting_queue.blocking_pop(timeout) == data); + REQUIRE(time_passed() > time_to_wait_before_send); + sender.join(); +} + +TEST_CASE_METHOD(waiting_queue_test, + "Sender should be blocked if the queue is full") { + fill_waiting_queue(); + + REQUIRE_THROWS_AS(m_bounded_waiting_queue.blocking_push(1, 100ms), + timed_out_exception); + REQUIRE(time_passed() > 100ms); +} + +TEST_CASE_METHOD(waiting_queue_test, + "Sender should be unblocked when the queue has free space") { + fill_waiting_queue(); + const auto timeout = 1s; + const auto time_to_wait_before_read = timeout / 2; + + std::thread receiver([&] { + std::this_thread::sleep_for(time_to_wait_before_read); + m_unbounded_waiting_queue.blocking_pop(); + }); + + m_unbounded_waiting_queue.blocking_push(1, timeout); + REQUIRE(time_passed() > time_to_wait_before_read); + receiver.join(); +} + +TEST_CASE_METHOD(waiting_queue_test, + "Receive operations should be interruptible") { + bool receiver1_interrupted{false}; + bool receiver2_interrupted{false}; + + auto receiveCall{[&] { + try { + m_unbounded_waiting_queue.blocking_pop(); + } catch (const interrupted_exception&) { + return true; + } + return false; + }}; + + std::thread receiver1{[&] { receiver1_interrupted = receiveCall(); }}; + std::thread receiver2{[&] { receiver2_interrupted = receiveCall(); }}; + + // wait until receiver threads are blocked on reading from the queue + std::this_thread::sleep_for(300ms); + + m_unbounded_waiting_queue.interrupt(); + + // wait until receivers get interrupted with an exception + receiver1.join(); + receiver2.join(); + + REQUIRE(receiver1_interrupted == true); + REQUIRE(receiver2_interrupted == true); +} + +TEST_CASE_METHOD(waiting_queue_test, + "Send operations should be interruptible") { + fill_waiting_queue(); + + auto sender1_interrupted{false}; + auto sender2_interrupted{false}; + + auto sendCall{[&] { + try { + m_bounded_waiting_queue.blocking_push(1); + } catch (const interrupted_exception&) { + return true; + } + return false; + }}; + + std::thread sender1{[&] { sender1_interrupted = sendCall(); }}; + std::thread sender2{[&] { sender2_interrupted = sendCall(); }}; + + // wait until sender threads are blocked on adding to the queue + std::this_thread::sleep_for(300ms); + + m_bounded_waiting_queue.interrupt(); + + // wait until senders get interrupted with an exception + sender1.join(); + sender2.join(); + + REQUIRE(sender1_interrupted == true); + REQUIRE(sender2_interrupted == true); +} + +TEST_CASE_METHOD( + waiting_queue_test, + "After the queue is interrupted all send and receive calls should fail") { + m_unbounded_waiting_queue.interrupt(); + + REQUIRE_THROWS_AS(m_unbounded_waiting_queue.blocking_push(1), + interrupted_exception); + REQUIRE_THROWS_AS(m_unbounded_waiting_queue.blocking_pop(), + interrupted_exception); +} diff --git a/src/test/concurrency/watchdog_test.cpp b/src/test/concurrency/watchdog_test.cpp new file mode 100644 index 0000000..d3882ce --- /dev/null +++ b/src/test/concurrency/watchdog_test.cpp @@ -0,0 +1,53 @@ +#include "utils_h/concurrency/observable_atomic_variable.h" +#include "utils_h/concurrency/watchdog.h" + +using namespace utils_h; +using namespace utils_h::concurrency; +using namespace std::chrono_literals; + +class watchdog_test { +protected: + watchdog_test() + : _watchdog{std::in_place, _watchdog_timeout, + [&] { _is_watchdog_timed_out.set(true); }} {} + + void reset_watchdog_for_some_time() { + for (auto i = 0; i < 5; i++) { + std::this_thread::sleep_for(_watchdog_timeout / 2); + _watchdog->Reset(); + } + } + + void wait_and_check_that_watchdog_triggered() { + return _is_watchdog_timed_out.wait_for_value(true, _watchdog_timeout * 2); + } + + void wait_and_check_that_watchdog_did_not_trigger() { + REQUIRE_THROWS_AS(wait_and_check_that_watchdog_triggered(), + timed_out_exception); + } + + const std::chrono::milliseconds _watchdog_timeout{300}; + observable_atomic_variable _is_watchdog_timed_out{false}; + std::optional _watchdog; +}; + +TEST_CASE_METHOD( + watchdog_test, + "Watchdog starts when it is constructed and timeouts If not reset") { + wait_and_check_that_watchdog_triggered(); +} + +TEST_CASE_METHOD(watchdog_test, "Watchdog can be reset and fires timed out " + "event when stopped being reset") { + reset_watchdog_for_some_time(); + + REQUIRE(_is_watchdog_timed_out.get() == false); + + wait_and_check_that_watchdog_triggered(); +} + +TEST_CASE_METHOD(watchdog_test, "Watchdog should be stopped on destruction") { + _watchdog.reset(); + wait_and_check_that_watchdog_did_not_trigger(); +} diff --git a/src/test/is_string_literal_test.cpp b/src/test/is_string_literal_test.cpp new file mode 100644 index 0000000..2308be7 --- /dev/null +++ b/src/test/is_string_literal_test.cpp @@ -0,0 +1,24 @@ +#include "utils_h/type_traits.h" + +using namespace utils_h; + +TEST_CASE("is_string_literal - Should detect non string literals") { + const char* msg1 = "some string"; + const char msg2[] = "some string"; + const char msg3[]{'s', 'o', 'm', 'e', '\0'}; + auto msg4 = "some string"; + const std::string msg5 = "some string"; + + static_assert(!is_string_literal::value); + static_assert(!is_string_literal::value); + static_assert(!is_string_literal::value); + static_assert(!is_string_literal::value); + static_assert(!is_string_literal::value); +} + +TEST_CASE("is_string_literal - Should detect string literals") { + const auto& msg1 = "some string"; + + static_assert(is_string_literal::value); + static_assert(is_string_literal::value); +} diff --git a/src/test/logging/assert_with_message_test.cpp b/src/test/logging/assert_with_message_test.cpp new file mode 100644 index 0000000..3b6f961 --- /dev/null +++ b/src/test/logging/assert_with_message_test.cpp @@ -0,0 +1,6 @@ +#include "utils_h/logging/assert_with_message.h" + +TEST_CASE("assert_macro") { + UTILS_H_ASSERT_MSG(true, "some message"); + UTILS_H_ASSERT_MSG(2 == 2, "some message"); +} diff --git a/src/test/logging/log_stream_test.cpp b/src/test/logging/log_stream_test.cpp new file mode 100644 index 0000000..b4e290c --- /dev/null +++ b/src/test/logging/log_stream_test.cpp @@ -0,0 +1,18 @@ +#include "utils_h/logging/log_stream.h" + +using namespace utils_h; +using namespace utils_h::logging; + +struct log_stream_test { + void log_function(const std::string& message) { m_logMessage = message; } + + std::string m_logMessage; +}; + +TEST_CASE_METHOD(log_stream_test, "Works correctly") { + log_stream{[&](const std::string& str) { log_function(str); }}.get_stream() + << "a" + << "b" << 10; + + REQUIRE(m_logMessage == "ab10"); +} diff --git a/src/test/logging/scoped_log_test.cpp b/src/test/logging/scoped_log_test.cpp new file mode 100644 index 0000000..09c577d --- /dev/null +++ b/src/test/logging/scoped_log_test.cpp @@ -0,0 +1,33 @@ +#include "utils_h/logging/scoped_log.h" + +using namespace utils_h; +using namespace utils_h::logging; +using namespace Catch::Matchers; + +class scoped_log_test { +protected: + void log_function(const std::string& message) { _log_message = message; } + + std::string _log_message; +}; + +TEST_CASE_METHOD(scoped_log_test, + "Should write correct information to the log") { + { + scoped_log scopedLog{"LineInfo ", + [&](const std::string& msg) { log_function(msg); }}; + REQUIRE(_log_message == "LineInfo Enter"); + } + REQUIRE_THAT(_log_message, Matches(R"(LineInfo Exit\(\d+ms\))")); +} + +TEST_CASE_METHOD(scoped_log_test, "Should detect exceptions") { + try { + scoped_log scopedLog{"LineInfo ", + [&](const std::string& msg) { log_function(msg); }}; + REQUIRE(_log_message == "LineInfo Enter"); + throw std::runtime_error("error"); + } catch (const std::runtime_error&) { + REQUIRE_THAT(_log_message, Matches(R"(LineInfo Exception\(\d+ms\))")); + } +} diff --git a/src/test/non_copyable_and_non_movable_test.cpp b/src/test/non_copyable_and_non_movable_test.cpp new file mode 100644 index 0000000..a675434 --- /dev/null +++ b/src/test/non_copyable_and_non_movable_test.cpp @@ -0,0 +1,10 @@ +#include "utils_h/non_copyable_and_non_movable.h" +#include "utils_h/type_traits.h" + +using namespace utils_h; + +class non_copyable_and_non_movable_class : non_copyable_and_non_movable {}; +static_assert(!is_copyable::value); +static_assert(!is_movable::value); +static_assert( + std::is_default_constructible_v); diff --git a/src/test/non_copyable_test.cpp b/src/test/non_copyable_test.cpp new file mode 100644 index 0000000..34c0cdc --- /dev/null +++ b/src/test/non_copyable_test.cpp @@ -0,0 +1,9 @@ +#include "utils_h/non_copyable.h" +#include "utils_h/type_traits.h" + +using namespace utils_h; + +class non_copyable_but_movable_class : non_copyable {}; +static_assert(!is_copyable::value); +static_assert(is_movable::value); +static_assert(std::is_default_constructible_v); diff --git a/src/test/precompiled_header.h b/src/test/precompiled_header.h new file mode 100644 index 0000000..4de0a6b --- /dev/null +++ b/src/test/precompiled_header.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/src/test/string_conversion_test.cpp b/src/test/string_conversion_test.cpp new file mode 100644 index 0000000..18b375b --- /dev/null +++ b/src/test/string_conversion_test.cpp @@ -0,0 +1,11 @@ +#include "utils_h/string_conversion.h" + +using namespace utils_h; + +TEST_CASE("utf8 to utf16") { + std::string utf8_str = "車B1234 こんにちは"; + std::wstring utf16_str = L"車B1234 こんにちは"; + + REQUIRE(to_utf16(utf8_str) == utf16_str); + REQUIRE(to_utf8(utf16_str) == utf8_str); +} diff --git a/src/test/struct_aliasing_disable_test.cpp b/src/test/struct_aliasing_disable_test.cpp new file mode 100644 index 0000000..8e4eff5 --- /dev/null +++ b/src/test/struct_aliasing_disable_test.cpp @@ -0,0 +1,15 @@ +#include "utils_h/struct_aliasing_disable.h" + +struct AliasedStruct { + std::array a; + uint32_t b; +}; +static_assert(sizeof(AliasedStruct) > 7, "Compiler should add padding"); + +UTILS_H_DISABLE_STRUCT_ALIASING_BEGIN +struct PackedStruct { + std::array a; + uint32_t b; +}; +UTILS_H_DISABLE_STRUCT_ALIASING_END +static_assert(sizeof(PackedStruct) == 7, "Compiler shouldn't add padding"); diff --git a/src/utils_h/bit_cast.h b/src/utils_h/bit_cast.h new file mode 100644 index 0000000..64189b2 --- /dev/null +++ b/src/utils_h/bit_cast.h @@ -0,0 +1,21 @@ +#pragma once +#include +#include + +namespace utils_h { + +template +static Dest bit_cast(const Source& source) { + static_assert(sizeof(Dest) == sizeof(Source), + "Source and Dest sizes should be the same"); + static_assert(std::is_trivially_copyable_v, + "Source should be trivially copyable"); + static_assert(std::is_trivially_copyable_v, + "Dest should be trivially copyable"); + + Dest dest; + memcpy(&dest, &source, sizeof(dest)); + return dest; +} + +} // namespace UtilsH diff --git a/src/utils_h/circular_buffer.h b/src/utils_h/circular_buffer.h new file mode 100644 index 0000000..3a5ec72 --- /dev/null +++ b/src/utils_h/circular_buffer.h @@ -0,0 +1,64 @@ +#pragma once +#include +#include + +namespace utils_h { + +struct circular_buffer_is_empty_exception final : public std::runtime_error { + explicit circular_buffer_is_empty_exception(char const* const msg) + : std::runtime_error(msg) {} +}; + +template class circular_buffer { +public: + explicit circular_buffer(const size_t bufferSize) : _buffer(bufferSize) {} + + void put(T item) { + _buffer[_head] = std::move(item); + + if (_isFull) { + _tail = (_tail + 1) % capacity(); + } + + _head = (_head + 1) % capacity(); + _isFull = _head == _tail; + } + + T get() { + if (is_empty()) { + throw circular_buffer_is_empty_exception("circular_buffer is empty"); + } + + auto val{_buffer[_tail]}; + _isFull = false; + _tail = (_tail + 1) % capacity(); + + return val; + } + + [[nodiscard]] bool is_empty() const { return !_isFull && _head == _tail; } + + [[nodiscard]] bool is_full() const { return _isFull; } + + [[nodiscard]] size_t capacity() const { return _buffer.size(); } + + [[nodiscard]] size_t size() const { + if (_isFull) { + return capacity(); + } + + if (_head >= _tail) { + return _head - _tail; + } + + return capacity() + _head - _tail; + } + +private: + size_t _head{0}; + size_t _tail{0}; + bool _isFull{false}; + std::vector _buffer; +}; + +} diff --git a/src/utils_h/concurrency/dispatcher.h b/src/utils_h/concurrency/dispatcher.h new file mode 100644 index 0000000..1e025dc --- /dev/null +++ b/src/utils_h/concurrency/dispatcher.h @@ -0,0 +1,74 @@ +#pragma once +#include "utils_h/concurrency/waiting_queue.h" +#include "utils_h/non_copyable_and_non_movable.h" +#include +#include + +namespace utils_h::concurrency { + +class dispatcher final : non_copyable_and_non_movable { + using Task = std::function; + +public: + explicit dispatcher(std::function + unhandled_exception_handler) + : _unhandled_exception_handler{std::move(unhandled_exception_handler)}, + _task_processing_thread{&dispatcher::tasks_processing, this} {} + + [[nodiscard]] bool is_dispatcher_thread() const { + return std::this_thread::get_id() == _task_processing_thread.get_id(); + } + + void dispatch(Task task) { _tasks_queue.blocking_push(std::move(task)); } + + template auto dispatch_and_wait(T task) -> decltype(task()) { + std::promise taskResult; + auto taskResultFuture{taskResult.get_future()}; + + _tasks_queue.blocking_push([task = std::move(task), &taskResult] { + try { + if constexpr (std::is_void_v) { + task(); + taskResult.set_value(); + } else { + taskResult.set_value(task()); + } + } catch (...) { + taskResult.set_exception(std::current_exception()); + } + }); + + return taskResultFuture.get(); + } + + ~dispatcher() { + _tasks_queue.interrupt(); + _task_processing_thread.join(); + } + +private: + void tasks_processing() { + try { + while (true) { + execute(_tasks_queue.blocking_pop()); + } + } catch (const interrupted_exception&) { + /* ignore */ + } + } + + void execute(Task&& task) const { + try { + task(); + } catch (...) { + _unhandled_exception_handler(std::current_exception()); + } + } + + const std::function + _unhandled_exception_handler; + waiting_queue _tasks_queue; + std::thread _task_processing_thread; +}; + +} diff --git a/src/utils_h/concurrency/interruptible_condition_variable.h b/src/utils_h/concurrency/interruptible_condition_variable.h new file mode 100644 index 0000000..71697f4 --- /dev/null +++ b/src/utils_h/concurrency/interruptible_condition_variable.h @@ -0,0 +1,59 @@ +#pragma once +#include "utils_h/non_copyable_and_non_movable.h" +#include +#include +#include +#include + +namespace utils_h::concurrency { + +struct timed_out_exception final : public std::runtime_error { + explicit timed_out_exception(char const* const msg) + : std::runtime_error(msg) {} +}; + +struct interrupted_exception final : public std::runtime_error { + explicit interrupted_exception(char const* const msg) + : std::runtime_error(msg) {} +}; + +class interruptible_condition_variable : non_copyable_and_non_movable { +public: + void wait_for( + std::unique_lock& lock, + const std::function& predicate, + const std::optional timeout = {}) { + if (timeout) { + m_cond_var.wait_for(lock, *timeout, [&] { + return predicate() || _is_interrupt_requested; + }); + } else { + m_cond_var.wait(lock, + [&] { return predicate() || _is_interrupt_requested; }); + } + + if (_is_interrupt_requested) { + throw interrupted_exception( + "interruptible_condition_variable: interrupted"); + } + if (!predicate()) { + throw timed_out_exception("interruptible_condition_variable: timed out"); + } + } + + void notify_one() { m_cond_var.notify_one(); } + void notify_all() { m_cond_var.notify_all(); } + + // The caller should pass the same lock that was used during the wait_for() + // call + void interrupt(std::unique_lock& /* lock */) { + _is_interrupt_requested = true; + m_cond_var.notify_all(); + } + +private: + std::condition_variable m_cond_var; + bool _is_interrupt_requested{false}; +}; + +} diff --git a/src/utils_h/concurrency/lock_guard.h b/src/utils_h/concurrency/lock_guard.h new file mode 100644 index 0000000..12e695e --- /dev/null +++ b/src/utils_h/concurrency/lock_guard.h @@ -0,0 +1,6 @@ +#pragma once +#include "utils_h/preprocessor.h" +#include + +#define UTILS_H_LOCK_GUARD(mutex) \ + std::lock_guard> UTILS_H_UNIQUE_NAME(mutex) diff --git a/src/utils_h/concurrency/observable_atomic_variable.h b/src/utils_h/concurrency/observable_atomic_variable.h new file mode 100644 index 0000000..9962020 --- /dev/null +++ b/src/utils_h/concurrency/observable_atomic_variable.h @@ -0,0 +1,52 @@ +#pragma once +#include "utils_h/concurrency/interruptible_condition_variable.h" +#include "utils_h/non_copyable_and_non_movable.h" +#include + +namespace utils_h::concurrency { + +template +class observable_atomic_variable final : non_copyable_and_non_movable { +public: + explicit observable_atomic_variable( + T value, std::function on_value_changed_callback = + [](const T& /* val */) {}) + : _on_value_changed_callback{std::move(on_value_changed_callback)}, + _value{std::move(value)} {} + + auto get() { + std::lock_guard lock{_mutex}; + return _value; + } + + void set(T value) { + std::lock_guard lock{_mutex}; + if (_value != value) { + _value = std::move(value); + _condVar.notify_all(); + _on_value_changed_callback(_value); + } + } + + void wait_for_value( + const T& value, + const std::optional timeout = {}) { + std::unique_lock lock{_mutex}; + + _condVar.wait_for( + lock, [&] { return _value == value; }, timeout); + } + + void interrupt() { + std::unique_lock lock{_mutex}; + _condVar.interrupt(lock); + } + +private: + mutable std::mutex _mutex; + interruptible_condition_variable _condVar; + std::function _on_value_changed_callback; + T _value; +}; + +} diff --git a/src/utils_h/concurrency/one_shot_timer.h b/src/utils_h/concurrency/one_shot_timer.h new file mode 100644 index 0000000..836db5e --- /dev/null +++ b/src/utils_h/concurrency/one_shot_timer.h @@ -0,0 +1,43 @@ +#pragma once +#include "utils_h/concurrency/observable_atomic_variable.h" +#include "utils_h/non_copyable_and_non_movable.h" +#include +#include + +namespace utils_h::concurrency { + +class one_shot_timer final : non_copyable_and_non_movable { +public: + one_shot_timer(const std::chrono::steady_clock::duration time, + std::function callback) + : _time{time}, _callback{std::move(callback)}, + _thread{&one_shot_timer::thread, this} {} + + ~one_shot_timer() { + _stopRequested.set(true); + _thread.join(); + } + +private: + void thread() { + wait_while_time_passes(); + if (!_stopRequested.get()) { + _callback(); + } + } + + void wait_while_time_passes() { + try { + _stopRequested.wait_for_value(true, _time); + } catch (const timed_out_exception&) { + /* ignore */ + } + } + + const std::chrono::steady_clock::duration _time; + const std::function _callback; + observable_atomic_variable _stopRequested{false}; + std::thread _thread; +}; + +} \ No newline at end of file diff --git a/src/utils_h/concurrency/waiting_queue.h b/src/utils_h/concurrency/waiting_queue.h new file mode 100644 index 0000000..764299d --- /dev/null +++ b/src/utils_h/concurrency/waiting_queue.h @@ -0,0 +1,64 @@ +#pragma once +#include "utils_h/concurrency/interruptible_condition_variable.h" +#include "utils_h/non_copyable_and_non_movable.h" +#include + +namespace utils_h::concurrency { + +template +class waiting_queue final : private non_copyable_and_non_movable { +public: + explicit waiting_queue( + const size_t max_size = std::numeric_limits::max()) + : _max_size{max_size} {} + + auto size() const { + std::lock_guard lock{_mutex}; + return _queue.size(); + } + + auto empty() const { + std::lock_guard lock{_mutex}; + return _queue.empty(); + } + + auto max_size() const { return _max_size; } + + void blocking_push( + T val, + const std::optional timeout = {}) { + std::unique_lock lock{_mutex}; + + _cond_var.wait_for( + lock, [&] { return _queue.size() < _max_size; }, timeout); + + _queue.emplace(std::move(val)); + _cond_var.notify_one(); + } + + T blocking_pop( + const std::optional timeout = {}) { + std::unique_lock lock{_mutex}; + + _cond_var.wait_for( + lock, [&] { return !_queue.empty(); }, timeout); + + T value{std::move(_queue.front())}; + _queue.pop(); + _cond_var.notify_one(); + return value; + } + + void interrupt() { + std::unique_lock lock{_mutex}; + _cond_var.interrupt(lock); + } + +private: + const size_t _max_size; + mutable std::mutex _mutex; + interruptible_condition_variable _cond_var; + std::queue _queue; +}; + +} diff --git a/src/utils_h/concurrency/watchdog.h b/src/utils_h/concurrency/watchdog.h new file mode 100644 index 0000000..06098a0 --- /dev/null +++ b/src/utils_h/concurrency/watchdog.h @@ -0,0 +1,24 @@ +#pragma once +#include "utils_h/concurrency/one_shot_timer.h" +#include "utils_h/non_copyable_and_non_movable.h" +#include +#include + +namespace utils_h::concurrency { + +class watchdog final : non_copyable_and_non_movable { +public: + watchdog(std::chrono::steady_clock::duration timeout, + std::function on_triggered) + : _on_triggered{std::move(on_triggered)}, _timeout{timeout}, + _timer{std::in_place, _timeout, _on_triggered} {} + + void Reset() { _timer.emplace(_timeout, _on_triggered); } + +private: + const std::function _on_triggered; + const std::chrono::steady_clock::duration _timeout; + std::optional _timer; +}; + +} diff --git a/src/utils_h/logging/assert_with_message.h b/src/utils_h/logging/assert_with_message.h new file mode 100644 index 0000000..3737b40 --- /dev/null +++ b/src/utils_h/logging/assert_with_message.h @@ -0,0 +1,9 @@ +#pragma once +#include "utils_h/type_traits.h" +#include +#include + +#define UTILS_H_ASSERT_MSG(expression, message) \ + static_assert(utils_h::is_string_literal::value, \ + "'message' should be a string literal"); \ + assert((expression) && (message)) diff --git a/src/utils_h/logging/log_stream.h b/src/utils_h/logging/log_stream.h new file mode 100644 index 0000000..971569f --- /dev/null +++ b/src/utils_h/logging/log_stream.h @@ -0,0 +1,23 @@ +#pragma once +#include "utils_h/non_copyable_and_non_movable.h" +#include +#include + +namespace utils_h::logging { + +class log_stream final : non_copyable_and_non_movable { + using log_function = std::function; + +public: + explicit log_stream(log_function log) : _log_function{std::move(log)} {} + + std::ostringstream& get_stream() { return _stream; } + + ~log_stream() { _log_function(_stream.str()); } + +private: + std::ostringstream _stream; + log_function _log_function; +}; + +} diff --git a/src/utils_h/logging/scoped_log.h b/src/utils_h/logging/scoped_log.h new file mode 100644 index 0000000..19338d1 --- /dev/null +++ b/src/utils_h/logging/scoped_log.h @@ -0,0 +1,44 @@ +#pragma once +#include "utils_h/non_copyable_and_non_movable.h" +#include +#include +#include + +namespace utils_h::logging { + +class scoped_log final : non_copyable_and_non_movable { + using log_function = std::function; + +public: + scoped_log(const char* const line_info, log_function log) + : _line_info{line_info}, _log_function{std::move(log)} { + _log_function(_line_info + "Enter"); + } + + ~scoped_log() { + std::stringstream stream; + stream << _line_info + << (is_destructor_called_because_of_exception() ? "Exception" + : "Exit") + << "(" << time_passed().count() << "ms)"; + _log_function(stream.str()); + } + +private: + [[nodiscard]] bool is_destructor_called_because_of_exception() const { + return std::uncaught_exceptions() > _uncaught_exceptions_when_created; + } + + [[nodiscard]] std::chrono::milliseconds time_passed() const { + return std::chrono::duration_cast( + std::chrono::steady_clock::now() - _creation_time); + } + + const std::chrono::steady_clock::time_point _creation_time{ + std::chrono::steady_clock::now()}; + const int _uncaught_exceptions_when_created{std::uncaught_exceptions()}; + const std::string _line_info; + log_function _log_function; +}; + +} diff --git a/src/utils_h/non_copyable.h b/src/utils_h/non_copyable.h new file mode 100644 index 0000000..800ca2d --- /dev/null +++ b/src/utils_h/non_copyable.h @@ -0,0 +1,18 @@ +#pragma once + +namespace utils_h { + +class non_copyable { +protected: + non_copyable() = default; + ~non_copyable() = default; + + non_copyable(non_copyable&&) = default; + non_copyable& operator=(non_copyable&&) = default; + +public: + non_copyable(const non_copyable&) = delete; + non_copyable& operator=(const non_copyable&) = delete; +}; + +} diff --git a/src/utils_h/non_copyable_and_non_movable.h b/src/utils_h/non_copyable_and_non_movable.h new file mode 100644 index 0000000..5a91370 --- /dev/null +++ b/src/utils_h/non_copyable_and_non_movable.h @@ -0,0 +1,17 @@ +#pragma once +#include "non_copyable.h" + +namespace utils_h { + +class non_copyable_and_non_movable : non_copyable { +protected: + non_copyable_and_non_movable() = default; + ~non_copyable_and_non_movable() = default; + +public: + non_copyable_and_non_movable(non_copyable_and_non_movable&&) = delete; + non_copyable_and_non_movable& + operator=(non_copyable_and_non_movable&&) = delete; +}; + +} diff --git a/src/utils_h/preprocessor.h b/src/utils_h/preprocessor.h new file mode 100644 index 0000000..267b550 --- /dev/null +++ b/src/utils_h/preprocessor.h @@ -0,0 +1,20 @@ +#pragma once + +#define UTILS_H_PRIV_NUMBER_TO_STR_HELPER(x) #x +#define UTILS_H_NUMBER_TO_STR(x) UTILS_H_PRIV_NUMBER_TO_STR_HELPER(x) + +#define UTILS_H_PRIV_NUMBER_TO_WSTR_HELPER(x) L#x +#define UTILS_H_NUMBER_TO_WSTR(x) UTILS_H_PRIV_NUMBER_TO_WSTR_HELPER(x) + +#define UTILS_H_PRIV_STR_TO_WSTR_HELPER(x) L##x +#define UTILS_H_STR_TO_WSTR(x) UTILS_H_PRIV_STR_TO_WSTR_HELPER(x) + +#define UTILS_H_PRIV_CONCATENATE_HELPER(x, y) x##y +#define UTILS_H_CONCATENATE(x, y) UTILS_H_PRIV_CONCATENATE_HELPER(x, y) + +#define UTILS_H_LINE_STR UTILS_H_NUMBER_TO_STR(__LINE__) +#define UTILS_H_LINE_WSTR UTILS_H_NUMBER_TO_WSTR(__LINE__) +#define UTILS_H_FILE_WSTR UTILS_H_STR_TO_WSTR(__FILE__) +#define UTILS_H_FUNC_WSTR UTILS_H_STR_TO_WSTR(__FUNCTION__) + +#define UTILS_H_UNIQUE_NAME UTILS_H_CONCATENATE(utilsUniqueName, __COUNTER__) diff --git a/src/utils_h/string_conversion.h b/src/utils_h/string_conversion.h new file mode 100644 index 0000000..af31298 --- /dev/null +++ b/src/utils_h/string_conversion.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include +#include +#include + +namespace utils_h { + +inline std::wstring to_utf16(std::string_view str) { + static std::wstring_convert> converter; + return converter.from_bytes(str.data()); +} + +inline std::string to_utf8(std::wstring_view wideStr) { + static std::wstring_convert> converter; + return converter.to_bytes(wideStr.data()); +} + +} diff --git a/src/utils_h/struct_aliasing_disable.h b/src/utils_h/struct_aliasing_disable.h new file mode 100644 index 0000000..4f987c3 --- /dev/null +++ b/src/utils_h/struct_aliasing_disable.h @@ -0,0 +1,4 @@ +#pragma once + +#define UTILS_H_DISABLE_STRUCT_ALIASING_BEGIN _Pragma("pack(push, 1)") +#define UTILS_H_DISABLE_STRUCT_ALIASING_END _Pragma("pack(pop)") diff --git a/src/utils_h/type_traits.h b/src/utils_h/type_traits.h new file mode 100644 index 0000000..2a6cbcb --- /dev/null +++ b/src/utils_h/type_traits.h @@ -0,0 +1,21 @@ +#pragma once +#include + +namespace utils_h { + +template +struct is_copyable : std::bool_constant && + std::is_copy_assignable_v> {}; + +template +struct is_movable : std::bool_constant && + std::is_move_assignable_v> {}; + +template +struct is_string_literal + : std::is_same>]>> { +}; + +} diff --git a/tools/cmake/catch2_lib.cmake b/tools/cmake/catch2_lib.cmake new file mode 100644 index 0000000..a0c1278 --- /dev/null +++ b/tools/cmake/catch2_lib.cmake @@ -0,0 +1,18 @@ +file(DOWNLOAD + https://github.com/catchorg/Catch2/releases/download/v3.2.1/catch_amalgamated.hpp + ${CMAKE_BINARY_DIR}/catch2/catch_amalgamated.hpp + EXPECTED_MD5 9fcaf5b0150a144543bf5d8177a48ebd) +file(DOWNLOAD + https://github.com/catchorg/Catch2/releases/download/v3.2.1/catch_amalgamated.cpp + ${CMAKE_BINARY_DIR}/catch2/catch_amalgamated.cpp + EXPECTED_MD5 aebd1ac63e23b0aeba85c6e3f0ce90a2) + +add_library(catch2 + ${CMAKE_BINARY_DIR}/catch2/catch_amalgamated.hpp + ${CMAKE_BINARY_DIR}/catch2/catch_amalgamated.cpp) +add_compiler_options_with_warnings_disabled(catch2) +target_include_directories(catch2 INTERFACE ${CMAKE_BINARY_DIR}/catch2) + +function(add_catch2_lib target) + target_link_libraries(${target} catch2) +endfunction() diff --git a/tools/cmake/clang_format.cmake b/tools/cmake/clang_format.cmake new file mode 100644 index 0000000..ed1eb39 --- /dev/null +++ b/tools/cmake/clang_format.cmake @@ -0,0 +1,29 @@ +set(CLANGFORMAT_EXECUTABLE "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/Llvm/x64/bin/clang-format.exe") + +function(add_clang_format target) + if(NOT EXISTS ${CLANGFORMAT_EXECUTABLE}) + message(WARNING "clang-format is not found. Skip code formatting step") + return() + endif() + + get_target_property(target_src_files ${target} SOURCES) + + foreach(src_file_name ${target_src_files}) + get_filename_component(src_file_full_name ${src_file_name} ABSOLUTE) + list(APPEND src_files_full_names ${src_file_full_name}) + endforeach() + + add_custom_target(${target}_clangformat + COMMAND + ${CLANGFORMAT_EXECUTABLE} + -style=file + -i + ${src_files_full_names} + WORKING_DIRECTORY + ${CMAKE_SOURCE_DIR} + COMMENT + "Formatting '${target}' project's source code with ${CLANGFORMAT_EXECUTABLE} ..." + ) + + add_dependencies(${target} ${target}_clangformat) +endfunction() diff --git a/tools/cmake/compiler_options.cmake b/tools/cmake/compiler_options.cmake new file mode 100644 index 0000000..9810d29 --- /dev/null +++ b/tools/cmake/compiler_options.cmake @@ -0,0 +1,19 @@ +if (NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + message(FATAL_ERROR "Unsupported compiler: ${CMAKE_CXX_COMPILER_ID}") +endif() + +function(_add_compiler_options target) + target_compile_features(${target} PRIVATE cxx_std_20) + target_compile_options(${target} PRIVATE /utf-8 /external:anglebrackets /external:W0) + target_compile_definitions(${target} PRIVATE _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING) +endfunction() + +function(add_compiler_options_with_warnings target) + _add_compiler_options(${target}) + target_compile_options(${target} PRIVATE /WX /W4) +endfunction() + +function(add_compiler_options_with_warnings_disabled target) + _add_compiler_options(${target}) + target_compile_options(${target} PRIVATE /W0) +endfunction()