| // Copyright 2014 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include <array> | |
| #include <memory> | |
| #include <numeric> | |
| #include <optional> | |
| #include <vector> | |
| #include "base/strings/string_number_conversions.h" | |
| #include "build/build_config.h" | |
| #include "testing/gtest/include/gtest/gtest-param-test.h" | |
| #include "testing/gtest/include/gtest/gtest.h" | |
| #include "ui/accessibility/ax_event_generator.h" | |
| #include "ui/accessibility/ax_node.h" | |
| #include "ui/accessibility/ax_serializable_tree.h" | |
| #include "ui/accessibility/ax_tree.h" | |
| #include "ui/accessibility/ax_tree_serializer.h" | |
| #include "ui/accessibility/ax_tree_update.h" | |
| #include "ui/accessibility/tree_generator.h" | |
| namespace ui { | |
| namespace { | |
| // Do a more exhaustive test in release mode. If you're modifying | |
| // the algorithm you may want to try even larger tree sizes if you | |
| // can afford the time. | |
| #ifdef NDEBUG | |
| constexpr int kMax_tree_size = 4; | |
| #else | |
| constexpr int kMax_tree_size = 3; | |
| #endif | |
| // We split the test into four by splitting the two nested loops that builds the | |
| // trees. To do so, we need to know the maximum number of (permuted) trees. | |
| constexpr int kMax_number_of_trees0 = | |
| TreeGenerator::ComputeUniqueTreeCount(kMax_tree_size, | |
| /* permutations */ false); | |
| constexpr int kMax_number_of_trees1 = | |
| TreeGenerator::ComputeUniqueTreeCount(kMax_tree_size, | |
| /* permutations */ true); | |
| // A function to turn a tree into a string, capturing only the node ids | |
| // and their relationship to one another. | |
| // | |
| // The string format is kind of like an S-expression, with each expression | |
| // being either a node id, or a node id followed by a subexpression | |
| // representing its children. | |
| // | |
| // Examples: | |
| // | |
| // (1) is a tree with a single node with id 1. | |
| // (1 (2 3)) is a tree with 1 as the root, and 2 and 3 as its children. | |
| // (1 (2 (3))) has 1 as the root, 2 as its child, and then 3 as the child of 2. | |
| // (1 (2 (3x))) is the same with node 3 ignored. | |
| std::string TreeToStringHelper(const AXNode* node) { | |
| std::string result = base::NumberToString(node->id()); | |
| if (node->IsIgnored()) | |
| result += "x"; | |
| if (node->children().empty()) | |
| return result; | |
| const auto add_children = [](const std::string& str, const AXNode* node) { | |
| return str + " " + TreeToStringHelper(node); | |
| }; | |
| return result + " (" + | |
| std::accumulate(node->children().cbegin() + 1, node->children().cend(), | |
| TreeToStringHelper(node->children().front()), | |
| add_children) + | |
| ")"; | |
| } | |
| std::string TreeToString(const AXTree& tree) { | |
| return "(" + TreeToStringHelper(tree.root()) + ")"; | |
| } | |
| AXTreeUpdate SerializeEntireTree(AXSerializableTree& tree) { | |
| std::unique_ptr<AXTreeSource<const AXNode*, AXTreeData*, AXNodeData>> | |
| tree_source(tree.CreateTreeSource()); | |
| AXTreeSerializer<const AXNode*, std::vector<const AXNode*>, AXTreeUpdate*, | |
| AXTreeData*, AXNodeData> | |
| serializer(tree_source.get()); | |
| AXTreeUpdate update; | |
| CHECK(serializer.SerializeChanges(tree.root(), &update)); | |
| return update; | |
| } | |
| // Create an AXTreeUpdate consisting of only those nodes from | |
| // |tree0| that changed their ignored status in |tree1|. | |
| AXTreeUpdate MakeTreeUpdateFromIgnoredChanges(AXSerializableTree& tree0, | |
| AXSerializableTree& tree1) { | |
| AXTreeUpdate update = SerializeEntireTree(tree1); | |
| AXTreeUpdate result; | |
| for (auto& node : update.nodes) { | |
| AXNode* tree0_node = tree0.GetFromId(node.id); | |
| AXNode* tree1_node = tree1.GetFromId(node.id); | |
| if (tree0_node->IsIgnored() != tree1_node->IsIgnored()) | |
| result.nodes.push_back(node); | |
| } | |
| return result; | |
| } | |
| void SerializeUnignoredNodes(AXNode* node, AXTreeUpdate* update) { | |
| AXNodeData data = node->data(); | |
| data.child_ids.clear(); | |
| for (size_t i = 0; i < node->GetUnignoredChildCount(); i++) { | |
| AXNode* child = node->GetUnignoredChildAtIndex(i); | |
| data.child_ids.push_back(child->id()); | |
| } | |
| update->nodes.push_back(data); | |
| for (size_t i = 0; i < node->GetUnignoredChildCount(); i++) { | |
| AXNode* child = node->GetUnignoredChildAtIndex(i); | |
| SerializeUnignoredNodes(child, update); | |
| } | |
| } | |
| void MakeTreeOfUnignoredNodesOnly(AXSerializableTree& src, | |
| AXSerializableTree* dst) { | |
| AXTreeUpdate update; | |
| update.root_id = src.root()->id(); | |
| SerializeUnignoredNodes(src.root(), &update); | |
| CHECK(dst->Unserialize(update)); | |
| } | |
| } // namespace | |
| // Test the TreeGenerator class by building all possible trees with | |
| // 3 nodes and the ids [1...3], with no permutations of ids. | |
| TEST(AXGeneratedTreeTest, TestTreeGeneratorNoPermutations) { | |
| int tree_size = 3; | |
| TreeGenerator generator(tree_size, false); | |
| // clang-format off | |
| auto EXPECTED_TREES = std::to_array<const char *>({ | |
| "(1)", | |
| "(1 (2))", | |
| "(1 (2 3))", | |
| "(1 (2 (3)))", | |
| }); | |
| // clang-format on | |
| int n = generator.UniqueTreeCount(); | |
| ASSERT_EQ(static_cast<int>(std::size(EXPECTED_TREES)), n); | |
| for (int i = 0; i < n; ++i) { | |
| AXTree tree; | |
| generator.BuildUniqueTree(i, &tree); | |
| std::string str = TreeToString(tree); | |
| EXPECT_EQ(EXPECTED_TREES[i], str); | |
| } | |
| } | |
| // Test generating trees with permutations of ignored nodes. | |
| TEST(AXGeneratedTreeTest, TestGeneratingTreesWithIgnoredNodes) { | |
| int tree_size = 3; | |
| TreeGenerator generator(tree_size, false); | |
| // clang-format off | |
| auto EXPECTED_TREES = std::to_array<const char *>({ | |
| "(1)", | |
| "(1 (2))", | |
| "(1 (2x))", | |
| "(1 (2 3))", | |
| "(1 (2x 3))", | |
| "(1 (2 3x))", | |
| "(1 (2x 3x))", | |
| "(1 (2 (3)))", | |
| "(1 (2x (3)))", | |
| "(1 (2 (3x)))", | |
| "(1 (2x (3x)))", | |
| }); | |
| // clang-format on | |
| int n = generator.UniqueTreeCount(); | |
| int expected_index = 0; | |
| for (int i = 0; i < n; ++i) { | |
| int ignored_permutation_count = | |
| generator.IgnoredPermutationCountPerUniqueTree(i); | |
| for (int j = 0; j < ignored_permutation_count; j++) { | |
| AXTree tree; | |
| generator.BuildUniqueTreeWithIgnoredNodes( | |
| i, j, /* focused_node */ std::nullopt, &tree); | |
| std::string str = TreeToString(tree); | |
| EXPECT_EQ(EXPECTED_TREES[expected_index++], str); | |
| } | |
| } | |
| EXPECT_EQ(11, expected_index); | |
| } | |
| // Test the TreeGenerator class by building all possible trees with | |
| // 3 nodes and the ids [1...3] permuted in any order. | |
| TEST(AXGeneratedTreeTest, TestTreeGeneratorWithPermutations) { | |
| int tree_size = 3; | |
| TreeGenerator generator(tree_size, true); | |
| // clang-format off | |
| auto EXPECTED_TREES = std::to_array<const char *>({ | |
| "(1)", | |
| "(1 (2))", | |
| "(2 (1))", | |
| "(1 (2 3))", | |
| "(2 (1 3))", | |
| "(3 (1 2))", | |
| "(1 (3 2))", | |
| "(2 (3 1))", | |
| "(3 (2 1))", | |
| "(1 (2 (3)))", | |
| "(2 (1 (3)))", | |
| "(3 (1 (2)))", | |
| "(1 (3 (2)))", | |
| "(2 (3 (1)))", | |
| "(3 (2 (1)))", | |
| }); | |
| // clang-format on | |
| int n = generator.UniqueTreeCount(); | |
| ASSERT_EQ(static_cast<int>(std::size(EXPECTED_TREES)), n); | |
| for (int i = 0; i < n; i++) { | |
| AXTree tree; | |
| generator.BuildUniqueTree(i, &tree); | |
| std::string str = TreeToString(tree); | |
| EXPECT_EQ(EXPECTED_TREES[i], str); | |
| } | |
| } | |
| struct PermutationBlock { | |
| PermutationBlock(int first_unique_tree0, | |
| int last_unique_tree0, | |
| int first_unique_tree1, | |
| int last_unique_tree1) | |
| : first_unique_tree0(first_unique_tree0), | |
| last_unique_tree0(last_unique_tree0), | |
| first_unique_tree1(first_unique_tree1), | |
| last_unique_tree1(last_unique_tree1) {} | |
| int first_unique_tree0; | |
| int last_unique_tree0; | |
| int first_unique_tree1; | |
| int last_unique_tree1; | |
| }; | |
| class SerializeGeneratedTreesTest | |
| : public testing::TestWithParam<PermutationBlock> {}; | |
| TEST_P(SerializeGeneratedTreesTest, SerializeGeneratedTrees) { | |
| const int first_tree0_ = GetParam().first_unique_tree0; | |
| const int last_tree0_ = GetParam().last_unique_tree0; | |
| const int first_tree1_ = GetParam().first_unique_tree1; | |
| const int last_tree1_ = GetParam().last_unique_tree1; | |
| TreeGenerator generator0(kMax_tree_size, /* permutations */ false); | |
| TreeGenerator generator1(kMax_tree_size, /* permutations */ true); | |
| for (int i = first_tree0_; i < last_tree0_; i++) { | |
| // Build the first tree, tree0. | |
| AXSerializableTree tree0; | |
| generator0.BuildUniqueTree(i, &tree0); | |
| SCOPED_TRACE("tree0 is " + TreeToString(tree0)); | |
| for (int j = first_tree1_; j < last_tree1_; j++) { | |
| // Build the second tree, tree1. | |
| AXSerializableTree tree1; | |
| generator1.BuildUniqueTree(j, &tree1); | |
| SCOPED_TRACE("tree1 is " + TreeToString(tree1)); | |
| int tree_size = tree1.size(); | |
| // Now iterate over which node to update first, |k|. | |
| for (int k = 0; k < tree_size; k++) { | |
| // Iterate over a node to invalidate, |l| (zero means no invalidation). | |
| for (int l = 0; l <= tree_size; l++) { | |
| SCOPED_TRACE("i=" + base::NumberToString(i) + | |
| " j=" + base::NumberToString(j) + | |
| " k=" + base::NumberToString(k) + | |
| " l=" + base::NumberToString(l)); | |
| // Start by serializing tree0 and unserializing it into a new | |
| // empty tree |dst_tree|. | |
| std::unique_ptr<AXTreeSource<const AXNode*, AXTreeData*, AXNodeData>> | |
| tree0_source(tree0.CreateTreeSource()); | |
| AXTreeSerializer<const AXNode*, std::vector<const AXNode*>, | |
| AXTreeUpdate*, AXTreeData*, AXNodeData> | |
| serializer(tree0_source.get()); | |
| AXTreeUpdate update0; | |
| ASSERT_TRUE(serializer.SerializeChanges(tree0.root(), &update0)); | |
| AXTree dst_tree; | |
| ASSERT_TRUE(dst_tree.Unserialize(update0)) | |
| << dst_tree.error() << "\n" | |
| << TreeToString(dst_tree) | |
| << "\nTree update: " << update0.ToString(); | |
| // At this point, |dst_tree| should now be identical to |tree0|. | |
| EXPECT_EQ(TreeToString(tree0), TreeToString(dst_tree)); | |
| // Next, pretend that tree0 turned into tree1. | |
| std::unique_ptr<AXTreeSource<const AXNode*, AXTreeData*, AXNodeData>> | |
| tree1_source(tree1.CreateTreeSource()); | |
| serializer.ChangeTreeSourceForTesting(tree1_source.get()); | |
| // Mark as dirty the subtree rooted at one of the nodes. | |
| if (l > 0) | |
| serializer.MarkSubtreeDirty(l); | |
| // Serialize a sequence of updates to |dst_tree| to match. | |
| for (int k_index = 0; k_index < tree_size; ++k_index) { | |
| int id = 1 + (k + k_index) % tree_size; | |
| AXTreeUpdate update; | |
| ASSERT_TRUE( | |
| serializer.SerializeChanges(tree1.GetFromId(id), &update)); | |
| std::string tree_before_str = TreeToString(dst_tree); | |
| ASSERT_TRUE(dst_tree.Unserialize(update)) | |
| << dst_tree.error() << "\nTree before : " << tree_before_str | |
| << "\nTree after : " << TreeToString(dst_tree) | |
| << "\nExpected after: " << TreeToString(tree1) | |
| << "\nTree update : " << update.ToString(); | |
| } | |
| // After the sequence of updates, |dst_tree| should now be | |
| // identical to |tree1|. | |
| EXPECT_EQ(TreeToString(tree1), TreeToString(dst_tree)); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Test mutating every possible tree with <n> nodes to every other possible | |
| // tree with <n> nodes, where <n> is 4 in release mode and 3 in debug mode | |
| // (for speed). For each possible combination of trees, we also vary which | |
| // node we serialize first. | |
| // | |
| // For every possible scenario, we check that the AXTreeUpdate is valid, | |
| // that the destination tree can unserialize it and create a valid tree, | |
| // and that after updating all nodes the resulting tree now matches the | |
| // intended tree. | |
| // | |
| // Sheriffs: this test is actually very stable and reliable, but it's | |
| // cpu-bound so under extremely heavy load it sometimes times out even | |
| // though it only takes 1 - 2 seconds to run under normal load. | |
| // Please don't disable unless it's actually flaking frequently (e.g., | |
| // every day). Check Flake Portal first. | |
| INSTANTIATE_TEST_SUITE_P( | |
| AXGeneratedTreeTest0, | |
| SerializeGeneratedTreesTest, | |
| testing::Values(PermutationBlock(0, | |
| kMax_number_of_trees0 / 2, | |
| 0, | |
| kMax_number_of_trees1 / 2), | |
| PermutationBlock(0, | |
| kMax_number_of_trees0 / 2, | |
| kMax_number_of_trees1 / 2, | |
| kMax_number_of_trees1), | |
| PermutationBlock(kMax_number_of_trees0 / 2, | |
| kMax_number_of_trees0, | |
| 0, | |
| kMax_number_of_trees1 / 2), | |
| PermutationBlock(kMax_number_of_trees0 / 2, | |
| kMax_number_of_trees0, | |
| kMax_number_of_trees1 / 2, | |
| kMax_number_of_trees1))); | |
| // Sheriffs: this test is actually very stable and reliable, but it's | |
| // cpu-bound so under extremely heavy load it sometimes times out even | |
| // though it only takes 1 - 2 seconds to run under normal load. | |
| // Please don't disable unless it's actually flaking frequently (e.g., | |
| // every day). Check Flake Portal first. | |
| TEST(AXGeneratedTreeTest, GeneratedTreesWithIgnoredNodes) { | |
| // Do a more exhaustive test in release mode. If you're modifying | |
| // the algorithm you may want to try even larger tree sizes if you | |
| // can afford the time. | |
| #ifdef NDEBUG | |
| int max_tree_size = 5; | |
| #else | |
| LOG(WARNING) << "Debug build, only testing trees with 4 nodes and not 5."; | |
| int max_tree_size = 4; | |
| #endif | |
| TreeGenerator generator(max_tree_size, false); | |
| int unique_tree_count = generator.UniqueTreeCount(); | |
| // Loop over every possible tree up to a certain size. | |
| for (int tree_index = 0; tree_index < unique_tree_count; tree_index++) { | |
| // Try each permutation of nodes other than the root being ignored. | |
| // We'll call this tree the "fat" tree because it has redundant | |
| // ignored nodes. Also try permuting the focused node, because focus affects | |
| // the ignored state of a node by removing it. | |
| int ignored_permutation_count = | |
| generator.IgnoredPermutationCountPerUniqueTree(tree_index); | |
| for (int perm_index0 = 0; perm_index0 < ignored_permutation_count; | |
| perm_index0++) { | |
| AXSerializableTree fat_tree; | |
| generator.BuildUniqueTreeWithIgnoredNodes( | |
| tree_index, perm_index0, /* focused_node */ std::nullopt, &fat_tree); | |
| SCOPED_TRACE("fat_tree is " + TreeToString(fat_tree)); | |
| // Create a second tree, also with each permutations of nodes other than | |
| // the root being ignored. | |
| for (int perm_index1 = 1; perm_index1 < ignored_permutation_count; | |
| perm_index1++) { | |
| AXSerializableTree fat_tree1; | |
| generator.BuildUniqueTreeWithIgnoredNodes( | |
| tree_index, perm_index1, /* focused_node */ std::nullopt, | |
| &fat_tree1); | |
| SCOPED_TRACE("fat_tree1 is " + TreeToString(fat_tree1)); | |
| // Make a source and destination tree using only the unignored nodes. | |
| // We call this one the "skinny" tree. | |
| AXSerializableTree skinny_tree; | |
| MakeTreeOfUnignoredNodesOnly(fat_tree, &skinny_tree); | |
| AXSerializableTree skinny_tree1; | |
| MakeTreeOfUnignoredNodesOnly(fat_tree1, &skinny_tree1); | |
| // Now, turn fat_tree into fat_tree1, and record the generated events. | |
| AXEventGenerator event_generator(&fat_tree); | |
| AXTreeUpdate update = | |
| MakeTreeUpdateFromIgnoredChanges(fat_tree, fat_tree1); | |
| std::string fat_tree_before_str = TreeToString(fat_tree); | |
| ASSERT_TRUE(fat_tree.Unserialize(update)) | |
| << fat_tree.error() << "\nTree before : " << fat_tree_before_str | |
| << "\nTree after :" << TreeToString(fat_tree) | |
| << "\nExpected after: " << TreeToString(fat_tree1) | |
| << "\nTree update : " << update.ToString(); | |
| EXPECT_EQ(TreeToString(fat_tree), TreeToString(fat_tree1)); | |
| // Capture the events generated. | |
| std::map<AXNodeID, std::set<AXEventGenerator::Event>> actual_events; | |
| for (const AXEventGenerator::TargetedEvent& event : event_generator) { | |
| const AXNode* node = fat_tree.GetFromId(event.node_id); | |
| ASSERT_NE(nullptr, node); | |
| if (node->IsIgnored() || | |
| event.event_params->event == | |
| AXEventGenerator::Event::IGNORED_CHANGED) { | |
| continue; | |
| } | |
| actual_events[event.node_id].insert(event.event_params->event); | |
| } | |
| // Now, turn skinny_tree into skinny_tree1 and compare | |
| // the generated events. | |
| AXEventGenerator skinny_event_generator(&skinny_tree); | |
| AXTreeUpdate skinny_update = SerializeEntireTree(skinny_tree1); | |
| std::string skinny_tree_before_str = TreeToString(skinny_tree); | |
| ASSERT_TRUE(skinny_tree.Unserialize(skinny_update)) | |
| << skinny_tree.error() | |
| << "\nTree before : " << skinny_tree_before_str | |
| << "\nTree after :" << TreeToString(skinny_tree) | |
| << "\nExpected after: " << TreeToString(skinny_tree1) | |
| << "\nTree update : " << skinny_update.ToString(); | |
| EXPECT_EQ(TreeToString(skinny_tree), TreeToString(skinny_tree1)); | |
| std::map<AXNodeID, std::set<AXEventGenerator::Event>> expected_events; | |
| for (const AXEventGenerator::TargetedEvent& event : | |
| skinny_event_generator) | |
| expected_events[event.node_id].insert(event.event_params->event); | |
| for (auto& entry : expected_events) { | |
| AXNodeID node_id = entry.first; | |
| for (auto& event_type : entry.second) { | |
| EXPECT_TRUE(actual_events[node_id].find(event_type) != | |
| actual_events[node_id].end()) | |
| << "Expected " << event_type << " on node " << node_id; | |
| } | |
| } | |
| for (auto& entry : actual_events) { | |
| AXNodeID node_id = entry.first; | |
| for (auto& event_type : entry.second) { | |
| EXPECT_TRUE(expected_events[node_id].find(event_type) != | |
| expected_events[node_id].end()) | |
| << "Unexpected " << event_type << " on node " << node_id; | |
| } | |
| } | |
| // For each node in skinny_tree (the tree with only the unignored | |
| // nodes), check the node in fat_tree (the tree with ignored nodes). | |
| // Make sure that the parents, children, and siblings are all computed | |
| // correctly. | |
| AXTreeUpdate skinny_tree_serialized = SerializeEntireTree(skinny_tree); | |
| for (const AXNodeData& skinny_tree_node_data : | |
| skinny_tree_serialized.nodes) { | |
| AXNodeID id = skinny_tree_node_data.id; | |
| AXNode* skinny_tree_node = skinny_tree.GetFromId(id); | |
| AXNode* fat_tree_node = fat_tree.GetFromId(id); | |
| SCOPED_TRACE("Testing node ID " + base::NumberToString(id)); | |
| // Check children. | |
| EXPECT_EQ(skinny_tree_node->children().size(), | |
| fat_tree_node->GetUnignoredChildCount()); | |
| // Check child IDs. | |
| for (size_t j = 0; j < skinny_tree_node->children().size(); j++) { | |
| AXNode* skinny_tree_child = skinny_tree_node->children()[j]; | |
| AXNode* fat_tree_child = fat_tree_node->GetUnignoredChildAtIndex(j); | |
| EXPECT_TRUE(skinny_tree_child); | |
| EXPECT_TRUE(fat_tree_child); | |
| if (fat_tree_child) | |
| EXPECT_EQ(skinny_tree_child->id(), fat_tree_child->id()); | |
| } | |
| // Check parent. | |
| if (skinny_tree_node->parent()) { | |
| EXPECT_EQ(skinny_tree_node->parent()->id(), | |
| fat_tree_node->GetUnignoredParent()->id()); | |
| } else { | |
| EXPECT_FALSE(fat_tree_node->GetUnignoredParent()); | |
| } | |
| // Check index in parent. | |
| EXPECT_EQ(skinny_tree_node->index_in_parent(), | |
| fat_tree_node->GetUnignoredIndexInParent()); | |
| // Unignored previous sibling. | |
| size_t index_in_parent = skinny_tree_node->index_in_parent(); | |
| size_t num_siblings = | |
| skinny_tree_node->parent() | |
| ? skinny_tree_node->parent()->children().size() | |
| : 1; | |
| if (index_in_parent > 0) { | |
| AXNode* skinny_tree_previous_sibling = | |
| skinny_tree_node->parent()->children()[index_in_parent - 1]; | |
| AXNode* fat_tree_previous_sibling = | |
| fat_tree_node->GetPreviousUnignoredSibling(); | |
| EXPECT_TRUE(fat_tree_previous_sibling); | |
| if (fat_tree_previous_sibling) { | |
| EXPECT_EQ(skinny_tree_previous_sibling->id(), | |
| fat_tree_previous_sibling->id()); | |
| } | |
| } | |
| // Unignored next sibling. | |
| if (index_in_parent < num_siblings - 1) { | |
| AXNode* skinny_tree_next_sibling = | |
| skinny_tree_node->parent()->children()[index_in_parent + 1]; | |
| AXNode* fat_tree_next_sibling = | |
| fat_tree_node->GetNextUnignoredSibling(); | |
| EXPECT_TRUE(fat_tree_next_sibling); | |
| if (fat_tree_next_sibling) { | |
| EXPECT_EQ(skinny_tree_next_sibling->id(), | |
| fat_tree_next_sibling->id()); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } // namespace ui |