/* * 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 { class DomPrefixTreeTests : public DomTestFixture { public: void SetUp() override { DomTestFixture::SetUp(); m_runData = AZStd::make_unique(); m_runData->m_visitorFn = [this](const Path& path, int value) { auto pathString = path.ToString(); AZStd::string visitedPathString = path.ToString(); for (const auto& entry : m_runData->m_visitorResults) { AZStd::string entryPathString = entry.first.ToString(); if (m_runData->m_useDepthFirstTraversal) { EXPECT_FALSE(visitedPathString.starts_with(entryPathString)); } else { EXPECT_FALSE(entryPathString.starts_with(visitedPathString)); } } m_runData->m_visitorResults.emplace_back(path, value); return true; }; m_runData->m_visitorTree.SetValue(Path("/foo"), 99); m_runData->m_visitorTree.SetValue(Path("/foo/0"), 0); m_runData->m_visitorTree.SetValue(Path("/foo/1"), 42); m_runData->m_visitorTree.SetValue(Path("/bar/bat"), 1); m_runData->m_visitorTree.SetValue(Path("/bar/baz"), 2); } void TearDown() override { m_runData.reset(); DomTestFixture::TearDown(); } protected: struct RunData { DomPrefixTree m_visitorTree; DomPrefixTree::VisitorFunction m_visitorFn; AZStd::vector> m_visitorResults; bool m_useDepthFirstTraversal = false; }; AZStd::unique_ptr m_runData; size_t VisitorResultCount() { return m_runData->m_visitorResults.size(); } bool ValidateResult(const Path& path, int n) { for (const auto& pair : m_runData->m_visitorResults) { if (pair.first == path) { return pair.second == n; } } return false; } void SetVisitDepthFirst(bool visitDepthFirst) { m_runData->m_useDepthFirstTraversal = visitDepthFirst; } void TestVisitPath(const Path& path, PrefixTreeTraversalFlags flags) { if (m_runData->m_useDepthFirstTraversal) { flags = flags | PrefixTreeTraversalFlags::TraverseMostToLeastSpecific; } else { flags = flags | PrefixTreeTraversalFlags::TraverseLeastToMostSpecific; } m_runData->m_visitorTree.VisitPath(path, m_runData->m_visitorFn, flags); } }; 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, GetParent) { DomPrefixTree tree; tree.SetValue(Path("/foo/0"), 0); tree.SetValue(Path("/foo/1"), 42); EXPECT_EQ(0, *tree.ValueAtPath(Path("/foo/0/bar"), PrefixTreeMatch::ParentsOnly)); EXPECT_EQ(0, *tree.ValueAtPath(Path("/foo/0/bar/baz"), PrefixTreeMatch::ParentsOnly)); EXPECT_EQ(42, *tree.ValueAtPath(Path("/foo/1/0"), PrefixTreeMatch::ParentsOnly)); EXPECT_EQ(nullptr, tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::ParentsOnly)); EXPECT_EQ(nullptr, tree.ValueAtPath(Path("/foo/1"), PrefixTreeMatch::ParentsOnly)); } TEST_F(DomPrefixTreeTests, GetPathOrParent) { DomPrefixTree tree; tree.SetValue(Path("/foo/0"), 0); tree.SetValue(Path("/foo/1"), 42); EXPECT_EQ(0, *tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::PathAndParents)); EXPECT_EQ(0, *tree.ValueAtPath(Path("/foo/0/bar"), PrefixTreeMatch::PathAndParents)); EXPECT_EQ(0, *tree.ValueAtPath(Path("/foo/0/bar/baz"), PrefixTreeMatch::PathAndParents)); EXPECT_EQ(42, *tree.ValueAtPath(Path("/foo/1"), PrefixTreeMatch::PathAndParents)); EXPECT_EQ(42, *tree.ValueAtPath(Path("/foo/1/0"), PrefixTreeMatch::PathAndParents)); EXPECT_EQ(nullptr, tree.ValueAtPath(Path(), PrefixTreeMatch::PathAndParents)); EXPECT_EQ(nullptr, tree.ValueAtPath(Path("/foo"), PrefixTreeMatch::PathAndParents)); EXPECT_EQ(nullptr, tree.ValueAtPath(Path("/path/0"), PrefixTreeMatch::PathAndParents)); } 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::PathAndParents)); EXPECT_EQ(80, *tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::PathAndParents)); } 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::PathAndParents)); EXPECT_EQ(20, *tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::PathAndParents)); } TEST_F(DomPrefixTreeTests, DetachSubTreeAtExistingPath) { DomPrefixTree tree; tree.SetValue(Path("/foo"), 40); tree.SetValue(Path("/foo/0"), 80); DomPrefixTree detachedTree = tree.DetachSubTree(AZ::Dom::Path("/foo")); // Validate that the values aren't present in the old tree after detaching. EXPECT_EQ(nullptr, tree.ValueAtPath(Path("/foo"), PrefixTreeMatch::ExactPath)); EXPECT_EQ(nullptr, tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::ExactPath)); // Validate that the values are present in the detached tree. EXPECT_EQ(40, *detachedTree.ValueAtPath(Path(), PrefixTreeMatch::ExactPath)); EXPECT_EQ(80, *detachedTree.ValueAtPath(Path("/0"), PrefixTreeMatch::ExactPath)); } TEST_F(DomPrefixTreeTests, DetachSubTreeAtNonExistingPath) { DomPrefixTree tree; tree.SetValue(Path("/foo"), 40); tree.SetValue(Path("/foo/0"), 80); DomPrefixTree detachedTree = tree.DetachSubTree(AZ::Dom::Path("/bar")); // Validate that the returned tree is empty since a non-existing path is provided. EXPECT_TRUE(detachedTree.IsEmpty()); // Validate that the values in the original tree are still present. EXPECT_EQ(40, *tree.ValueAtPath(Path("/foo"), PrefixTreeMatch::ExactPath)); EXPECT_EQ(80, *tree.ValueAtPath(Path("/foo/0"), PrefixTreeMatch::ExactPath)); } TEST_F(DomPrefixTreeTests, OverwritePathAddsNewNodeAtExistingPath) { DomPrefixTree tree; tree.SetValue(Path("/foo"), 40); DomPrefixTree subtree; subtree.SetValue(Path(), 80); subtree.SetValue(Path("/1"), 120); bool isOverwritten = tree.OverwritePath(Path("/foo"), AZStd::move(subtree)); // Validate that the subtree is overwritten and new values are present at the paths in the original tree. EXPECT_TRUE(isOverwritten); EXPECT_EQ(80, *tree.ValueAtPath(Path("/foo"), PrefixTreeMatch::ExactPath)); EXPECT_EQ(120, *tree.ValueAtPath(Path("/foo/1"), PrefixTreeMatch::ExactPath)); } TEST_F(DomPrefixTreeTests, OverwritePathReplacesExistingNodeAndChildren) { DomPrefixTree tree; tree.SetValue(Path("/foo"), 40); tree.SetValue(Path("/foo/1"), 60); tree.SetValue(Path("/foo/2"), 70); DomPrefixTree subtree; subtree.SetValue(Path(), 80); subtree.SetValue(Path("/1"), 120); bool isOverwritten = tree.OverwritePath(Path("/foo"), AZStd::move(subtree)); // Validate that the subtree is overwritten and existing nodes are replaced in the original tree. EXPECT_TRUE(isOverwritten); EXPECT_EQ(80, *tree.ValueAtPath(Path("/foo"), PrefixTreeMatch::ExactPath)); EXPECT_EQ(120, *tree.ValueAtPath(Path("/foo/1"), PrefixTreeMatch::ExactPath)); EXPECT_EQ(nullptr, tree.ValueAtPath(Path("/foo/2"), PrefixTreeMatch::ExactPath)); } TEST_F(DomPrefixTreeTests, OverwritePathSucceedsAtNonExistingPath) { DomPrefixTree tree; tree.SetValue(Path("/foo"), 40); DomPrefixTree subtree; subtree.SetValue(Path(), 80); subtree.SetValue(Path("/1"), 120); tree.OverwritePath(Path("/foo/bar"), AZStd::move(subtree)); // create the node if needed // Validate that the subtree is overwritten and new values are present at the paths in the original tree. EXPECT_EQ(80, *tree.ValueAtPath(Path("/foo/bar"), PrefixTreeMatch::ExactPath)); EXPECT_EQ(120, *tree.ValueAtPath(Path("/foo/bar/1"), PrefixTreeMatch::ExactPath)); } TEST_F(DomPrefixTreeTests, OverwritePathSucceedsAtPathWithoutParents) { DomPrefixTree tree; tree.SetValue(Path("/foo"), 40); DomPrefixTree subtree; subtree.SetValue(Path(), 80); subtree.SetValue(Path("/1"), 120); tree.OverwritePath(Path("/foo/bar/baz"), AZStd::move(subtree)); // create all nodes if needed // Validate that the subtree is overwritten and new values are present at the paths in the original tree. EXPECT_EQ(80, *tree.ValueAtPath(Path("/foo/bar/baz"), PrefixTreeMatch::ExactPath)); EXPECT_EQ(120, *tree.ValueAtPath(Path("/foo/bar/baz/1"), PrefixTreeMatch::ExactPath)); } TEST_F(DomPrefixTreeTests, OverwritePathFailsAtNonExistingPathIfNotCreateNodes) { DomPrefixTree tree; tree.SetValue(Path("/foo"), 40); DomPrefixTree subtree; subtree.SetValue(Path(), 80); bool isOverwritten = tree.OverwritePath(Path("/foo/bar"), AZStd::move(subtree), false); // 'bar' node does not exist // Validate that the subtree could not be overwritten. EXPECT_FALSE(isOverwritten); } TEST_F(DomPrefixTreeTests, OverwritePathFailsAtPathWithMissingParentsIfNotCreateNodes) { DomPrefixTree tree; tree.SetValue(Path("/foo"), 40); DomPrefixTree subtree; subtree.SetValue(Path(), 80); bool isOverwritten = tree.OverwritePath(Path("/foo/bar/baz"), AZStd::move(subtree), false); // Parent entry 'bar' does not exist // Validate that the subtree could not be overwritten. EXPECT_FALSE(isOverwritten); } 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::PathAndParents)); EXPECT_TRUE(tree.IsEmpty()); } TEST_F(DomPrefixTreeTests, VisitMissingExactPath) { TestVisitPath(Path("/bar"), PrefixTreeTraversalFlags::ExcludeChildPaths | PrefixTreeTraversalFlags::ExcludeParentPaths); EXPECT_EQ(0, VisitorResultCount()); } TEST_F(DomPrefixTreeTests, VisitExactPath) { TestVisitPath(Path("/foo/0"), PrefixTreeTraversalFlags::ExcludeChildPaths | PrefixTreeTraversalFlags::ExcludeParentPaths); EXPECT_EQ(1, VisitorResultCount()); EXPECT_TRUE(ValidateResult(Path("/foo/0"), 0)); } TEST_F(DomPrefixTreeTests, VisitChildren) { TestVisitPath(Path("/foo"), PrefixTreeTraversalFlags::ExcludeExactPath | PrefixTreeTraversalFlags::ExcludeParentPaths); EXPECT_EQ(2, VisitorResultCount()); EXPECT_TRUE(ValidateResult(Path("/foo/0"), 0)); EXPECT_TRUE(ValidateResult(Path("/foo/1"), 42)); } TEST_F(DomPrefixTreeTests, VisitPathAndChildren) { TestVisitPath(Path("/foo"), PrefixTreeTraversalFlags::ExcludeParentPaths); EXPECT_EQ(3, VisitorResultCount()); EXPECT_TRUE(ValidateResult(Path("/foo"), 99)); EXPECT_TRUE(ValidateResult(Path("/foo/0"), 0)); EXPECT_TRUE(ValidateResult(Path("/foo/1"), 42)); } TEST_F(DomPrefixTreeTests, VisitEntireTree) { TestVisitPath(Path(), PrefixTreeTraversalFlags::ExcludeExactPath); EXPECT_EQ(5, VisitorResultCount()); EXPECT_TRUE(ValidateResult(Path("/foo"), 99)); EXPECT_TRUE(ValidateResult(Path("/foo/0"), 0)); EXPECT_TRUE(ValidateResult(Path("/foo/1"), 42)); EXPECT_TRUE(ValidateResult(Path("/bar/bat"), 1)); EXPECT_TRUE(ValidateResult(Path("/bar/baz"), 2)); } TEST_F(DomPrefixTreeTests, VisitEntireTree_DepthFirst) { SetVisitDepthFirst(true); TestVisitPath(Path(), PrefixTreeTraversalFlags::ExcludeExactPath); EXPECT_EQ(5, VisitorResultCount()); EXPECT_TRUE(ValidateResult(Path("/foo"), 99)); EXPECT_TRUE(ValidateResult(Path("/foo/0"), 0)); EXPECT_TRUE(ValidateResult(Path("/foo/1"), 42)); EXPECT_TRUE(ValidateResult(Path("/bar/bat"), 1)); EXPECT_TRUE(ValidateResult(Path("/bar/baz"), 2)); } TEST_F(DomPrefixTreeTests, VisitParentsAndSelf) { TestVisitPath(Path("/foo/0"), PrefixTreeTraversalFlags::ExcludeChildPaths); EXPECT_EQ(2, VisitorResultCount()); EXPECT_TRUE(ValidateResult(Path("/foo"), 99)); EXPECT_TRUE(ValidateResult(Path("/foo/0"), 0)); } TEST_F(DomPrefixTreeTests, VisitParentsOnly) { TestVisitPath(Path("/foo/0"), PrefixTreeTraversalFlags::ExcludeExactPath | PrefixTreeTraversalFlags::ExcludeChildPaths); EXPECT_EQ(1, VisitorResultCount()); EXPECT_TRUE(ValidateResult(Path("/foo"), 99)); } TEST_F(DomPrefixTreeTests, EarlyTerminateVisit) { size_t visitCalls = 0; auto visitorFn = [&visitCalls](const Path&, int) { ++visitCalls; return false; }; m_runData->m_visitorTree.VisitPath(Path(), visitorFn, PrefixTreeTraversalFlags::TraverseLeastToMostSpecific); EXPECT_EQ(visitCalls, 1); visitCalls = 0; m_runData->m_visitorTree.VisitPath(Path(), visitorFn, PrefixTreeTraversalFlags::TraverseMostToLeastSpecific); EXPECT_EQ(visitCalls, 1); } TEST_F(DomPrefixTreeTests, RemoveChildrenDuringVisit) { // Remove children during a depth-first traversal auto visitorFn = [&](const Path& path, int) { m_runData->m_visitorTree.EraseValue(path, true); return true; }; m_runData->m_visitorTree.VisitPath(Path(), visitorFn, PrefixTreeTraversalFlags::TraverseMostToLeastSpecific); // Ensure all entries were successfully deleted TestVisitPath(Path(), PrefixTreeTraversalFlags::None); EXPECT_EQ(0, VisitorResultCount()); } } // namespace AZ::Dom::Tests