1 // Copyright 2021 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 "components/metrics/structured/external_metrics.h"
6 #include "components/metrics/structured/structured_metrics_features.h"
7
8 #include <memory>
9 #include <numeric>
10
11 #include "base/files/file_util.h"
12 #include "base/files/scoped_temp_dir.h"
13 #include "base/logging.h"
14 #include "base/strings/string_number_conversions.h"
15 #include "base/test/metrics/histogram_tester.h"
16 #include "base/test/scoped_feature_list.h"
17 #include "base/test/task_environment.h"
18 #include "build/build_config.h"
19 #include "components/metrics/structured/proto/event_storage.pb.h"
20 #include "testing/gmock/include/gmock/gmock.h"
21 #include "testing/gtest/include/gtest/gtest.h"
22
23 namespace metrics {
24 namespace structured {
25 namespace {
26
27 using testing::UnorderedElementsAre;
28
29 // Make a simple testing proto with one |uma_events| message for each id in
30 // |ids|.
MakeTestingProto(const std::vector<uint64_t> & ids,uint64_t project_name_hash=0)31 EventsProto MakeTestingProto(const std::vector<uint64_t>& ids,
32 uint64_t project_name_hash = 0) {
33 EventsProto proto;
34
35 for (const auto id : ids) {
36 auto* event = proto.add_uma_events();
37 event->set_project_name_hash(project_name_hash);
38 event->set_profile_event_id(id);
39 }
40
41 return proto;
42 }
43
44 // Check that |proto| is consistent with the proto that would be generated by
45 // MakeTestingProto(ids).
AssertEqualsTestingProto(const EventsProto & proto,const std::vector<uint64_t> & ids)46 void AssertEqualsTestingProto(const EventsProto& proto,
47 const std::vector<uint64_t>& ids) {
48 ASSERT_EQ(proto.uma_events().size(), static_cast<int>(ids.size()));
49 ASSERT_TRUE(proto.events().empty());
50
51 for (size_t i = 0; i < ids.size(); ++i) {
52 const auto& event = proto.uma_events(i);
53 ASSERT_EQ(event.profile_event_id(), ids[i]);
54 ASSERT_FALSE(event.has_event_name_hash());
55 ASSERT_TRUE(event.metrics().empty());
56 }
57 }
58
59 } // namespace
60
61 class ExternalMetricsTest : public testing::Test {
62 public:
SetUp()63 void SetUp() override {
64 ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
65
66 // TODO(b/181724341): Remove this when the bluetooth metrics feature is
67 // enabled by default.
68 scoped_feature_list_.InitWithFeatures(
69 /*enabled_features=*/{},
70 /*disabled_features=*/{kBluetoothSessionizedMetrics});
71 }
72
Init()73 void Init() {
74 // We don't use the scheduling feature when testing ExternalMetrics, instead
75 // we just call CollectMetrics directly. So make up a time interval here
76 // that we'll never reach in a test.
77 const auto one_hour = base::Hours(1);
78 external_metrics_ = std::make_unique<ExternalMetrics>(
79 temp_dir_.GetPath(), one_hour,
80 base::BindRepeating(&ExternalMetricsTest::OnEventsCollected,
81 base::Unretained(this)));
82
83 // For most tests the recording needs to be enabled.
84 EnableRecording();
85 }
86
EnableRecording()87 void EnableRecording() { external_metrics_->EnableRecording(); }
88
DisableRecording()89 void DisableRecording() { external_metrics_->DisableRecording(); }
90
CollectEvents()91 void CollectEvents() {
92 external_metrics_->CollectEvents();
93 Wait();
94 CHECK(proto_.has_value());
95 }
96
OnEventsCollected(const EventsProto & proto)97 void OnEventsCollected(const EventsProto& proto) {
98 proto_ = std::move(proto);
99 }
100
WriteToDisk(const std::string & name,const EventsProto & proto)101 void WriteToDisk(const std::string& name, const EventsProto& proto) {
102 CHECK(base::WriteFile(temp_dir_.GetPath().Append(name),
103 proto.SerializeAsString()));
104 }
105
WriteToDisk(const std::string & name,const std::string & str)106 void WriteToDisk(const std::string& name, const std::string& str) {
107 CHECK(base::WriteFile(temp_dir_.GetPath().Append(name), str));
108 }
109
Wait()110 void Wait() { task_environment_.RunUntilIdle(); }
111
112 base::test::ScopedFeatureList scoped_feature_list_;
113 base::ScopedTempDir temp_dir_;
114 std::unique_ptr<ExternalMetrics> external_metrics_;
115 std::optional<EventsProto> proto_;
116
117 base::test::TaskEnvironment task_environment_{
118 base::test::TaskEnvironment::MainThreadType::UI,
119 base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED};
120 base::HistogramTester histogram_tester_;
121 };
122
TEST_F(ExternalMetricsTest,ReadOneFile)123 TEST_F(ExternalMetricsTest, ReadOneFile) {
124 // Make one proto with three events.
125 WriteToDisk("myproto", MakeTestingProto({111, 222, 333}));
126 Init();
127
128 CollectEvents();
129
130 // We should have correctly picked up the three events.
131 AssertEqualsTestingProto(proto_.value(), {111, 222, 333});
132 // And the directory should now be empty.
133 ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
134 }
135
TEST_F(ExternalMetricsTest,ReadManyFiles)136 TEST_F(ExternalMetricsTest, ReadManyFiles) {
137 // Make three protos with three events each.
138 WriteToDisk("first", MakeTestingProto({111, 222, 333}));
139 WriteToDisk("second", MakeTestingProto({444, 555, 666}));
140 WriteToDisk("third", MakeTestingProto({777, 888, 999}));
141 Init();
142
143 CollectEvents();
144
145 // We should have correctly picked up the nine events. Don't check for order,
146 // because we can't guarantee the files will be read from disk in any
147 // particular order.
148 std::vector<int64_t> ids;
149 for (const auto& event : proto_.value().uma_events()) {
150 ids.push_back(event.profile_event_id());
151 }
152 ASSERT_THAT(
153 ids, UnorderedElementsAre(111, 222, 333, 444, 555, 666, 777, 888, 999));
154
155 // The directory should be empty after reading.
156 ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
157 }
158
TEST_F(ExternalMetricsTest,ReadZeroFiles)159 TEST_F(ExternalMetricsTest, ReadZeroFiles) {
160 Init();
161 CollectEvents();
162 // We should have an empty proto.
163 AssertEqualsTestingProto(proto_.value(), {});
164 // And the directory should be empty too.
165 ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
166 }
167
TEST_F(ExternalMetricsTest,CollectTwice)168 TEST_F(ExternalMetricsTest, CollectTwice) {
169 Init();
170 WriteToDisk("first", MakeTestingProto({111, 222, 333}));
171 CollectEvents();
172 AssertEqualsTestingProto(proto_.value(), {111, 222, 333});
173
174 WriteToDisk("first", MakeTestingProto({444}));
175 CollectEvents();
176 AssertEqualsTestingProto(proto_.value(), {444});
177 }
178
TEST_F(ExternalMetricsTest,HandleCorruptFile)179 TEST_F(ExternalMetricsTest, HandleCorruptFile) {
180 Init();
181
182 WriteToDisk("invalid", "surprise i'm not a proto");
183 WriteToDisk("valid", MakeTestingProto({111, 222, 333}));
184
185 CollectEvents();
186 AssertEqualsTestingProto(proto_.value(), {111, 222, 333});
187 // Should have deleted the invalid file too.
188 ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
189 }
190
191 // TODO(b/181724341): Remove this when the bluetooth metrics feature is enabled
192 // by default.
TEST_F(ExternalMetricsTest,FilterBluetoothEvents)193 TEST_F(ExternalMetricsTest, FilterBluetoothEvents) {
194 // Event name hash for cros's BluetoothPairingStateChanged event.
195 const uint64_t event_hash = UINT64_C(11839023048095184048);
196
197 Init();
198
199 // Use the profile_event_id as an marker of which event is which, and assign a
200 // bluetooth event hash to ids > 100.
201 EventsProto proto;
202 for (const auto id : {101, 1, 2, 102, 103, 3, 104}) {
203 auto* event = proto.add_uma_events();
204 event->set_profile_event_id(id);
205 if (id > 100) {
206 event->set_event_name_hash(event_hash);
207 }
208 }
209 WriteToDisk("proto", proto);
210
211 CollectEvents();
212 AssertEqualsTestingProto(proto_.value(), {1, 2, 3});
213 }
214
TEST_F(ExternalMetricsTest,FileNumberReadCappedAndDiscarded)215 TEST_F(ExternalMetricsTest, FileNumberReadCappedAndDiscarded) {
216 // Setup feature.
217 base::test::ScopedFeatureList feature_list;
218 const int file_limit = 2;
219 feature_list.InitAndEnableFeatureWithParameters(
220 features::kStructuredMetrics,
221 {{"file_limit", base::NumberToString(file_limit)}});
222
223 Init();
224
225 // File limit is set to 2. Include third file to test that it is omitted and
226 // deleted.
227 WriteToDisk("first", MakeTestingProto({111}));
228 WriteToDisk("second", MakeTestingProto({222}));
229 WriteToDisk("third", MakeTestingProto({333}));
230
231 CollectEvents();
232
233 // Number of events should be capped to the file limit since above records one
234 // event per file.
235 ASSERT_EQ(proto_.value().uma_events().size(), file_limit);
236
237 // And the directory should be empty too.
238 ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
239 }
240
TEST_F(ExternalMetricsTest,FilterDisallowedProjects)241 TEST_F(ExternalMetricsTest, FilterDisallowedProjects) {
242 Init();
243 external_metrics_->AddDisallowedProjectForTest(2);
244
245 // Add 3 events with a project of 1 and 2.
246 WriteToDisk("first", MakeTestingProto({111}, 1));
247 WriteToDisk("second", MakeTestingProto({222}, 2));
248 WriteToDisk("third", MakeTestingProto({333}, 1));
249
250 CollectEvents();
251
252 // The events at second should be filtered.
253 ASSERT_EQ(proto_.value().uma_events().size(), 2);
254
255 std::vector<int64_t> ids;
256 for (const auto& event : proto_.value().uma_events()) {
257 ids.push_back(event.profile_event_id());
258 }
259
260 // Validate that only project 1 remains.
261 ASSERT_THAT(ids, UnorderedElementsAre(111, 333));
262
263 // And the directory should be empty too.
264 ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
265 }
266
TEST_F(ExternalMetricsTest,DroppedEventsWhenDisabled)267 TEST_F(ExternalMetricsTest, DroppedEventsWhenDisabled) {
268 Init();
269 DisableRecording();
270
271 // Add 3 events with a project of 1 and 2.
272 WriteToDisk("first", MakeTestingProto({111}, 1));
273 WriteToDisk("second", MakeTestingProto({222}, 2));
274 WriteToDisk("third", MakeTestingProto({333}, 1));
275
276 CollectEvents();
277
278 // No events should have been collected.
279 ASSERT_EQ(proto_.value().uma_events().size(), 0);
280
281 // And the directory should be empty too.
282 ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
283 }
284
285 // TODO(crbug.com/40941078): Failing consistently on MSAN.
286 #if defined(MEMORY_SANITIZER)
287 #define MAYBE_ProducedAndDroppedEventMetricCollected \
288 DISABLED_ProducedAndDroppedEventMetricCollected
289 #else
290 #define MAYBE_ProducedAndDroppedEventMetricCollected \
291 ProducedAndDroppedEventMetricCollected
292 #endif
293
TEST_F(ExternalMetricsTest,MAYBE_ProducedAndDroppedEventMetricCollected)294 TEST_F(ExternalMetricsTest, MAYBE_ProducedAndDroppedEventMetricCollected) {
295 base::test::ScopedFeatureList feature_list;
296 const int file_limit = 5;
297 feature_list.InitAndEnableFeatureWithParameters(
298 features::kStructuredMetrics,
299 {{"file_limit", base::NumberToString(file_limit)}});
300
301 Init();
302
303 // Wifi
304 WriteToDisk("event1", MakeTestingProto({0}, UINT64_C(4320592646346933548)));
305 WriteToDisk("event2", MakeTestingProto({1}, UINT64_C(4320592646346933548)));
306 // Bluetooth
307 WriteToDisk("event3", MakeTestingProto({2}, UINT64_C(9074739597929991885)));
308 WriteToDisk("event4", MakeTestingProto({3}, UINT64_C(9074739597929991885)));
309 // Cellular
310 WriteToDisk("event5", MakeTestingProto({4}, UINT64_C(8206859287963243715)));
311 WriteToDisk("event6", MakeTestingProto({5}, UINT64_C(8206859287963243715)));
312 // WIfi
313 WriteToDisk("event7", MakeTestingProto({6}, UINT64_C(4320592646346933548)));
314 WriteToDisk("event8", MakeTestingProto({7}, UINT64_C(4320592646346933548)));
315 // Bluetooth
316 WriteToDisk("event9", MakeTestingProto({8}, UINT64_C(9074739597929991885)));
317 WriteToDisk("event10", MakeTestingProto({9}, UINT64_C(9074739597929991885)));
318
319 CollectEvents();
320
321 ASSERT_EQ(proto_.value().uma_events().size(), file_limit);
322
323 // Unable to guarantee the order the events are read in. Using counts to
324 // verify that the number of histograms produced are what is expected.
325 base::HistogramTester::CountsMap produced_map =
326 histogram_tester_.GetTotalCountsForPrefix(
327 "StructuredMetrics.ExternalMetricsProduced2.");
328 int produced_acc = 0;
329 for (const auto& hist : produced_map) {
330 produced_acc += hist.second;
331 }
332
333 base::HistogramTester::CountsMap dropped_map =
334 histogram_tester_.GetTotalCountsForPrefix(
335 "StructuredMetrics.ExternalMetricsDropped2.");
336
337 int dropped_acc = 0;
338 for (const auto& hist : dropped_map) {
339 dropped_acc += hist.second;
340 }
341
342 EXPECT_EQ(produced_acc, 3);
343 EXPECT_EQ(dropped_acc, 3);
344 }
345
346 // TODO(crbug.com/1148168): Add a test for concurrent reading and writing here
347 // once we know the specifics of how the lock in cros is performed.
348
349 } // namespace structured
350 } // namespace metrics
351