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/histogram_variants_reader.h"
6
7 #include <map>
8 #include <optional>
9 #include <string>
10
11 #include "base/base_paths.h"
12 #include "base/files/file_path.h"
13 #include "base/files/file_util.h"
14 #include "base/logging.h"
15 #include "base/path_service.h"
16 #include "testing/gtest/include/gtest/gtest.h"
17 #include "third_party/libxml/chromium/xml_reader.h"
18
19 namespace base {
20
21 namespace {
22
23 // Extracts single variants block from a histograms.xml.
24 //
25 // Expects |reader| to point at the given <variants> element with the name
26 // `variants_name`.
27 //
28 // Returns map { name => summary } on success, and nullopt on failure.
ParseVariantsFromHistogramsXml(const std::string & variants_name,XmlReader & reader)29 std::optional<HistogramVariantsEntryMap> ParseVariantsFromHistogramsXml(
30 const std::string& variants_name,
31 XmlReader& reader) {
32 HistogramVariantsEntryMap result;
33 bool success = true;
34
35 while (true) {
36 // Because reader initially points to the start of the <variants> element,
37 // and because <variants> elements are not nested, when the closing tag is
38 // reached, parsing is complete.
39 const std::string node_name = reader.NodeName();
40 if (node_name == "variants" && reader.IsClosingElement()) {
41 break;
42 }
43
44 if (node_name == "variant") {
45 std::string name;
46 std::string summary;
47 const bool has_name = reader.NodeAttribute("name", &name);
48 const bool has_summary = reader.NodeAttribute("summary", &summary);
49
50 if (!has_name) {
51 ADD_FAILURE() << "Bad " << variants_name << " variant entry, summary='"
52 << summary << "'): No 'name' attribute.";
53 success = false;
54 }
55
56 if (!has_summary) {
57 ADD_FAILURE() << "Bad " << variants_name << " variant entry, name='"
58 << name << "'): No 'summary' attribute.";
59 success = false;
60 }
61
62 // Don't check summary here because we want to check for duplicate names,
63 // and if there was a missing summary the function has already failed.
64 if (has_name) {
65 const auto insert_result = result.emplace(name, summary);
66 if (!insert_result.second) {
67 ADD_FAILURE() << "Duplicate entry in " << variants_name
68 << " variant entry, name='" << name << ')';
69 success = false;
70 }
71 }
72 }
73
74 // All variant entries are on the same level, so advance to the next
75 // sibling.
76 reader.Next();
77 }
78
79 return success ? std::make_optional(result) : std::nullopt;
80 }
81
82 } // namespace
83
ReadVariantsFromHistogramsXml(const std::string & variants_name,const std::string & subdirectory,bool from_metadata)84 std::optional<HistogramVariantsEntryMap> ReadVariantsFromHistogramsXml(
85 const std::string& variants_name,
86 const std::string& subdirectory,
87 bool from_metadata) {
88 base::FilePath src_root;
89 if (!base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &src_root)) {
90 ADD_FAILURE() << "Failed to get src root.";
91 return std::nullopt;
92 }
93
94 base::FilePath path =
95 src_root.AppendASCII("tools").AppendASCII("metrics").AppendASCII(
96 "histograms");
97 if (from_metadata) {
98 path = path.AppendASCII("metadata");
99 }
100 if (!subdirectory.empty()) {
101 path = path.AppendASCII(subdirectory);
102 }
103 path = path.AppendASCII("histograms.xml");
104
105 if (!base::PathExists(path)) {
106 ADD_FAILURE() << "File does not exist: " << path;
107 return std::nullopt;
108 }
109
110 XmlReader reader;
111 if (!reader.LoadFile(path.MaybeAsASCII())) {
112 ADD_FAILURE() << "Failed to load " << path;
113 return std::nullopt;
114 }
115
116 std::optional<HistogramVariantsEntryMap> result;
117
118 // Implement simple depth first search.
119 while (true) {
120 const std::string node_name = reader.NodeName();
121 if (node_name == "variants") {
122 std::string name;
123 if (reader.NodeAttribute("name", &name) && name == variants_name) {
124 if (result.has_value()) {
125 ADD_FAILURE() << "Duplicate variant '" << variants_name
126 << "' found in " << path;
127 return std::nullopt;
128 }
129
130 const bool got_into_variant = reader.Read();
131 if (!got_into_variant) {
132 ADD_FAILURE() << "Bad variant '" << variants_name
133 << "' (looks empty) found in " << path;
134 return std::nullopt;
135 }
136
137 result = ParseVariantsFromHistogramsXml(variants_name, reader);
138 if (!result.has_value()) {
139 ADD_FAILURE() << "Bad variant '" << variants_name << "' found in "
140 << path << " (format error).";
141 return std::nullopt;
142 }
143 }
144 }
145
146 // Go deeper if possible (stops at the closing tag of the deepest node).
147 if (reader.Read()) {
148 continue;
149 }
150
151 // Try next node on the same level (skips closing tag).
152 if (reader.Next()) {
153 continue;
154 }
155
156 // Go up until next node on the same level exists.
157 while (reader.Depth() && !reader.SkipToElement()) {
158 }
159
160 // Reached top. histograms.xml consists of the single top level node
161 // 'histogram-configuration', so this is the end.
162 if (!reader.Depth()) {
163 break;
164 }
165 }
166
167 return result;
168 }
169
170 } // namespace base
171