• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 #include "components/metrics/structured/external_metrics.h"
5 
6 #include <memory>
7 #include <numeric>
8 #include <string>
9 
10 #include "base/files/file_util.h"
11 #include "base/files/scoped_temp_dir.h"
12 #include "base/logging.h"
13 #include "base/strings/string_number_conversions.h"
14 #include "base/test/metrics/histogram_tester.h"
15 #include "base/test/scoped_feature_list.h"
16 #include "base/test/task_environment.h"
17 #include "build/build_config.h"
18 #include "components/metrics/structured/histogram_util.h"
19 #include "components/metrics/structured/proto/event_storage.pb.h"
20 #include "components/metrics/structured/structured_metrics_features.h"
21 #include "testing/gmock/include/gmock/gmock.h"
22 #include "testing/gtest/include/gtest/gtest.h"
23 
24 namespace metrics::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 { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }
64 
Init()65   void Init() {
66     // We don't use the scheduling feature when testing ExternalMetrics, instead
67     // we just call CollectMetrics directly. So make up a time interval here
68     // that we'll never reach in a test.
69     const auto one_hour = base::Hours(1);
70     external_metrics_ = std::make_unique<ExternalMetrics>(
71         temp_dir_.GetPath(), one_hour,
72         base::BindRepeating(&ExternalMetricsTest::OnEventsCollected,
73                             base::Unretained(this)));
74 
75     // For most tests the recording needs to be enabled.
76     EnableRecording();
77   }
78 
EnableRecording()79   void EnableRecording() { external_metrics_->EnableRecording(); }
80 
DisableRecording()81   void DisableRecording() { external_metrics_->DisableRecording(); }
82 
CollectEvents()83   void CollectEvents() {
84     external_metrics_->CollectEvents();
85     Wait();
86     CHECK(proto_.has_value());
87   }
88 
OnEventsCollected(const EventsProto & proto)89   void OnEventsCollected(const EventsProto& proto) {
90     proto_ = std::move(proto);
91   }
92 
WriteToDisk(const std::string & name,const EventsProto & proto)93   void WriteToDisk(const std::string& name, const EventsProto& proto) {
94     CHECK(base::WriteFile(temp_dir_.GetPath().Append(name),
95                           proto.SerializeAsString()));
96   }
97 
WriteToDisk(const std::string & name,const std::string & str)98   void WriteToDisk(const std::string& name, const std::string& str) {
99     CHECK(base::WriteFile(temp_dir_.GetPath().Append(name), str));
100   }
101 
Wait()102   void Wait() { task_environment_.RunUntilIdle(); }
103 
104   base::ScopedTempDir temp_dir_;
105   std::unique_ptr<ExternalMetrics> external_metrics_;
106   std::optional<EventsProto> proto_;
107 
108   base::test::TaskEnvironment task_environment_{
109       base::test::TaskEnvironment::MainThreadType::UI,
110       base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED};
111   base::HistogramTester histogram_tester_;
112 };
113 
TEST_F(ExternalMetricsTest,ReadOneFile)114 TEST_F(ExternalMetricsTest, ReadOneFile) {
115   // Make one proto with three events.
116   WriteToDisk("myproto", MakeTestingProto({111, 222, 333}));
117   Init();
118 
119   CollectEvents();
120 
121   // We should have correctly picked up the three events.
122   AssertEqualsTestingProto(proto_.value(), {111, 222, 333});
123   // And the directory should now be empty.
124   ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
125 }
126 
TEST_F(ExternalMetricsTest,ReadManyFiles)127 TEST_F(ExternalMetricsTest, ReadManyFiles) {
128   // Make three protos with three events each.
129   WriteToDisk("first", MakeTestingProto({111, 222, 333}));
130   WriteToDisk("second", MakeTestingProto({444, 555, 666}));
131   WriteToDisk("third", MakeTestingProto({777, 888, 999}));
132   Init();
133 
134   CollectEvents();
135 
136   // We should have correctly picked up the nine events. Don't check for order,
137   // because we can't guarantee the files will be read from disk in any
138   // particular order.
139   std::vector<int64_t> ids;
140   for (const auto& event : proto_.value().uma_events()) {
141     ids.push_back(event.profile_event_id());
142   }
143   ASSERT_THAT(
144       ids, UnorderedElementsAre(111, 222, 333, 444, 555, 666, 777, 888, 999));
145 
146   // The directory should be empty after reading.
147   ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
148 }
149 
TEST_F(ExternalMetricsTest,ReadZeroFiles)150 TEST_F(ExternalMetricsTest, ReadZeroFiles) {
151   Init();
152   CollectEvents();
153   // We should have an empty proto.
154   AssertEqualsTestingProto(proto_.value(), {});
155   // And the directory should be empty too.
156   ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
157 }
158 
TEST_F(ExternalMetricsTest,CollectTwice)159 TEST_F(ExternalMetricsTest, CollectTwice) {
160   Init();
161   WriteToDisk("first", MakeTestingProto({111, 222, 333}));
162   CollectEvents();
163   AssertEqualsTestingProto(proto_.value(), {111, 222, 333});
164 
165   WriteToDisk("first", MakeTestingProto({444}));
166   CollectEvents();
167   AssertEqualsTestingProto(proto_.value(), {444});
168 }
169 
TEST_F(ExternalMetricsTest,HandleCorruptFile)170 TEST_F(ExternalMetricsTest, HandleCorruptFile) {
171   Init();
172 
173   WriteToDisk("invalid", "surprise i'm not a proto");
174   WriteToDisk("valid", MakeTestingProto({111, 222, 333}));
175 
176   CollectEvents();
177   AssertEqualsTestingProto(proto_.value(), {111, 222, 333});
178   // Should have deleted the invalid file too.
179   ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
180 }
181 
TEST_F(ExternalMetricsTest,FileNumberReadCappedAndDiscarded)182 TEST_F(ExternalMetricsTest, FileNumberReadCappedAndDiscarded) {
183   // Setup feature.
184   base::test::ScopedFeatureList feature_list;
185   const int file_limit = 2;
186   feature_list.InitAndEnableFeatureWithParameters(
187       features::kStructuredMetrics,
188       {{"file_limit", base::NumberToString(file_limit)}});
189 
190   Init();
191 
192   // File limit is set to 2. Include third file to test that it is omitted and
193   // deleted.
194   WriteToDisk("first", MakeTestingProto({111}));
195   WriteToDisk("second", MakeTestingProto({222}));
196   WriteToDisk("third", MakeTestingProto({333}));
197 
198   CollectEvents();
199 
200   // Number of events should be capped to the file limit since above records one
201   // event per file.
202   ASSERT_EQ(proto_.value().uma_events().size(), file_limit);
203 
204   // And the directory should be empty too.
205   ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
206 }
207 
TEST_F(ExternalMetricsTest,FilterDisallowedProjects)208 TEST_F(ExternalMetricsTest, FilterDisallowedProjects) {
209   Init();
210   external_metrics_->AddDisallowedProjectForTest(2);
211 
212   // Add 3 events with a project of 1 and 2.
213   WriteToDisk("first", MakeTestingProto({111}, 1));
214   WriteToDisk("second", MakeTestingProto({222}, 2));
215   WriteToDisk("third", MakeTestingProto({333}, 1));
216 
217   CollectEvents();
218 
219   // The events at second should be filtered.
220   ASSERT_EQ(proto_.value().uma_events().size(), 2);
221 
222   std::vector<int64_t> ids;
223   for (const auto& event : proto_.value().uma_events()) {
224     ids.push_back(event.profile_event_id());
225   }
226 
227   // Validate that only project 1 remains.
228   ASSERT_THAT(ids, UnorderedElementsAre(111, 333));
229 
230   // And the directory should be empty too.
231   ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
232 }
233 
TEST_F(ExternalMetricsTest,DroppedEventsWhenDisabled)234 TEST_F(ExternalMetricsTest, DroppedEventsWhenDisabled) {
235   Init();
236   DisableRecording();
237 
238   // Add 3 events with a project of 1 and 2.
239   WriteToDisk("first", MakeTestingProto({111}, 1));
240   WriteToDisk("second", MakeTestingProto({222}, 2));
241   WriteToDisk("third", MakeTestingProto({333}, 1));
242 
243   CollectEvents();
244 
245   // No events should have been collected.
246   ASSERT_EQ(proto_.value().uma_events().size(), 0);
247 
248   // And the directory should be empty too.
249   ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
250 }
251 
TEST_F(ExternalMetricsTest,ProducedAndDroppedEventMetricCollected)252 TEST_F(ExternalMetricsTest, ProducedAndDroppedEventMetricCollected) {
253   base::test::ScopedFeatureList feature_list;
254   const int file_limit = 5;
255   feature_list.InitAndEnableFeatureWithParameters(
256       features::kStructuredMetrics,
257       {{"file_limit", base::NumberToString(file_limit)}});
258 
259   Init();
260 
261   // Generate 9 events.
262   WriteToDisk("event0", MakeTestingProto({0}, UINT64_C(4320592646346933548)));
263   WriteToDisk("event1", MakeTestingProto({1}, UINT64_C(4320592646346933548)));
264   WriteToDisk("event2", MakeTestingProto({2}, UINT64_C(4320592646346933548)));
265   WriteToDisk("event3", MakeTestingProto({3}, UINT64_C(4320592646346933548)));
266   WriteToDisk("event4", MakeTestingProto({4}, UINT64_C(4320592646346933548)));
267   WriteToDisk("event5", MakeTestingProto({5}, UINT64_C(4320592646346933548)));
268   WriteToDisk("event6", MakeTestingProto({6}, UINT64_C(4320592646346933548)));
269   WriteToDisk("event7", MakeTestingProto({7}, UINT64_C(4320592646346933548)));
270   WriteToDisk("event8", MakeTestingProto({8}, UINT64_C(4320592646346933548)));
271 
272   CollectEvents();
273 
274   // There should be 9 files processed and 4 events dropped. We analyze the
275   // histograms to verify this.
276   EXPECT_EQ(histogram_tester_.GetTotalSum(
277                 std::string(kExternalMetricsProducedHistogramPrefix) + "WiFi"),
278             9);
279   EXPECT_EQ(histogram_tester_.GetTotalSum(
280                 std::string(kExternalMetricsDroppedHistogramPrefix) + "WiFi"),
281             4);
282 
283   // There should |file_limit| events. The rest should have been dropped.
284   ASSERT_EQ(proto_.value().uma_events().size(), file_limit);
285 
286   // The directory should be empty.
287   ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
288 }
289 
290 // TODO(crbug.com/40156926): Add a test for concurrent reading and writing here
291 // once we know the specifics of how the lock in cros is performed.
292 
293 }  // namespace metrics::structured
294