diff --git a/CMakeLists.txt b/CMakeLists.txt index 1059af5..48d4306 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -116,6 +116,19 @@ install( # DESTINATION "${MOSTLYHARMLESS_INSTALL_DEST}" # COMPONENT MostlyHarmless # ) +if(TESTS) + set(CMAKE_DEBUG_POSTFIX "") + FetchContent_Declare(Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.0.1 + GIT_SHALLOW ON) + FetchContent_MakeAvailable(Catch2) + add_subdirectory(tests) + add_executable(mostly-harmless-tests) + target_sources(mostly-harmless-tests PRIVATE ${MOSTLYHARMLESS_TEST_SOURCE}) + target_link_libraries(mostly-harmless-tests PUBLIC MostlyHarmless Catch2::Catch2WithMain) + +endif() if(${PROJECT_IS_TOP_LEVEL}) add_subdirectory(docs) diff --git a/include/mostly_harmless/utils/mostlyharmless_TaskThread.h b/include/mostly_harmless/utils/mostlyharmless_TaskThread.h index 64e3e8d..0daf6e1 100644 --- a/include/mostly_harmless/utils/mostlyharmless_TaskThread.h +++ b/include/mostly_harmless/utils/mostlyharmless_TaskThread.h @@ -6,6 +6,7 @@ #define MOSTLYHARMLESS_MOSTLYHARMLESS_TASKTHREAD_H #include #include +#include namespace mostly_harmless::utils { /** * \brief Wrapper around a std::thread to perform a single action, then exit. @@ -20,6 +21,16 @@ namespace mostly_harmless::utils { */ void perform(); + /** + * Sleeps the thread until `wake` is called. Only call this from the thread! + */ + void sleep(); + + /** + * Wakes a thread previously suspended with `sleep()`. + */ + void wake(); + /** * Sets an internal atomic bool to specify that the thread should exit, accessible through `threadShouldExit`. Note that this doesn't actually kill the thread, * it's up to you to do something with the bool. @@ -41,8 +52,12 @@ namespace mostly_harmless::utils { */ std::function action{ nullptr }; private: + std::mutex m_mutex; + std::condition_variable m_conditionVariable; + std::atomic m_canWakeUp{ false }; std::atomic m_isThreadRunning{ false }; std::atomic m_threadShouldExit{ false }; + }; } #endif // MOSTLYHARMLESS_MOSTLYHARMLESS_TASKTHREAD_H diff --git a/source/utils/mostlyharmless_TaskThread.cpp b/source/utils/mostlyharmless_TaskThread.cpp index 38c8841..7ce54c2 100644 --- a/source/utils/mostlyharmless_TaskThread.cpp +++ b/source/utils/mostlyharmless_TaskThread.cpp @@ -20,6 +20,18 @@ namespace mostly_harmless::utils { worker.detach(); } + void TaskThread::sleep() { + m_canWakeUp = false; + std::unique_lock lock(m_mutex); + m_conditionVariable.wait(lock, [this]() -> bool{ return m_canWakeUp; }); + } + + void TaskThread::wake() { + std::lock_guard guard{ m_mutex }; + m_canWakeUp = true; + m_conditionVariable.notify_one(); + } + void TaskThread::signalThreadShouldExit() { m_threadShouldExit.store(true); } @@ -27,4 +39,8 @@ namespace mostly_harmless::utils { bool TaskThread::threadShouldExit() const noexcept { return m_threadShouldExit; } + + bool TaskThread::isThreadRunning() const noexcept { + return m_isThreadRunning; + } } \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..ebaaa2a --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,5 @@ +set(MOSTLYHARMLESS_TEST_SOURCE + ${CMAKE_CURRENT_SOURCE_DIR}/mostlyharmless_TestCreatePluginImpl.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mostlyharmless_TestDescriptor.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_TaskThreadTests.cpp +PARENT_SCOPE) \ No newline at end of file diff --git a/tests/mostlyharmless_TestCreatePluginImpl.cpp b/tests/mostlyharmless_TestCreatePluginImpl.cpp new file mode 100644 index 0000000..08b7113 --- /dev/null +++ b/tests/mostlyharmless_TestCreatePluginImpl.cpp @@ -0,0 +1,9 @@ +// +// Created by Syl on 12/08/2024. +// +#include +namespace mostly_harmless::entry { + const clap_plugin* clap_create_plugin(const clap_plugin_factory* /*f*/, const clap_host* /*h*/, const char* /*id*/) { + return nullptr; + } +} \ No newline at end of file diff --git a/tests/mostlyharmless_TestDescriptor.cpp b/tests/mostlyharmless_TestDescriptor.cpp new file mode 100644 index 0000000..064829f --- /dev/null +++ b/tests/mostlyharmless_TestDescriptor.cpp @@ -0,0 +1,34 @@ + +#include +namespace mostly_harmless { + // clang-format off + static BusConfig s_audioBusConfig { BusConfig::InputOutput }; + static BusConfig s_noteBusConfig { BusConfig::None }; + + static std::vector s_features { "audio-effect", nullptr }; + static clap_plugin_descriptor s_descriptor{ + .clap_version = CLAP_VERSION, + .id = "slma.gain", + .name = "Gain", + .vendor = "", + .url = "", + .manual_url = "", + .support_url = "", + .version = "", + .description = "", + .features = s_features.data() + }; + + clap_plugin_descriptor& getDescriptor() { + return s_descriptor; + } + + BusConfig getAudioBusConfig() noexcept { + return s_audioBusConfig; + } + + BusConfig getNoteBusConfig() noexcept { + return s_noteBusConfig; + } + // clang-format on +} // namespace mostly_harmless diff --git a/tests/utils/mostlyharmless_TaskThreadTests.cpp b/tests/utils/mostlyharmless_TaskThreadTests.cpp new file mode 100644 index 0000000..5aa900a --- /dev/null +++ b/tests/utils/mostlyharmless_TaskThreadTests.cpp @@ -0,0 +1,55 @@ +// +// Created by Syl on 12/08/2024. +// +#include +#include +#include +namespace mostly_harmless::testing { + TEST_CASE("Test TaskThread") { + std::mutex mutex; + mostly_harmless::utils::TaskThread taskThread; + SECTION("Wait for lock") { + auto x{ false }; + auto task = [&mutex, &x]() -> void { + std::scoped_lock sl{ mutex }; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + x = true; + }; + taskThread.action = std::move(task); + taskThread.perform(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + REQUIRE(taskThread.isThreadRunning()); + // Sleep for a ms so the task has a chance to acquire the mutex.. + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + std::scoped_lock sl{ mutex }; + REQUIRE(x); + REQUIRE(!taskThread.isThreadRunning()); + } + + SECTION("Kill") { + auto task = [&taskThread]() -> void { + while(!taskThread.threadShouldExit()); + }; + taskThread.action = std::move(task); + taskThread.perform(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + REQUIRE(taskThread.isThreadRunning()); + taskThread.signalThreadShouldExit(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + REQUIRE(!taskThread.isThreadRunning()); + } + + SECTION("Sleep/Wake") { + auto task = [&taskThread]() -> void { + taskThread.sleep(); + }; + taskThread.action = std::move(task); + taskThread.perform(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + REQUIRE(taskThread.isThreadRunning()); + taskThread.wake(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + REQUIRE(!taskThread.isThreadRunning()); + } + } +}