diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index e006a23ce..c2f828521 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -44,11 +44,10 @@ jobs: cmake_args: "-DCODE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug" run_coverage: true build_type: "Debug" -# - name: "Coverage" -# os: macos-latest -# cmake_args: "-DCODE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug" -# run_coverage: true (this is breaking for some reason @TODO) -# build_type: "Debug" + - name: "Test" + os: macos-14 + cmake_args: "-DCMAKE_BUILD_TYPE=Debug" + build_type: "Debug" - name: "Live GUI Test" os: ubuntu-22.04 tests: "live_gui_test" @@ -58,8 +57,6 @@ jobs: build_type: "Debug" exclude: # so we don't break GitHub Actions concurrency limit - - name: "Test" - os: macos-14 - name: "Coverage" os: macos-14 - name: "Coverage" diff --git a/modules/common/chowdsp_data_structures/Structures/chowdsp_AbstractTree.cpp b/modules/common/chowdsp_data_structures/Structures/chowdsp_AbstractTree.cpp index 3b49285ee..ed1041365 100644 --- a/modules/common/chowdsp_data_structures/Structures/chowdsp_AbstractTree.cpp +++ b/modules/common/chowdsp_data_structures/Structures/chowdsp_AbstractTree.cpp @@ -157,22 +157,29 @@ void AbstractTree::clear() } template -ElementType* AbstractTree::findElement (const ElementType& element) +OptionalRef AbstractTree::findElement (const ElementType& element) { - ElementType* result = nullptr; + OptionalRef result {}; doForAllElements ( [&result, element] (ElementType& candidate) { if (element == candidate) - result = &candidate; + result = candidate; }); return result; } template -const ElementType* AbstractTree::findElement (const ElementType& element) const +OptionalRef AbstractTree::findElement (const ElementType& element) const { - return const_cast (*this).findElement (element); // NOSONAR + OptionalRef result {}; + doForAllElements ( + [&result, element] (const ElementType& candidate) + { + if (element == candidate) + result = candidate; + }); + return result; } template diff --git a/modules/common/chowdsp_data_structures/Structures/chowdsp_AbstractTree.h b/modules/common/chowdsp_data_structures/Structures/chowdsp_AbstractTree.h index aa53aff05..7ef847dcf 100644 --- a/modules/common/chowdsp_data_structures/Structures/chowdsp_AbstractTree.h +++ b/modules/common/chowdsp_data_structures/Structures/chowdsp_AbstractTree.h @@ -60,10 +60,10 @@ class AbstractTree [[nodiscard]] int size() const { return count; } /** Checks if the tree currently contains an element. If true, then return the element, else return nullptr. */ - [[nodiscard]] ElementType* findElement (const ElementType& element); + [[nodiscard]] OptionalRef findElement (const ElementType& element); /** Checks if the tree currently contains an element. If true, then return the element, else return nullptr. */ - [[nodiscard]] const ElementType* findElement (const ElementType& element) const; + [[nodiscard]] OptionalRef findElement (const ElementType& element) const; template void doForAllNodes (Callable&& callable); diff --git a/modules/common/chowdsp_data_structures/Structures/chowdsp_OptionalArray.h b/modules/common/chowdsp_data_structures/Structures/chowdsp_OptionalArray.h new file mode 100644 index 000000000..6cf91af24 --- /dev/null +++ b/modules/common/chowdsp_data_structures/Structures/chowdsp_OptionalArray.h @@ -0,0 +1,135 @@ +#pragma once + +namespace chowdsp +{ +/** + * A data structure similar to std::array, N>, but with less memory overhead. + * + * This implementation is still a work-in-progress. + */ +template +class OptionalArray +{ + std::array, N> objects {}; + std::bitset optional_flags {}; + +public: + using value_type = T; + static constexpr auto max_size = N; + + OptionalArray() = default; + OptionalArray (const OptionalArray&) = default; + OptionalArray& operator= (const OptionalArray&) = default; + OptionalArray (OptionalArray&&) noexcept = default; + OptionalArray& operator= (OptionalArray&&) noexcept = default; + + ~OptionalArray() + { + for (auto [idx, object] : enumerate (objects)) + { + if (optional_flags[idx]) + object.destruct(); + } + } + + [[nodiscard]] bool has_value (size_t idx) const noexcept + { + return optional_flags[idx]; + } + + [[nodiscard]] bool empty() const noexcept + { + return optional_flags.none(); + } + + [[nodiscard]] size_t count_values() const noexcept + { + return optional_flags.count(); + } + + OptionalRef operator[] (size_t idx) + { + if (! optional_flags[idx]) + return {}; + return objects[idx].item(); + } + + OptionalRef operator[] (size_t idx) const + { + if (! optional_flags[idx]) + return {}; + return objects[idx].item(); + } + + template + T& emplace (size_t idx, Args&&... args) + { + if (optional_flags[idx]) + objects[idx].destruct(); + optional_flags[idx] = true; + return *objects[idx].construct (std::forward (args)...); + } + + void erase (size_t idx) + { + if (optional_flags[idx]) + { + objects[idx].destruct(); + optional_flags[idx] = false; + } + } + + template + struct iterator + { + size_t index {}; + std::conditional_t array {}; + + bool operator!= (const iterator& other) const noexcept + { + return &array != &other.array || index != other.index; + } + void operator++() noexcept + { + do + { + ++index; + } while (index < N && ! array.optional_flags[index]); + } + auto& operator*() const noexcept + { + return array.objects[index].item(); + } + }; + + auto begin() + { + return iterator<> { 0, *this }; + } + + auto end() + { + return iterator<> { N, *this }; + } + + auto begin() const + { + return cbegin(); + } + + auto end() const + { + return cend(); + } + + auto cbegin() const + { + return iterator { 0, *this }; + } + + auto cend() const + { + return iterator { N, *this }; + } +}; +} // namespace chowdsp diff --git a/modules/common/chowdsp_data_structures/Structures/chowdsp_OptionalRef.h b/modules/common/chowdsp_data_structures/Structures/chowdsp_OptionalRef.h new file mode 100644 index 000000000..c9845df8d --- /dev/null +++ b/modules/common/chowdsp_data_structures/Structures/chowdsp_OptionalRef.h @@ -0,0 +1,141 @@ +#pragma once + +namespace chowdsp +{ +/** + * This class is basically an implementation of std::optional. + * Under the hood, it's basically just a pointer but with an + * optional-like interface. + * + * Make sure that an OptionalRef never holds on to a reference + * past the lifetime of the referenced object. + */ +template +class OptionalRef +{ + T* ptr = nullptr; + +public: + OptionalRef() = default; + OptionalRef (const OptionalRef&) = default; + OptionalRef& operator= (const OptionalRef&) = default; + OptionalRef (OptionalRef&&) noexcept = default; + OptionalRef& operator= (OptionalRef&&) noexcept = default; + + OptionalRef (std::nullopt_t) // NOLINT(google-explicit-constructor) + { + } + + OptionalRef (T& value) // NOLINT(google-explicit-constructor) + : ptr { &value } + { + } + + OptionalRef& operator= (std::nullopt_t) noexcept // NOLINT + { + ptr = nullptr; + return *this; + } + + OptionalRef& operator= (T& value) noexcept // NOLINT + { + ptr = &value; + return *this; + } + + T* operator->() noexcept { return ptr; } + const T* operator->() const noexcept { return ptr; } + T& operator*() noexcept { return *ptr; } + const T& operator*() const noexcept { return *ptr; } + + [[nodiscard]] explicit operator bool() const noexcept { return has_value(); } + [[nodiscard]] bool has_value() const noexcept { return ptr != nullptr; } + + T& value() + { + if (! has_value()) + throw std::bad_optional_access(); + return *ptr; + } + + const T& value() const + { + if (! has_value()) + throw std::bad_optional_access(); + return *ptr; + } + + void reset() noexcept { ptr = nullptr; } + void swap (OptionalRef& other) noexcept { std::swap (ptr, other.ptr); } + + const T& value_or (const T& other) const { return has_value() ? *ptr : other; } +}; + +template +bool operator== (const OptionalRef& p1, const OptionalRef& p2) +{ + if (p1.has_value() != p2.has_value()) + return false; + + if (! p1.has_value()) + return true; // both nullopt + + return *p1 == *p2; +} + +template +bool operator!= (const OptionalRef& p1, const OptionalRef& p2) +{ + return ! (p1 == p2); +} + +template +bool operator== (const OptionalRef& p1, std::nullopt_t) +{ + return ! p1.has_value(); +} + +template +bool operator!= (const OptionalRef& p1, std::nullopt_t) +{ + return p1.has_value(); +} + +template +bool operator== (std::nullopt_t, const OptionalRef& p1) +{ + return ! p1.has_value(); +} + +template +bool operator!= (std::nullopt_t, const OptionalRef& p1) +{ + return p1.has_value(); +} + +template +bool operator== (const OptionalRef& p1, const std::remove_cv_t& p2) +{ + if (! p1.has_value()) + return false; + return *p1 == p2; +} + +template +bool operator!= (const OptionalRef& p1, const std::remove_cv_t& p2) +{ + return ! (p1 == p2); +} + +template +bool operator== (const std::remove_cv_t& p2, const OptionalRef& p1) +{ + return p1 == p2; +} + +template +bool operator!= (const std::remove_cv_t& p2, const OptionalRef& p1) +{ + return p1 != p2; +} +} // namespace chowdsp diff --git a/modules/common/chowdsp_data_structures/chowdsp_data_structures.h b/modules/common/chowdsp_data_structures/chowdsp_data_structures.h index ed665821e..17bcfba03 100644 --- a/modules/common/chowdsp_data_structures/chowdsp_data_structures.h +++ b/modules/common/chowdsp_data_structures/chowdsp_data_structures.h @@ -20,6 +20,9 @@ BEGIN_JUCE_MODULE_DECLARATION #pragma once +#include +#include + #include #include "third_party/short_alloc.h" @@ -36,7 +39,9 @@ BEGIN_JUCE_MODULE_DECLARATION #include "Structures/chowdsp_OptionalPointer.h" #include "Structures/chowdsp_SmallVector.h" #include "Structures/chowdsp_StringLiteral.h" +#include "Structures/chowdsp_OptionalRef.h" #include "Structures/chowdsp_EnumMap.h" +#include "Structures/chowdsp_OptionalArray.h" #include "Allocators/chowdsp_ArenaAllocator.h" #include "Allocators/chowdsp_ChainedArenaAllocator.h" diff --git a/modules/plugin/chowdsp_presets_v2/Backend/chowdsp_PresetManager.cpp b/modules/plugin/chowdsp_presets_v2/Backend/chowdsp_PresetManager.cpp index 513abd851..8dfff0f02 100644 --- a/modules/plugin/chowdsp_presets_v2/Backend/chowdsp_PresetManager.cpp +++ b/modules/plugin/chowdsp_presets_v2/Backend/chowdsp_PresetManager.cpp @@ -65,7 +65,7 @@ void PresetManager::saveUserPreset (const juce::File& file, Preset&& preset) // preset.toFile (file); loadUserPresetsFromFolder (getUserPresetPath()); - if (const auto* justSavedPreset = presetTree.findElement (preset)) + if (const auto justSavedPreset = presetTree.findElement (preset)) loadPreset (*justSavedPreset); else jassertfalse; // preset was not saved correctly! @@ -76,9 +76,9 @@ void PresetManager::setDefaultPreset (Preset&& newDefaultPreset) // default preset must be a valid preset! jassert (newDefaultPreset.isValid()); - if (const auto* foundDefaultPreset = presetTree.findElement (newDefaultPreset)) + if (const auto foundDefaultPreset = presetTree.findElement (newDefaultPreset)) { - defaultPreset = foundDefaultPreset; + defaultPreset = &foundDefaultPreset.value(); return; } diff --git a/tests/common_tests/chowdsp_data_structures_test/AbstractTreeTest.cpp b/tests/common_tests/chowdsp_data_structures_test/AbstractTreeTest.cpp index d52487f0c..0a723f040 100644 --- a/tests/common_tests/chowdsp_data_structures_test/AbstractTreeTest.cpp +++ b/tests/common_tests/chowdsp_data_structures_test/AbstractTreeTest.cpp @@ -126,20 +126,20 @@ TEST_CASE ("Abstract Tree Test", "[common][data-structures]") REQUIRE (tree.getRootNode().first_child->tag == "m"); REQUIRE (tree.getRootNode().first_child->first_child->leaf == "mussels"); - auto* found = tree.findElement ("mussels"); - REQUIRE (found != nullptr); + auto found = tree.findElement ("mussels"); + REQUIRE (found); } SECTION ("Find Success") { - auto* found = std::as_const (tree).findElement ("apples"); - jassert (found != nullptr); + auto found = std::as_const (tree).findElement ("apples"); + jassert (found); } SECTION ("Find Fail") { - [[maybe_unused]] auto* found = std::as_const (tree).findElement ("bologna"); - jassert (found == nullptr); + [[maybe_unused]] auto found = std::as_const (tree).findElement ("bologna"); + jassert (! found); } SECTION ("To Uppercase") diff --git a/tests/common_tests/chowdsp_data_structures_test/CMakeLists.txt b/tests/common_tests/chowdsp_data_structures_test/CMakeLists.txt index 96e733620..1b0c8d721 100644 --- a/tests/common_tests/chowdsp_data_structures_test/CMakeLists.txt +++ b/tests/common_tests/chowdsp_data_structures_test/CMakeLists.txt @@ -19,6 +19,8 @@ target_sources(chowdsp_data_structures_test STLArenaAllocatorTest.cpp RawObjectTest.cpp EnumMapTest.cpp + OptionalRefTest.cpp + OptionalArrayTest.cpp ) target_compile_features(chowdsp_data_structures_test PRIVATE cxx_std_20) diff --git a/tests/common_tests/chowdsp_data_structures_test/OptionalArrayTest.cpp b/tests/common_tests/chowdsp_data_structures_test/OptionalArrayTest.cpp new file mode 100644 index 000000000..78956d628 --- /dev/null +++ b/tests/common_tests/chowdsp_data_structures_test/OptionalArrayTest.cpp @@ -0,0 +1,56 @@ +#include +#include + +TEST_CASE ("Optional Array Test", "[common][data-structures]") +{ + chowdsp::OptionalArray array {}; + REQUIRE (array.empty()); + REQUIRE (array.count_values() == 0); + REQUIRE (! array.has_value (0)); + + SECTION ("emplace()") + { + array.emplace (0, "test"); + array.emplace (1); + + REQUIRE (array[0].has_value()); + REQUIRE (array[0] == std::string { "test" }); + REQUIRE (array[1].has_value()); + REQUIRE (array[1] == std::string {}); + + array.emplace (1, "test2"); + REQUIRE (array[1] == std::string { "test2" }); + REQUIRE (std::as_const (array)[1] == std::string { "test2" }); + + REQUIRE (array[2] == std::nullopt); + REQUIRE (std::as_const (array)[2] == std::nullopt); + } + + SECTION ("erase()") + { + array.erase (0); + REQUIRE (! array.has_value (0)); + + array.emplace (0, "test"); + REQUIRE (array[0].has_value()); + + array.erase (0); + REQUIRE (! array.has_value (0)); + } + + SECTION ("iterate") + { + for (size_t i = 0; i < array.max_size; ++i) + array.emplace (i, std::to_string (i)); + + for (auto [idx, str] : chowdsp::enumerate (array)) + { + str = "#" + str; + } + + for (const auto& [idx, str] : chowdsp::enumerate (std::as_const (array))) + { + REQUIRE (str == std::string { "#" } + std::to_string (idx)); + } + } +} diff --git a/tests/common_tests/chowdsp_data_structures_test/OptionalRefTest.cpp b/tests/common_tests/chowdsp_data_structures_test/OptionalRefTest.cpp new file mode 100644 index 000000000..a5a2f52e2 --- /dev/null +++ b/tests/common_tests/chowdsp_data_structures_test/OptionalRefTest.cpp @@ -0,0 +1,94 @@ +#include +#include + +TEST_CASE ("OptionalRef Test", "[common][data-structures]") +{ + int x = 42; + chowdsp::OptionalRef x_opt { x }; + + SECTION ("has_value()") + { + REQUIRE (x_opt.has_value()); + REQUIRE (x_opt.has_value()); + } + + SECTION ("operator->()") + { + REQUIRE (x_opt.operator->() == &x); + REQUIRE (std::as_const (x_opt).operator->() == &x); + } + + SECTION ("value()") + { + REQUIRE (std::as_const (x_opt).value() == 42); + x_opt.value() = 43; + REQUIRE (std::as_const (x_opt).value() == 43); + *x_opt = 44; + REQUIRE (*std::as_const (x_opt) == 44); + } + + SECTION ("value() throws") + { + chowdsp::OptionalRef opt {}; + REQUIRE_THROWS (opt.value() == 0); + REQUIRE_THROWS (std::as_const (opt).value() == 0); + } + + SECTION ("swap()") + { + chowdsp::OptionalRef x_other {}; + x_opt.swap (x_other); + REQUIRE (! x_opt.has_value()); + REQUIRE (x_other.has_value()); + x_opt.swap (x_other); + REQUIRE (x_opt.has_value()); + REQUIRE (! x_other.has_value()); + } + + SECTION ("reset()") + { + x_opt.reset(); + REQUIRE (! x_opt); + } + + SECTION ("assignment") + { + int y = 100; + x_opt = y; + REQUIRE (*x_opt == 100); + } + + SECTION ("nullopt") + { + x_opt = std::nullopt; + REQUIRE (! x_opt); + chowdsp::OptionalRef y_opt { std::nullopt }; + REQUIRE (! y_opt); + } + + SECTION ("value_or()") + { + REQUIRE (x_opt.value_or (-10) == 42); + x_opt = std::nullopt; + REQUIRE (x_opt.value_or (-10) == -10); + } + + SECTION ("equality") + { + int x_copy = 42; + int y = -1000; + REQUIRE (chowdsp::OptionalRef {} == chowdsp::OptionalRef {}); + REQUIRE (x_opt == chowdsp::OptionalRef { x_copy }); + REQUIRE (x_opt != chowdsp::OptionalRef { y }); + REQUIRE (x_opt != chowdsp::OptionalRef {}); + REQUIRE (x_opt != std::nullopt); + REQUIRE (std::nullopt != x_opt); + REQUIRE (std::nullopt == chowdsp::OptionalRef {}); + REQUIRE (chowdsp::OptionalRef {} == std::nullopt); + REQUIRE (100 != chowdsp::OptionalRef {}); + REQUIRE (x_opt == 42); + REQUIRE (x_opt != 100); + REQUIRE (42 == x_opt); + REQUIRE (100 != x_opt); + } +} diff --git a/tests/plugin_tests/chowdsp_presets_v2_test/PresetTreeTest.cpp b/tests/plugin_tests/chowdsp_presets_v2_test/PresetTreeTest.cpp index 109af9ed6..2c8327cdc 100644 --- a/tests/plugin_tests/chowdsp_presets_v2_test/PresetTreeTest.cpp +++ b/tests/plugin_tests/chowdsp_presets_v2_test/PresetTreeTest.cpp @@ -229,10 +229,10 @@ TEST_CASE ("Preset Tree Test", "[plugin][presets]") chowdsp::presets::PresetTree preset_tree; const auto& preset = preset_tree.insertElement (chowdsp::presets::Preset { "Blah", "Jatin", { { "tag", 1.0f } }, "Cat1" }); - const auto* foundPreset = preset_tree.findElement (preset); + const auto foundPreset = preset_tree.findElement (preset); REQUIRE (*foundPreset == preset); - REQUIRE (preset_tree.findElement (chowdsp::presets::Preset { "Blah", "Jatin", { { "tag", 100.0f } } }) == nullptr); + REQUIRE (! preset_tree.findElement (chowdsp::presets::Preset { "Blah", "Jatin", { { "tag", 100.0f } } })); } SECTION ("With Preset State")