diff --git a/src/Time/BoundaryHistory.hpp b/src/Time/BoundaryHistory.hpp index f52fe9afdb94a..b4259293afc8e 100644 --- a/src/Time/BoundaryHistory.hpp +++ b/src/Time/BoundaryHistory.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "DataStructures/CircularDeque.hpp" @@ -151,6 +152,17 @@ class BoundaryHistory { } /// @} + /// Apply \p func to each entry. + /// + /// The function \p func must accept two arguments, one of type + /// `const TimeStepId&` and a second of either type `const Data&` + /// or `gsl::not_null`. (Note that `Data` may be a + /// const-qualified type.) If entries are modified, the coupling + /// cache must be cleared by calling `clear_coupling_cache()` on + /// the parent `BoundaryHistory` object. + template + void for_each(Func&& func) const; + protected: ~SideAccessCommon() = default; @@ -350,6 +362,23 @@ class BoundaryHistory { couplings_; }; +template +template +template +void BoundaryHistory::SideAccessCommon< + Local, Mutable>::for_each(Func&& func) const { + for (auto& step : parent_data()) { + for (auto& substep : step.substeps) { + if constexpr (std::is_invocable_v) { + func(std::as_const(substep.id), std::as_const(substep.data)); + } else { + func(std::as_const(substep.id), make_not_null(&substep.data)); + } + } + } +} + template template void BoundaryHistory::MutableSideAccess< diff --git a/tests/Unit/Time/Test_BoundaryHistory.cpp b/tests/Unit/Time/Test_BoundaryHistory.cpp index 162204af64c72..14d06a8972b62 100644 --- a/tests/Unit/Time/Test_BoundaryHistory.cpp +++ b/tests/Unit/Time/Test_BoundaryHistory.cpp @@ -3,11 +3,14 @@ #include "Framework/TestingFramework.hpp" +#include +#include #include #include #include #include #include +#include #include #include @@ -582,6 +585,119 @@ void test_substeps() { CHECK(CacheCheck::count() == 1); CHECK(CacheCheck::entries.at({2.0, 1.0}) == 1); } + +template +struct ReferenceFunctor { + using ExpectedData = + tmpl::conditional_t>; + std::unordered_set ids{}; + std::unordered_set> entries{}; + + // Templated to test that the correct types are passed. + template + void operator()(Id& id, Data& data) { + static_assert(std::is_same_v); + static_assert(std::is_same_v); + + CHECK(ids.insert(id).second); + CHECK(entries.insert(data).second); + if constexpr (Local) { + CHECK(static_cast(std::islower(data[0])) == Modified); + } else { + CHECK((data[0] < 0) == Modified); + } + } +}; + +template +struct NotNullFunctor { + using ExpectedData = + tmpl::conditional_t>; + std::unordered_set ids{}; + std::unordered_set> entries{}; + + // Templated to test that the correct types are passed. + template + void operator()(Id& id, const gsl::not_null data) { + static_assert(std::is_same_v); + static_assert(std::is_same_v, ExpectedData>); + static_assert(std::is_const_v == Const); + + CHECK(ids.insert(id).second); + CHECK(entries.insert(*data).second); + if constexpr (Local) { + CHECK(static_cast(std::islower((*data)[0])) == Modified); + if constexpr (not Const) { + (*data)[0] = + Modified ? std::toupper((*data)[0]) : std::tolower((*data)[0]); + } + } else { + CHECK(((*data)[0] < 0) == Modified); + if constexpr (not Const) { + (*data)[0] *= -1; + } + } + } +}; + +template +void check_reference(const Times& times, const size_t expected_size) { + ReferenceFunctor func{}; + times.for_each(func); + CHECK(func.ids.size() == expected_size); +} + +template +void check_not_null(const Times& times, const size_t expected_size) { + NotNullFunctor func{}; + times.for_each(func); + CHECK(func.ids.size() == expected_size); +} + +void test_for_each() { + INFO("for_each"); + + BoundaryHistoryType history{}; + const BoundaryHistoryType& const_history = history; + + history.local().insert(make_time_id(0.), 1, "A"); + history.local().insert(make_time_id(1.), 1, "B"); + history.local().insert(make_time_id(2.), 1, "C"); + history.local().insert(make_time_id(2., 1), 1, "D"); + const size_t local_size = 4; + + history.remote().insert(make_time_id(0.), 1, std::vector{1}); + history.remote().insert(make_time_id(1.), 1, std::vector{2}); + history.remote().insert(make_time_id(2.), 1, std::vector{3}); + history.remote().insert(make_time_id(3.), 1, std::vector{4}); + history.remote().insert(make_time_id(3., 1), 1, std::vector{5}); + const size_t remote_size = 5; + + // The second template parameter indicates whether the data is + // expected to have been modified from its original state at that + // point. Modification happens with each call to + // `check_not_null<..., ..., false>`. Modifying twice gives the + // original data. + check_reference(const_history.local(), local_size); + check_reference(history.local(), local_size); + check_not_null(const_history.local(), local_size); + check_not_null(history.local(), local_size); + check_reference(const_history.local(), local_size); + check_reference(history.local(), local_size); + check_not_null(const_history.local(), local_size); + check_not_null(history.local(), local_size); + check_reference(const_history.local(), local_size); + + check_reference(const_history.remote(), remote_size); + check_reference(history.remote(), remote_size); + check_not_null(const_history.remote(), remote_size); + check_not_null(history.remote(), remote_size); + check_reference(const_history.remote(), remote_size); + check_reference(history.remote(), remote_size); + check_not_null(const_history.remote(), remote_size); + check_not_null(history.remote(), remote_size); + check_reference(const_history.remote(), remote_size); +} } // namespace SPECTRE_TEST_CASE("Unit.Time.BoundaryHistory", "[Unit][Time]") { @@ -589,4 +705,5 @@ SPECTRE_TEST_CASE("Unit.Time.BoundaryHistory", "[Unit][Time]") { test_boundary_history(); test_substeps(); test_substeps(); + test_for_each(); }