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