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