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 "components/metrics/structured/flushed_map.h"
6
7 #include <cstdint>
8 #include <optional>
9 #include <vector>
10
11 #include "base/files/file_path.h"
12 #include "base/files/file_util.h"
13 #include "base/files/scoped_temp_dir.h"
14 #include "base/run_loop.h"
15 #include "base/test/bind.h"
16 #include "base/test/task_environment.h"
17 #include "components/metrics/structured/lib/event_buffer.h"
18 #include "components/metrics/structured/proto/event_storage.pb.h"
19 #include "testing/gmock/include/gmock/gmock-matchers.h"
20 #include "testing/gtest/include/gtest/gtest.h"
21 #include "third_party/metrics_proto/structured_data.pb.h"
22
23 namespace metrics::structured {
24 namespace {
25
26 class TestEventBuffer : public EventBuffer<StructuredEventProto> {
27 public:
TestEventBuffer()28 TestEventBuffer() : EventBuffer<StructuredEventProto>(ResourceInfo(1024)) {}
29
30 // EventBuffer:
AddEvent(StructuredEventProto event)31 Result AddEvent(StructuredEventProto event) override {
32 events_.mutable_events()->Add(std::move(event));
33 return Result::kOk;
34 }
35
Purge()36 void Purge() override { events_.Clear(); }
37
Serialize()38 google::protobuf::RepeatedPtrField<StructuredEventProto> Serialize()
39 override {
40 return events_.events();
41 }
42
Flush(const base::FilePath & path,FlushedCallback callback)43 void Flush(const base::FilePath& path, FlushedCallback callback) override {
44 std::string content;
45 EXPECT_TRUE(events_.SerializeToString(&content));
46 EXPECT_TRUE(base::WriteFile(path, content));
47 std::move(callback).Run(FlushedKey{
48 .size = static_cast<int32_t>(content.size()),
49 .path = path,
50 .creation_time = base::Time::Now(),
51 });
52 }
53
Size()54 uint64_t Size() override { return events_.events_size(); }
55
events() const56 const EventsProto& events() const { return events_; }
57
58 private:
59 EventsProto events_;
60 };
61
BuildTestEvent(int id)62 StructuredEventProto BuildTestEvent(int id) {
63 StructuredEventProto event;
64 event.set_device_project_id(id);
65 return event;
66 }
67
BuildTestBuffer(const std::vector<int> & ids)68 TestEventBuffer BuildTestBuffer(const std::vector<int>& ids) {
69 TestEventBuffer buffer;
70
71 for (int id : ids) {
72 EXPECT_EQ(buffer.AddEvent(BuildTestEvent(id)), Result::kOk);
73 }
74
75 return buffer;
76 }
77
BuildTestEvents(const std::vector<int> & ids)78 EventsProto BuildTestEvents(const std::vector<int>& ids) {
79 EventsProto events;
80
81 for (int id : ids) {
82 events.mutable_events()->Add(BuildTestEvent(id));
83 }
84
85 return events;
86 }
87
88 } // namespace
89
90 class FlushedMapTest : public testing::Test {
91 public:
92 FlushedMapTest() = default;
93 ~FlushedMapTest() override = default;
94
SetUp()95 void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }
96
GetDir() const97 base::FilePath GetDir() const {
98 return temp_dir_.GetPath().Append(FILE_PATH_LITERAL("events"));
99 }
100
BuildFlushedMap(int32_t max_size=2048)101 FlushedMap BuildFlushedMap(int32_t max_size = 2048) {
102 return FlushedMap(GetDir(), max_size);
103 }
104
ReadEventsProto(const base::FilePath & path)105 EventsProto ReadEventsProto(const base::FilePath& path) {
106 std::string content;
107 EXPECT_TRUE(base::ReadFileToString(path, &content));
108
109 EventsProto events;
110 EXPECT_TRUE(events.MergeFromString(content));
111 return events;
112 }
113
WriteToDisk(const base::FilePath & path,EventsProto && events)114 void WriteToDisk(const base::FilePath& path, EventsProto&& events) {
115 std::string content;
116 EXPECT_TRUE(events.SerializeToString(&content));
117 EXPECT_TRUE(base::WriteFile(path, content));
118 }
119
Wait()120 void Wait() { task_environment_.RunUntilIdle(); }
121
122 private:
123 base::test::TaskEnvironment task_environment_{
124 base::test::TaskEnvironment::MainThreadType::UI,
125 base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED};
126
127 base::ScopedTempDir temp_dir_;
128
129 protected:
130 };
131
TEST_F(FlushedMapTest,FlushFile)132 TEST_F(FlushedMapTest, FlushFile) {
133 FlushedMap map = BuildFlushedMap();
134 Wait();
135 auto buffer = BuildTestBuffer({1, 2, 3});
136
137 map.Flush(buffer,
138 base::BindLambdaForTesting(
139 [&](base::expected<FlushedKey, FlushError> key) {
140 EXPECT_TRUE(key.has_value());
141 EXPECT_TRUE(base::PathExists(base::FilePath(key->path)));
142 }));
143 Wait();
144
145 const std::vector<FlushedKey>& keys = map.keys();
146 EXPECT_EQ(keys.size(), 1ul);
147
148 std::optional<int64_t> file_size = base::GetFileSize(keys[0].path);
149 ASSERT_TRUE(file_size.has_value());
150 EXPECT_EQ(keys[0].size, static_cast<int32_t>(file_size.value()));
151 }
152
TEST_F(FlushedMapTest,ReadFile)153 TEST_F(FlushedMapTest, ReadFile) {
154 FlushedMap map = BuildFlushedMap();
155 Wait();
156
157 auto buffer = BuildTestBuffer({1, 2, 3});
158 const EventsProto& events = buffer.events();
159
160 map.Flush(buffer,
161 base::BindLambdaForTesting(
162 [&](base::expected<FlushedKey, FlushError> key) {
163 EXPECT_TRUE(key.has_value());
164 EXPECT_TRUE(base::PathExists(base::FilePath(key->path)));
165 }));
166 Wait();
167
168 auto key = map.keys().front();
169
170 std::optional<EventsProto> read_events = map.ReadKey(key);
171 EXPECT_EQ(read_events->events_size(), events.events_size());
172
173 for (int i = 0; i < read_events->events_size(); ++i) {
174 EXPECT_EQ(read_events->events(i).device_project_id(),
175 events.events(i).device_project_id());
176 }
177 }
178
TEST_F(FlushedMapTest,UniqueFlushes)179 TEST_F(FlushedMapTest, UniqueFlushes) {
180 FlushedMap map = BuildFlushedMap();
181 Wait();
182
183 auto events = BuildTestBuffer({1, 2, 3});
184 map.Flush(events,
185 base::BindLambdaForTesting(
186 [&](base::expected<FlushedKey, FlushError> key) {
187 EXPECT_TRUE(key.has_value());
188 EXPECT_TRUE(base::PathExists(base::FilePath(key->path)));
189 }));
190 Wait();
191
192 auto events2 = BuildTestBuffer({4, 5, 6});
193 map.Flush(events2,
194 base::BindLambdaForTesting(
195 [&](base::expected<FlushedKey, FlushError> key) {
196 EXPECT_TRUE(key.has_value());
197 EXPECT_TRUE(base::PathExists(base::FilePath(key->path)));
198 }));
199 Wait();
200
201 EXPECT_EQ(map.keys().size(), 2ul);
202 const auto& key1 = map.keys()[0];
203 const auto& key2 = map.keys()[1];
204
205 EXPECT_NE(key1.path, key2.path);
206 }
207
TEST_F(FlushedMapTest,DeleteKey)208 TEST_F(FlushedMapTest, DeleteKey) {
209 FlushedMap map = BuildFlushedMap();
210 Wait();
211
212 auto events = BuildTestBuffer({1, 2, 3});
213 map.Flush(events, base::BindLambdaForTesting(
214 [&](base::expected<FlushedKey, FlushError> key) {
215 EXPECT_TRUE(key.has_value());
216 EXPECT_TRUE(base::PathExists(key->path));
217 }));
218 Wait();
219
220 const std::vector<FlushedKey>& keys = map.keys();
221 auto key = map.keys().front();
222 EXPECT_EQ(keys.size(), 1ul);
223 EXPECT_TRUE(base::PathExists(key.path));
224
225 map.DeleteKey(key);
226 Wait();
227
228 EXPECT_FALSE(base::PathExists(key.path));
229 }
230
TEST_F(FlushedMapTest,LoadPreviousSessionKeys)231 TEST_F(FlushedMapTest, LoadPreviousSessionKeys) {
232 EXPECT_TRUE(base::CreateDirectory(GetDir()));
233 auto events = BuildTestEvents({1, 2, 3});
234 auto events2 = BuildTestEvents({4, 5, 6});
235
236 base::FilePath path1 = GetDir().Append(FILE_PATH_LITERAL("events"));
237 base::FilePath path2 = GetDir().Append(FILE_PATH_LITERAL("events2"));
238
239 WriteToDisk(path1, std::move(events));
240 // Force a small difference in the creation time of the two files.
241 base::PlatformThreadBase::Sleep(base::Seconds(1));
242 WriteToDisk(path2, std::move(events2));
243
244 FlushedMap map = BuildFlushedMap();
245 Wait();
246
247 const std::vector<FlushedKey>& keys = map.keys();
248 EXPECT_EQ(keys.size(), 2ul);
249
250 // The order the files are loaded in is unknown.
251 std::vector<base::FilePath> paths;
252 for (const auto& key : keys) {
253 paths.push_back(base::FilePath(key.path));
254 }
255
256 EXPECT_THAT(paths, testing::ElementsAre(path1, path2));
257 }
258
TEST_F(FlushedMapTest,ExceedQuota)259 TEST_F(FlushedMapTest, ExceedQuota) {
260 FlushedMap map = BuildFlushedMap(/*max_size=*/64);
261 Wait();
262
263 auto events = BuildTestBuffer({1, 2, 3});
264 map.Flush(events, base::BindLambdaForTesting(
265 [&](base::expected<FlushedKey, FlushError> key) {
266 EXPECT_TRUE(key.has_value());
267 EXPECT_TRUE(base::PathExists(key->path));
268 }));
269 Wait();
270
271 auto events2 = BuildTestBuffer({1, 2, 3});
272 map.Flush(events2, base::BindLambdaForTesting(
273 [&](base::expected<FlushedKey, FlushError> key) {
274 EXPECT_FALSE(key.has_value());
275 const FlushError err = key.error();
276 EXPECT_EQ(err, kQuotaExceeded);
277 }));
278 Wait();
279 }
280
281 } // namespace metrics::structured
282