diff --git a/Code/Framework/AzCore/AzCore/DOM/DomPath.cpp b/Code/Framework/AzCore/AzCore/DOM/DomPath.cpp index 5f6438518f..9be99d5a03 100644 --- a/Code/Framework/AzCore/AzCore/DOM/DomPath.cpp +++ b/Code/Framework/AzCore/AzCore/DOM/DomPath.cpp @@ -7,9 +7,9 @@ */ #include +#include #include #include -#include namespace AZ::Dom { @@ -117,6 +117,36 @@ namespace AZ::Dom return AZStd::get(m_value); } + size_t PathEntry::GetHash() const + { + return AZStd::visit( + [&](auto&& value) -> size_t + { + using CurrentType = AZStd::decay_t; + if constexpr (AZStd::is_same_v) + { + AZStd::hash hasher; + return hasher(value); + } + else if constexpr (AZStd::is_same_v) + { + return value.GetHash(); + } + }, + m_value); + } +} // namespace AZ::Dom + +namespace AZStd +{ + size_t AZStd::hash::operator()(const AZ::Dom::PathEntry& entry) const + { + return entry.GetHash(); + } +} // namespace AZStd + +namespace AZ::Dom +{ const AZ::Name& PathEntry::GetKey() const { AZ_Assert(IsKey(), "Key called on PathEntry that is not a key"); diff --git a/Code/Framework/AzCore/AzCore/DOM/DomPath.h b/Code/Framework/AzCore/AzCore/DOM/DomPath.h index 7a031a2c68..416f3b6903 100644 --- a/Code/Framework/AzCore/AzCore/DOM/DomPath.h +++ b/Code/Framework/AzCore/AzCore/DOM/DomPath.h @@ -55,11 +55,24 @@ namespace AZ::Dom size_t GetIndex() const; const AZ::Name& GetKey() const; + size_t GetHash() const; private: AZStd::variant m_value; }; +} // namespace AZ::Dom +namespace AZStd +{ + template<> + struct hash + { + size_t operator()(const AZ::Dom::PathEntry& entry) const; + }; +} // namespace AZStd + +namespace AZ::Dom +{ //! Represents a path, represented as a series of PathEntry values, to a position in a Value. class Path final { @@ -135,7 +148,7 @@ namespace AZ::Dom AZStd::string ToString() const; void AppendToString(AZStd::string& output) const; - template + template void AppendToString(T& output) const { const size_t startIndex = output.length(); diff --git a/Code/Framework/AzCore/AzCore/DOM/DomPrefixTree.h b/Code/Framework/AzCore/AzCore/DOM/DomPrefixTree.h new file mode 100644 index 0000000000..a2d96b5bab --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/DomPrefixTree.h @@ -0,0 +1,100 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace AZ::Dom +{ + //! Specifies how a path matches against a DomPrefixTree + enum class PrefixTreeMatch + { + //! Only an exact path will match. + //! For the path "/foo/bar" only "/foo/bar" will match while + //! "/foo" and "/foo/bar/baz" will not. + ExactPath, + //! The path, and any of its subpaths, will match. + //! For the path "/foo/bar" both "/foo/bar" and any subpaths like + //! "/foo/bar/0" will match, while "/foo" and orthogonal paths like + //! "/bar" will not + PathAndSubpaths, + //! Any of the path's subpaths will match, excepting the path itself. + //! For the path "/foo/bar", "/foo/bar" will not match but "/foo/bar/0" + //! will. + SubpathsOnly, + }; + + template + constexpr bool RangeConvertibleToPrefixTree = false; + + template + constexpr bool RangeConvertibleToPrefixTree< + Range, + T, + AZStd::enable_if_t< + AZStd::ranges::input_range && AZStd::tuple_size>::value == 2 && + AZStd::convertible_to>, Path> && + AZStd::convertible_to>, T>>> = true; + + //! A prefix tree that maps DOM paths to some arbitrary value. + template + class DomPrefixTree + { + public: + DomPrefixTree() = default; + DomPrefixTree(const DomPrefixTree&) = default; + DomPrefixTree(DomPrefixTree&&) = default; + explicit DomPrefixTree(AZStd::initializer_list> init); + + template>> + explicit DomPrefixTree(Range&& range); + + DomPrefixTree& operator=(const DomPrefixTree&) = default; + DomPrefixTree& operator=(DomPrefixTree&&) = default; + + using VisitorFunction = AZStd::function; + + //! Visits a path and calls a visitor for each matching path and value. + void VisitPath(const Path& path, PrefixTreeMatch match, const VisitorFunction& visitor) const; + //! Visits a path and returns the most specific matching value, or null if none was found. + T* ValueAtPath(const Path& path, PrefixTreeMatch match); + //! \see ValueAtPath + const T* ValueAtPath(const Path& path, PrefixTreeMatch match) const; + //! Visits a path and returns the most specific matching value or some default value. + template + T ValueAtPathOrDefault(const Path& path, Deduced&& defaultValue, PrefixTreeMatch match) const; + + //! Sets the value stored at path. + template + void SetValue(const Path& path, Deduced&& value); + //! Removes the value stored at path. If removeChildren is true, also removes any values stored at subpaths. + void EraseValue(const Path& path, bool removedChildren = false); + //! Removes all entries from this tree. + void Clear(); + + private: + struct Node + { + AZStd::unordered_map m_values; + AZStd::optional m_data; + }; + + Node* GetNodeForPath(const Path& path); + const Node* GetNodeForPath(const Path& path) const; + + Node m_rootNode; + }; +} // namespace AZ::Dom + +#include diff --git a/Code/Framework/AzCore/AzCore/DOM/DomPrefixTree.inl b/Code/Framework/AzCore/AzCore/DOM/DomPrefixTree.inl new file mode 100644 index 0000000000..8b37d6bb72 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/DomPrefixTree.inl @@ -0,0 +1,234 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +namespace AZ::Dom +{ + template + DomPrefixTree::DomPrefixTree(AZStd::initializer_list> init) + { + for (const auto& [path, value] : init) + { + SetValue(path, value); + } + } + + template + template + DomPrefixTree::DomPrefixTree(Range&& range) + { + for (auto&& [path, value] : AZStd::forward(range)) + { + SetValue(path, value); + } + } + + template + auto DomPrefixTree::GetNodeForPath(const Path& path) -> Node* + { + Node* node = &m_rootNode; + for (const auto& entry : path) + { + auto entryIt = node->m_values.find(entry); + if (entryIt == node->m_values.end()) + { + return nullptr; + } + node = &entryIt->second; + } + return node; + } + + template + auto DomPrefixTree::GetNodeForPath(const Path& path) const -> const Node* + { + const Node* node = &m_rootNode; + for (const auto& entry : path) + { + auto entryIt = node->m_values.find(entry); + if (entryIt == node->m_values.end()) + { + return nullptr; + } + node = &entryIt->second; + } + return node; + } + + template + void DomPrefixTree::VisitPath(const Path& path, PrefixTreeMatch match, const VisitorFunction& visitor) const + { + const Node* rootNode = GetNodeForPath(path); + if (rootNode == nullptr) + { + return; + } + + if ((match == PrefixTreeMatch::ExactPath || match == PrefixTreeMatch::PathAndSubpaths) && rootNode->m_data.has_value()) + { + visitor(path, rootNode->m_data.value()); + } + + if (match == PrefixTreeMatch::ExactPath) + { + return; + } + + Path currentPath = path; + struct PopPathEntry + { + }; + using StackEntry = AZStd::variant; + AZStd::stack stack({ rootNode }); + while (!stack.empty()) + { + StackEntry entry = AZStd::move(stack.top()); + stack.pop(); + AZStd::visit( + [&](auto&& value) + { + using CurrentType = AZStd::decay_t; + if constexpr (AZStd::is_same_v) + { + if (value != rootNode && value->m_data.has_value()) + { + visitor(currentPath, value->m_data.value()); + } + + for (const auto& entry : value->m_values) + { + // The stack runs this in reverse order, so we'll: + // 1) Push the current path entry to currentPath + // 2a) Process the value at the path (if any) + // 2b) Process the value's descendants at the path (if any) + // 3) Pop the path entry from the stack + stack.push(PopPathEntry{}); + stack.push(&entry.second); + stack.push(entry.first); + } + } + else if constexpr (AZStd::is_same_v) + { + currentPath.Push(value); + } + else if constexpr (AZStd::is_same_v) + { + currentPath.Pop(); + } + }, + entry); + } + } + + template + T* DomPrefixTree::ValueAtPath(const Path& path, PrefixTreeMatch match) + { + // Just look up the node if we're looking for an exact path + if (match == PrefixTreeMatch::ExactPath) + { + if (Node* node = GetNodeForPath(path); node != nullptr && node->m_data.has_value()) + { + return &node->m_data.value(); + } + return {}; + } + + // Otherwise, walk to find the closest anscestor with a value + Node* node = &m_rootNode; + T* result = nullptr; + const size_t lengthToIterate = match == PrefixTreeMatch::SubpathsOnly ? path.Size() - 1 : path.Size(); + for (size_t i = 0; i < lengthToIterate; ++i) + { + if (node->m_data.has_value()) + { + result = &node->m_data.value(); + } + + const PathEntry& entry = path[i]; + auto entryIt = node->m_values.find(entry); + if (entryIt == node->m_values.end()) + { + break; + } + + node = &entryIt->second; + } + + if (node->m_data.has_value()) + { + result = &node->m_data.value(); + } + + return result; + } + + template + const T* DomPrefixTree::ValueAtPath(const Path& path, PrefixTreeMatch match) const + { + // Const coerce the ValueAtPath result, which doesn't mutate but returns a mutable pointer + return const_cast*>(this)->ValueAtPath(path, match); + } + + template + template + T DomPrefixTree::ValueAtPathOrDefault(const Path& path, Deduced&& defaultValue, PrefixTreeMatch match) const + { + const T* value = ValueAtPath(path, match); + return value == nullptr ? AZStd::forward(defaultValue) : *value; + } + + template + template + void DomPrefixTree::SetValue(const Path& path, Deduced&& value) + { + Node* node = &m_rootNode; + for (const PathEntry& entry : path) + { + // Get or create an entry in this node + node = &node->m_values[entry]; + } + node->m_data = AZStd::forward(value); + } + + template + void DomPrefixTree::EraseValue(const Path& path, bool removeChildren) + { + Node* node = &m_rootNode; + const size_t entriesToIterate = path.Size() - 1; + for (size_t i = 0; i < entriesToIterate; ++i) + { + const PathEntry& entry = path[i]; + auto nodeIt = node->m_values.find(entry); + if (nodeIt == node->m_values.end()) + { + return; + } + node = &nodeIt->second; + } + + auto nodeIt = node->m_values.find(path[path.Size() - 1]); + if (nodeIt != node->m_values.end()) + { + if (removeChildren) + { + node->m_values.erase(nodeIt); + } + else + { + nodeIt->second.m_data = {}; + } + } + } + + template + void DomPrefixTree::Clear() + { + m_rootNode = Node(); + } +} // namespace AZ::Dom diff --git a/Code/Framework/AzCore/AzCore/DOM/DomValue.cpp b/Code/Framework/AzCore/AzCore/DOM/DomValue.cpp index 793fadd092..2e266e58a6 100644 --- a/Code/Framework/AzCore/AzCore/DOM/DomValue.cpp +++ b/Code/Framework/AzCore/AzCore/DOM/DomValue.cpp @@ -151,6 +151,18 @@ namespace AZ::Dom return Value(value); } + Value Value::CreateNode(AZ::Name nodeName) + { + Value result(Type::Node); + result.SetNodeName(AZStd::move(nodeName)); + return result; + } + + Value Value::CreateNode(AZStd::string_view nodeName) + { + return CreateNode(AZ::Name(nodeName)); + } + Value::Value(int8_t value) : m_value(aznumeric_cast(value)) { diff --git a/Code/Framework/AzCore/AzCore/DOM/DomValue.h b/Code/Framework/AzCore/AzCore/DOM/DomValue.h index 4620692b5a..f713ed5f71 100644 --- a/Code/Framework/AzCore/AzCore/DOM/DomValue.h +++ b/Code/Framework/AzCore/AzCore/DOM/DomValue.h @@ -222,6 +222,8 @@ namespace AZ::Dom explicit Value(T*) = delete; static Value FromOpaqueValue(const AZStd::any& value); + static Value CreateNode(AZ::Name nodeName); + static Value CreateNode(AZStd::string_view nodeName); // Equality / comparison / swap... Value& operator=(const Value&); diff --git a/Code/Framework/AzCore/AzCore/azcore_files.cmake b/Code/Framework/AzCore/AzCore/azcore_files.cmake index dc2481b4c5..89923b415f 100644 --- a/Code/Framework/AzCore/AzCore/azcore_files.cmake +++ b/Code/Framework/AzCore/AzCore/azcore_files.cmake @@ -130,6 +130,8 @@ set(FILES DOM/DomVisitor.h DOM/DomComparison.cpp DOM/DomComparison.h + DOM/DomPrefixTree.h + DOM/DomPrefixTree.inl DOM/Backends/JSON/JsonBackend.h DOM/Backends/JSON/JsonSerializationUtils.cpp DOM/Backends/JSON/JsonSerializationUtils.h diff --git a/Code/Framework/AzCore/Tests/DOM/DomPrefixTreeBenchmarks.cpp b/Code/Framework/AzCore/Tests/DOM/DomPrefixTreeBenchmarks.cpp new file mode 100644 index 0000000000..db55109835 --- /dev/null +++ b/Code/Framework/AzCore/Tests/DOM/DomPrefixTreeBenchmarks.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include + +#define REGISTER_TREE_BENCHMARK(BaseClass, Method) \ + BENCHMARK_REGISTER_F(BaseClass, Method)->Args({10, 1})->Args({1000, 1})->Args({10, 5})->Args({1000, 5}) + +namespace AZ::Dom::Benchmark +{ + class DomPrefixTreeBenchmark : public Tests::DomBenchmarkFixture + { + public: + void SetUpHarness() override + { + Tests::DomBenchmarkFixture::SetUpHarness(); + m_registeredPaths = AZStd::make_unique>(); + } + + void TearDownHarness() override + { + m_registeredPaths.reset(); + m_tree.Clear(); + Tests::DomBenchmarkFixture::TearDownHarness(); + } + + void SetupTree(benchmark::State& state) + { + const size_t numPaths = aznumeric_cast(state.range(0)); + const size_t depth = aznumeric_cast(state.range(1)); + + Path path("/root"); + for (size_t i = 0; i < numPaths; ++i) + { + for (size_t c = 0; c < depth; ++c) + { + path.Push(i % 4); + m_tree.SetValue(path, AZStd::string::format("entry%zu", i)); + m_registeredPaths->push_back(path); + } + + for (size_t c = 0; c < depth; ++c) + { + path.Pop(); + } + } + } + + DomPrefixTree m_tree; + AZStd::unique_ptr> m_registeredPaths; + }; + + BENCHMARK_DEFINE_F(DomPrefixTreeBenchmark, FindValue_ExactPath)(benchmark::State& state) + { + SetupTree(state); + for (auto _ : state) + { + for (const auto& pathToCheck : *m_registeredPaths) + { + benchmark::DoNotOptimize(m_tree.ValueAtPath(pathToCheck, PrefixTreeMatch::ExactPath)); + } + } + state.SetItemsProcessed(m_registeredPaths->size() * state.iterations()); + } + REGISTER_TREE_BENCHMARK(DomPrefixTreeBenchmark, FindValue_ExactPath); + + BENCHMARK_DEFINE_F(DomPrefixTreeBenchmark, FindValue_InexactPath)(benchmark::State& state) + { + SetupTree(state); + for (auto _ : state) + { + for (const auto& pathToCheck : *m_registeredPaths) + { + benchmark::DoNotOptimize(m_tree.ValueAtPath(pathToCheck, PrefixTreeMatch::PathAndSubpaths)); + } + } + state.SetItemsProcessed(m_registeredPaths->size() * state.iterations()); + } + REGISTER_TREE_BENCHMARK(DomPrefixTreeBenchmark, FindValue_InexactPath); + + BENCHMARK_DEFINE_F(DomPrefixTreeBenchmark, FindValue_VisitEntries)(benchmark::State& state) + { + SetupTree(state); + + for (auto _ : state) + { + m_tree.VisitPath(Path(), PrefixTreeMatch::PathAndSubpaths, [](const Path& path, const AZStd::string& value) + { + benchmark::DoNotOptimize(path); + benchmark::DoNotOptimize(value); + }); + } + state.SetItemsProcessed(m_registeredPaths->size() * state.iterations()); + } + REGISTER_TREE_BENCHMARK(DomPrefixTreeBenchmark, FindValue_VisitEntries); +} + +#undef REGISTER_TREE_BENCHMARK diff --git a/Code/Framework/AzCore/Tests/DOM/DomPrefixTreeTests.cpp b/Code/Framework/AzCore/Tests/DOM/DomPrefixTreeTests.cpp new file mode 100644 index 0000000000..be9c7db0c2 --- /dev/null +++ b/Code/Framework/AzCore/Tests/DOM/DomPrefixTreeTests.cpp @@ -0,0 +1,204 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include + +namespace AZ::Dom::Tests +{ + using DomPrefixTreeTests = DomTestFixture; + + static_assert(!RangeConvertibleToPrefixTree, int>, "Non-pair range should not convert to tree"); + static_assert( + !RangeConvertibleToPrefixTree>, int>, + "Mismatched value type should not convert to tree"); + static_assert( + !RangeConvertibleToPrefixTree>, int>, + "Mismatched value type should not convert to tree"); + static_assert( + RangeConvertibleToPrefixTree>, int>, + "Vector with path / key type pairs should convert to tree"); + + TEST_F(DomPrefixTreeTests, InitializeFromInitializerList) + { + DomPrefixTree tree({ + { Path(), 0 }, + { Path("/foo/bar"), 1 }, + }); + + EXPECT_EQ(0, *tree.ValueAtPath(Path(), PrefixTreeMatch::ExactPath)); + EXPECT_EQ(1, *tree.ValueAtPath(Path("/foo/bar"), PrefixTreeMatch::ExactPath)); + } + + TEST_F(DomPrefixTreeTests, InitializeFromRange) + { + AZStd::vector> container({ + { Path(), 21 }, + { Path("/foo/bar"), 42 }, + }); + DomPrefixTree tree(container); + + EXPECT_EQ(21, *tree.ValueAtPath(Path(), PrefixTreeMatch::ExactPath)); + EXPECT_EQ(42, *tree.ValueAtPath(Path("/foo/bar"), PrefixTreeMatch::ExactPath)); + } + + TEST_F(DomPrefixTreeTests, GetAndSetRoot) + { + DomPrefixTree tree; + tree.SetValue(Path(), "root"); + EXPECT_EQ("root", *tree.ValueAtPath(Path(), PrefixTreeMatch::ExactPath)); + } + + TEST_F(DomPrefixTreeTests, GetExactPath) + { + DomPrefixTree tree; + + tree.SetValue(Path("/foo/0"), 0); + tree.SetValue(Path("/foo/1"), 42); + tree.SetValue(Path("/foo/foo"), 1); + tree.SetValue(Path("/foo/bar"), 2); + + EXPECT_EQ(0, *tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::ExactPath)); + EXPECT_EQ(42, *tree.ValueAtPath(Path("/foo/1"), PrefixTreeMatch::ExactPath)); + EXPECT_EQ(1, *tree.ValueAtPath(Path("/foo/foo"), PrefixTreeMatch::ExactPath)); + EXPECT_EQ(2, *tree.ValueAtPath(Path("/foo/bar"), PrefixTreeMatch::ExactPath)); + + EXPECT_EQ(nullptr, tree.ValueAtPath(Path(), PrefixTreeMatch::ExactPath)); + EXPECT_EQ(nullptr, tree.ValueAtPath(Path("/foo"), PrefixTreeMatch::ExactPath)); + EXPECT_EQ(nullptr, tree.ValueAtPath(Path("/foo/0/subpath"), PrefixTreeMatch::ExactPath)); + } + + TEST_F(DomPrefixTreeTests, GetSubpath) + { + DomPrefixTree tree; + + tree.SetValue(Path("/foo/0"), 0); + tree.SetValue(Path("/foo/1"), 42); + + EXPECT_EQ(0, *tree.ValueAtPath(Path("/foo/0/bar"), PrefixTreeMatch::SubpathsOnly)); + EXPECT_EQ(0, *tree.ValueAtPath(Path("/foo/0/bar/baz"), PrefixTreeMatch::SubpathsOnly)); + EXPECT_EQ(42, *tree.ValueAtPath(Path("/foo/1/0"), PrefixTreeMatch::SubpathsOnly)); + + EXPECT_EQ(nullptr, tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::SubpathsOnly)); + EXPECT_EQ(nullptr, tree.ValueAtPath(Path("/foo/1"), PrefixTreeMatch::SubpathsOnly)); + } + + TEST_F(DomPrefixTreeTests, GetPathOrSubpath) + { + DomPrefixTree tree; + + tree.SetValue(Path("/foo/0"), 0); + tree.SetValue(Path("/foo/1"), 42); + + EXPECT_EQ(0, *tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::PathAndSubpaths)); + EXPECT_EQ(0, *tree.ValueAtPath(Path("/foo/0/bar"), PrefixTreeMatch::PathAndSubpaths)); + EXPECT_EQ(0, *tree.ValueAtPath(Path("/foo/0/bar/baz"), PrefixTreeMatch::PathAndSubpaths)); + EXPECT_EQ(42, *tree.ValueAtPath(Path("/foo/1"), PrefixTreeMatch::PathAndSubpaths)); + EXPECT_EQ(42, *tree.ValueAtPath(Path("/foo/1/0"), PrefixTreeMatch::PathAndSubpaths)); + + EXPECT_EQ(nullptr, tree.ValueAtPath(Path(), PrefixTreeMatch::PathAndSubpaths)); + EXPECT_EQ(nullptr, tree.ValueAtPath(Path("/foo"), PrefixTreeMatch::PathAndSubpaths)); + EXPECT_EQ(nullptr, tree.ValueAtPath(Path("/path/0"), PrefixTreeMatch::PathAndSubpaths)); + } + + TEST_F(DomPrefixTreeTests, RemovePath) + { + DomPrefixTree tree; + + tree.SetValue(Path(), 20); + tree.SetValue(Path("/foo"), 40); + tree.SetValue(Path("/foo/0"), 80); + + tree.EraseValue(Path("/foo")); + + EXPECT_EQ(20, *tree.ValueAtPath(Path("/foo"), PrefixTreeMatch::PathAndSubpaths)); + EXPECT_EQ(80, *tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::PathAndSubpaths)); + } + + TEST_F(DomPrefixTreeTests, RemovePathAndChildren) + { + DomPrefixTree tree; + + tree.SetValue(Path(), 20); + tree.SetValue(Path("/foo"), 40); + tree.SetValue(Path("/foo/0"), 80); + + tree.EraseValue(Path("/foo"), true); + + EXPECT_EQ(20, *tree.ValueAtPath(Path("/foo"), PrefixTreeMatch::PathAndSubpaths)); + EXPECT_EQ(20, *tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::PathAndSubpaths)); + } + + TEST_F(DomPrefixTreeTests, ClearTree) + { + DomPrefixTree tree; + + tree.SetValue(Path(), 20); + tree.SetValue(Path("/foo"), 40); + + tree.Clear(); + + EXPECT_EQ(-10, tree.ValueAtPathOrDefault(Path("/foo"), -10, PrefixTreeMatch::PathAndSubpaths)); + } + + TEST_F(DomPrefixTreeTests, Visit) + { + DomPrefixTree tree; + + AZStd::vector> results; + auto visitorFn = [&results](const Path& path, int n) + { + results.emplace_back(path, n); + }; + + auto validateResult = [&results](const Path& path, int n) + { + for (const auto& pair : results) + { + if (pair.first == path) + { + return pair.second == n; + } + } + return false; + }; + + tree.SetValue(Path("/foo"), 99); + tree.SetValue(Path("/foo/0"), 0); + tree.SetValue(Path("/foo/1"), 42); + tree.SetValue(Path("/bar/bat"), 1); + tree.SetValue(Path("/bar/baz"), 2); + + tree.VisitPath(Path("/bar"), PrefixTreeMatch::ExactPath, visitorFn); + EXPECT_EQ(0, results.size()); + results.clear(); + + tree.VisitPath(Path("/foo/0"), PrefixTreeMatch::ExactPath, visitorFn); + EXPECT_EQ(1, results.size()); + EXPECT_TRUE(validateResult(Path("/foo/0"), 0)); + results.clear(); + + tree.VisitPath(Path("/foo/1"), PrefixTreeMatch::ExactPath, visitorFn); + EXPECT_EQ(1, results.size()); + EXPECT_TRUE(validateResult(Path("/foo/1"), 42)); + results.clear(); + + tree.VisitPath(Path("/foo"), PrefixTreeMatch::SubpathsOnly, visitorFn); + EXPECT_EQ(2, results.size()); + EXPECT_TRUE(validateResult(Path("/foo/0"), 0)); + EXPECT_TRUE(validateResult(Path("/foo/1"), 42)); + results.clear(); + + tree.VisitPath(Path("/foo"), PrefixTreeMatch::PathAndSubpaths, visitorFn); + EXPECT_EQ(3, results.size()); + EXPECT_TRUE(validateResult(Path("/foo"), 99)); + EXPECT_TRUE(validateResult(Path("/foo/0"), 0)); + EXPECT_TRUE(validateResult(Path("/foo/1"), 42)); + results.clear(); + } +} // namespace AZ::Dom::Tests diff --git a/Code/Framework/AzCore/Tests/azcoretests_files.cmake b/Code/Framework/AzCore/Tests/azcoretests_files.cmake index 99e97ca3cf..26181db90b 100644 --- a/Code/Framework/AzCore/Tests/azcoretests_files.cmake +++ b/Code/Framework/AzCore/Tests/azcoretests_files.cmake @@ -231,6 +231,8 @@ set(FILES DOM/DomPatchBenchmarks.cpp DOM/DomValueTests.cpp DOM/DomValueBenchmarks.cpp + DOM/DomPrefixTreeTests.cpp + DOM/DomPrefixTreeBenchmarks.cpp ) # Prevent the following files from being grouped in UNITY builds