• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2024 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "base/test/metrics/action_suffix_reader.h"
6 
7 #include <map>
8 #include <optional>
9 #include <sstream>
10 #include <string>
11 #include <vector>
12 
13 #include "base/base_paths.h"
14 #include "base/files/file_path.h"
15 #include "base/files/file_util.h"
16 #include "base/logging.h"
17 #include "base/path_service.h"
18 #include "base/strings/stringprintf.h"
19 #include "testing/gtest/include/gtest/gtest.h"
20 #include "third_party/libxml/chromium/xml_reader.h"
21 
22 namespace base {
23 
24 namespace {
25 
26 // Extracts suffixes from a histograms.xml if the suffixes apply to
27 // `affected_action`, otherwise null.
28 //
29 // Expects |reader| to point at the starting node of the suffixes block.
30 //
31 // Returns map { name => label } on success, and nullopt on failure.
ParseActionSuffixesFromActionsXml(const std::string & affected_action,XmlReader & reader)32 std::optional<ActionSuffixEntryMap> ParseActionSuffixesFromActionsXml(
33     const std::string& affected_action,
34     XmlReader& reader) {
35   ActionSuffixEntryMap result;
36   std::vector<std::string> failures;
37   bool action_found = false;
38 
39   while (true) {
40     // Because reader initially points to the start of the <action-suffix>
41     // element, and because <suffix> and <affected-action> elements are not
42     // nested, when the closing tag is reached, parsing is complete.
43     const std::string node_name = reader.NodeName();
44     if (node_name == "action-suffix" && reader.IsClosingElement()) {
45       break;
46     }
47 
48     std::string name;
49 
50     // Affected actions can be anywhere in the XML block, so just check if the
51     // one the caller cares about is present.
52     if (node_name == "affected-action" && reader.NodeAttribute("name", &name) &&
53         name == affected_action) {
54       action_found = true;
55     }
56 
57     // The other thing found in this block is the list of suffixes. Capture them
58     // all, recording failures, and then later the list will be returned if the
59     // action was found.
60     if (node_name == "suffix") {
61       std::string label;
62       const bool has_name = reader.NodeAttribute("name", &name);
63       const bool has_label = reader.NodeAttribute("label", &label);
64 
65       if (!has_name) {
66         failures.emplace_back(StringPrintf(
67             "Bad suffix entry with label \"%s\"; no 'name' attribute.",
68             label.c_str()));
69       }
70 
71       if (!has_label) {
72         failures.emplace_back(StringPrintf(
73             "Bad suffix entry with name \"%s\"; no 'label' attribute.",
74             name.c_str()));
75       }
76 
77       // Don't check label here because we want to check for duplicate names,
78       // and if there was a missing label the function has already failed.
79       if (has_name) {
80         const auto insert_result = result.emplace(name, label);
81         if (!insert_result.second) {
82           failures.emplace_back(
83               StringPrintf("Duplicate suffix name \"%s\"", name.c_str()));
84         }
85       }
86     }
87 
88     // All variant entries are on the same level, so advance to the next
89     // sibling.
90     reader.Next();
91   }
92 
93   if (!action_found) {
94     return std::nullopt;
95   }
96 
97   if (!failures.empty()) {
98     for (const auto& failure : failures) {
99       ADD_FAILURE() << failure;
100     }
101     return std::nullopt;
102   }
103 
104   return result;
105 }
106 
ReadActionSuffixesForActionImpl(XmlReader & reader,const std::string & affected_action)107 std::vector<ActionSuffixEntryMap> ReadActionSuffixesForActionImpl(
108     XmlReader& reader,
109     const std::string& affected_action) {
110   std::vector<ActionSuffixEntryMap> result;
111 
112   // Implement simple depth first search.
113   while (true) {
114     const std::string node_name = reader.NodeName();
115     if (node_name == "action-suffix") {
116       // Try to step into the node.
117       if (reader.Read()) {
118         auto suffixes =
119             ParseActionSuffixesFromActionsXml(affected_action, reader);
120         if (suffixes) {
121           result.emplace_back(std::move(*suffixes));
122         }
123       }
124     }
125 
126     // Go deeper if possible (stops at the closing tag of the deepest node).
127     if (reader.Read()) {
128       continue;
129     }
130 
131     // Try next node on the same level (skips closing tag).
132     if (reader.Next()) {
133       continue;
134     }
135 
136     // Go up until next node on the same level exists.
137     while (reader.Depth() && !reader.SkipToElement()) {
138     }
139 
140     // Reached top. actions.xml consists of the single top level node 'actions',
141     // so this is the end.
142     if (!reader.Depth()) {
143       break;
144     }
145   }
146 
147   return result;
148 }
149 
150 }  // namespace
151 
152 // Hidden function that reads from `xml_string` instead of actions.xml.
153 // Used to unit test the internal logic.
ReadActionSuffixesForActionForTesting(const std::string & xml_string,const std::string & affected_action)154 std::vector<ActionSuffixEntryMap> ReadActionSuffixesForActionForTesting(
155     const std::string& xml_string,
156     const std::string& affected_action) {
157   XmlReader reader;
158   CHECK(reader.Load(xml_string));
159   return ReadActionSuffixesForActionImpl(reader, affected_action);
160 }
161 
ReadActionSuffixesForAction(const std::string & affected_action)162 std::vector<ActionSuffixEntryMap> ReadActionSuffixesForAction(
163     const std::string& affected_action) {
164   FilePath src_root;
165   if (!PathService::Get(DIR_SRC_TEST_DATA_ROOT, &src_root)) {
166     ADD_FAILURE() << "Failed to get src root.";
167     return {};
168   }
169 
170   const FilePath path = src_root.AppendASCII("tools")
171                             .AppendASCII("metrics")
172                             .AppendASCII("actions")
173                             .AppendASCII("actions.xml");
174 
175   if (!PathExists(path)) {
176     ADD_FAILURE() << "File does not exist: " << path;
177     return {};
178   }
179 
180   XmlReader reader;
181   if (!reader.LoadFile(path.MaybeAsASCII())) {
182     ADD_FAILURE() << "Failed to load " << path;
183     return {};
184   }
185 
186   return ReadActionSuffixesForActionImpl(reader, affected_action);
187 }
188 
189 }  // namespace base
190