From 27c6388c3c1653c3c3820575a411a4bdcd391730 Mon Sep 17 00:00:00 2001 From: Nicholas Van Sickle Date: Thu, 10 Feb 2022 15:20:04 -0800 Subject: [PATCH] Add DomPrefixTree, a DOM path => value lookup structure This is Document Property Editor work I've pulled out as I needed it in a few places, and the API may be generally useful. Real-world performance measurements will need to be done, if we have path lookup based bottlenecks we can consider adopting a contiguous memory approach with a sorted vector. Signed-off-by: Nicholas Van Sickle --- Code/Framework/AzCore/AzCore/DOM/DomPath.cpp | 22 +- Code/Framework/AzCore/AzCore/DOM/DomPath.h | 4 + .../AzCore/AzCore/DOM/DomPrefixTree.h | 77 ++++++ .../AzCore/AzCore/DOM/DomPrefixTree.inl | 222 ++++++++++++++++++ Code/Framework/AzCore/AzCore/DOM/DomValue.cpp | 12 + Code/Framework/AzCore/AzCore/DOM/DomValue.h | 2 + .../AzCore/AzCore/azcore_files.cmake | 2 + .../Tests/DOM/DomPrefixTreeBenchmarks.cpp | 104 ++++++++ .../AzCore/Tests/DOM/DomPrefixTreeTests.cpp | 170 ++++++++++++++ .../AzCore/Tests/azcoretests_files.cmake | 2 + 10 files changed, 616 insertions(+), 1 deletion(-) create mode 100644 Code/Framework/AzCore/AzCore/DOM/DomPrefixTree.h create mode 100644 Code/Framework/AzCore/AzCore/DOM/DomPrefixTree.inl create mode 100644 Code/Framework/AzCore/Tests/DOM/DomPrefixTreeBenchmarks.cpp create mode 100644 Code/Framework/AzCore/Tests/DOM/DomPrefixTreeTests.cpp diff --git a/Code/Framework/AzCore/AzCore/DOM/DomPath.cpp b/Code/Framework/AzCore/AzCore/DOM/DomPath.cpp index 5f6438518f..62e6209dcb 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 { @@ -89,6 +89,26 @@ namespace AZ::Dom return !operator==(key); } + bool PathEntry::operator<(const PathEntry& rhs) const + { + return AZStd::visit( + [&](auto&& lhsValue) + { + using CurrentType = AZStd::decay_t; + if constexpr (AZStd::is_same_v) + { + const size_t* rhsValue = AZStd::get_if(&rhs.m_value); + return rhsValue == nullptr ? true : lhsValue < *rhsValue; + } + else if constexpr (AZStd::is_same_v) + { + const AZ::Name* rhsValue = AZStd::get_if(&rhs.m_value); + return rhsValue == nullptr ? false : lhsValue.GetStringView() < rhsValue->GetStringView(); + } + }, + m_value); + } + void PathEntry::SetEndOfArray() { m_value = EndOfArrayIndex; diff --git a/Code/Framework/AzCore/AzCore/DOM/DomPath.h b/Code/Framework/AzCore/AzCore/DOM/DomPath.h index 7a031a2c68..7317175490 100644 --- a/Code/Framework/AzCore/AzCore/DOM/DomPath.h +++ b/Code/Framework/AzCore/AzCore/DOM/DomPath.h @@ -47,6 +47,10 @@ namespace AZ::Dom bool operator!=(const AZ::Name& key) const; bool operator!=(AZStd::string_view key) const; + //! Comparison operator used for storage in map structures + //! Compares hash order and not lexigraphic order, so not suitable for UI purposes + bool operator<(const PathEntry& rhs) const; + void SetEndOfArray(); bool IsEndOfArray() const; diff --git a/Code/Framework/AzCore/AzCore/DOM/DomPrefixTree.h b/Code/Framework/AzCore/AzCore/DOM/DomPrefixTree.h new file mode 100644 index 0000000000..42a93041a4 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/DomPrefixTree.h @@ -0,0 +1,77 @@ +/* + * 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 + +namespace AZ::Dom +{ + //! Specifies how a patch 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, + }; + + //! 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); + + //! Visits a path and calls a visitor for each matching path and value. + void VisitPath(const Path& path, PrefixTreeMatch match, AZStd::function 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. + //! \note This returns a copy of a value. If T is expensive to copy, consider using ValueAtPath instead. + T ValueAtPathOrDefault(const Path& path, const T& defaultValue, PrefixTreeMatch match) const; + + //! Sets the value stored at path. + void SetValue(const Path& path, T 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::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..6c0e342ead --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/DomPrefixTree.inl @@ -0,0 +1,222 @@ +/* + * 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& entry : init) + { + SetValue(entry.first, entry.second); + } + } + + 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, AZStd::function 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 + T DomPrefixTree::ValueAtPathOrDefault(const Path& path, const T& defaultValue, PrefixTreeMatch match) const + { + const T* value = ValueAtPath(path, match); + return value == nullptr ? defaultValue : *value; + } + + template + void DomPrefixTree::SetValue(const Path& path, T 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 = 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 b7821f1d65..e55f6b5b69 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..715e06b603 --- /dev/null +++ b/Code/Framework/AzCore/Tests/DOM/DomPrefixTreeTests.cpp @@ -0,0 +1,170 @@ +/* + * 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; + + TEST_F(DomPrefixTreeTests, GetAndSetRoot) + { + DomPrefixTree tree; + tree.SetValue(Path(), "root"); + EXPECT_EQ(*tree.ValueAtPath(Path(), PrefixTreeMatch::ExactPath), "root"); + } + + 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(*tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::ExactPath), 0); + EXPECT_EQ(*tree.ValueAtPath(Path("/foo/1"), PrefixTreeMatch::ExactPath), 42); + EXPECT_EQ(*tree.ValueAtPath(Path("/foo/foo"), PrefixTreeMatch::ExactPath), 1); + EXPECT_EQ(*tree.ValueAtPath(Path("/foo/bar"), PrefixTreeMatch::ExactPath), 2); + + EXPECT_EQ(tree.ValueAtPath(Path(), PrefixTreeMatch::ExactPath), nullptr); + EXPECT_EQ(tree.ValueAtPath(Path("/foo"), PrefixTreeMatch::ExactPath), nullptr); + EXPECT_EQ(tree.ValueAtPath(Path("/foo/0/subpath"), PrefixTreeMatch::ExactPath), nullptr); + } + + TEST_F(DomPrefixTreeTests, GetSubpath) + { + DomPrefixTree tree; + + tree.SetValue(Path("/foo/0"), 0); + tree.SetValue(Path("/foo/1"), 42); + + EXPECT_EQ(*tree.ValueAtPath(Path("/foo/0/bar"), PrefixTreeMatch::SubpathsOnly), 0); + EXPECT_EQ(*tree.ValueAtPath(Path("/foo/0/bar/baz"), PrefixTreeMatch::SubpathsOnly), 0); + EXPECT_EQ(*tree.ValueAtPath(Path("/foo/1/0"), PrefixTreeMatch::SubpathsOnly), 42); + + EXPECT_EQ(tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::SubpathsOnly), nullptr); + EXPECT_EQ(tree.ValueAtPath(Path("/foo/1"), PrefixTreeMatch::SubpathsOnly), nullptr); + } + + TEST_F(DomPrefixTreeTests, GetPathOrSubpath) + { + DomPrefixTree tree; + + tree.SetValue(Path("/foo/0"), 0); + tree.SetValue(Path("/foo/1"), 42); + + EXPECT_EQ(*tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::PathAndSubpaths), 0); + EXPECT_EQ(*tree.ValueAtPath(Path("/foo/0/bar"), PrefixTreeMatch::PathAndSubpaths), 0); + EXPECT_EQ(*tree.ValueAtPath(Path("/foo/0/bar/baz"), PrefixTreeMatch::PathAndSubpaths), 0); + EXPECT_EQ(*tree.ValueAtPath(Path("/foo/1"), PrefixTreeMatch::PathAndSubpaths), 42); + EXPECT_EQ(*tree.ValueAtPath(Path("/foo/1/0"), PrefixTreeMatch::PathAndSubpaths), 42); + + EXPECT_EQ(tree.ValueAtPath(Path(), PrefixTreeMatch::PathAndSubpaths), nullptr); + EXPECT_EQ(tree.ValueAtPath(Path("/foo"), PrefixTreeMatch::PathAndSubpaths), nullptr); + EXPECT_EQ(tree.ValueAtPath(Path("/path/0"), PrefixTreeMatch::PathAndSubpaths), nullptr); + } + + 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(*tree.ValueAtPath(Path("/foo"), PrefixTreeMatch::PathAndSubpaths), 20); + EXPECT_EQ(*tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::PathAndSubpaths), 80); + } + + 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(*tree.ValueAtPath(Path("/foo"), PrefixTreeMatch::PathAndSubpaths), 20); + EXPECT_EQ(*tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::PathAndSubpaths), 20); + } + + TEST_F(DomPrefixTreeTests, ClearTree) + { + DomPrefixTree tree; + + tree.SetValue(Path(), 20); + tree.SetValue(Path("/foo"), 40); + + tree.Clear(); + + EXPECT_EQ(tree.ValueAtPathOrDefault(Path("/foo"), -10, PrefixTreeMatch::PathAndSubpaths), -10); + } + + 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(results.size(), 0); + results.clear(); + + tree.VisitPath(Path("/foo/0"), PrefixTreeMatch::ExactPath, visitorFn); + EXPECT_EQ(results.size(), 1); + EXPECT_TRUE(validateResult(Path("/foo/0"), 0)); + results.clear(); + + tree.VisitPath(Path("/foo/1"), PrefixTreeMatch::ExactPath, visitorFn); + EXPECT_EQ(results.size(), 1); + EXPECT_TRUE(validateResult(Path("/foo/1"), 42)); + results.clear(); + + tree.VisitPath(Path("/foo"), PrefixTreeMatch::SubpathsOnly, visitorFn); + EXPECT_EQ(results.size(), 2); + 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(results.size(), 3); + EXPECT_TRUE(validateResult(Path("/foo"), 99)); + EXPECT_TRUE(validateResult(Path("/foo/0"), 0)); + EXPECT_TRUE(validateResult(Path("/foo/1"), 42)); + results.clear(); + } +} diff --git a/Code/Framework/AzCore/Tests/azcoretests_files.cmake b/Code/Framework/AzCore/Tests/azcoretests_files.cmake index b07cd32ec1..9da50e81a1 100644 --- a/Code/Framework/AzCore/Tests/azcoretests_files.cmake +++ b/Code/Framework/AzCore/Tests/azcoretests_files.cmake @@ -228,6 +228,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