xref: /aosp_15_r20/external/cronet/components/metrics/structured/external_metrics_unittest.cc (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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