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 <memory>
6 #include <string>
7
8 #include "base/files/file_path.h"
9 #include "base/files/file_util.h"
10 #include "base/files/scoped_temp_dir.h"
11 #include "base/logging.h"
12 #include "base/test/task_environment.h"
13 #include "base/time/time.h"
14 #include "components/metrics/structured/lib/arena_persistent_proto.h"
15 #include "components/metrics/structured/lib/persistent_proto.h"
16 #include "components/metrics/structured/lib/persistent_proto_internal.h"
17 #include "components/metrics/structured/lib/proto/key.pb.h"
18 #include "testing/gtest/include/gtest/gtest.h"
19
20 namespace metrics::structured {
21 namespace {
22
23 // Populate |proto| with some test data.
PopulateTestProto(KeyProto * proto)24 void PopulateTestProto(KeyProto* proto) {
25 proto->set_key("abcdefghijkl");
26 proto->set_last_rotation(12345);
27 proto->set_rotation_period(54321);
28 }
29
30 // Make a proto with test data.
MakeTestProto()31 KeyProto MakeTestProto() {
32 KeyProto proto;
33 PopulateTestProto(&proto);
34 return proto;
35 }
36
37 // Returns whether |actual| and |expected| are equal.
ProtoEquals(const KeyProto * actual,const KeyProto * expected)38 bool ProtoEquals(const KeyProto* actual, const KeyProto* expected) {
39 bool equal = true;
40 equal &= actual->key() == expected->key();
41 equal &= actual->last_rotation() == expected->last_rotation();
42 equal &= actual->rotation_period() == expected->rotation_period();
43 return equal;
44 }
45
WriteDelay()46 base::TimeDelta WriteDelay() {
47 return base::Seconds(0);
48 }
49
50 template <typename T>
51 class TestCase {
52 public:
53 using PProtoType = T;
54
TestCase()55 TestCase() { Setup(); }
56 TestCase(const TestCase&) = delete;
57 ~TestCase() = default;
58
Setup()59 void Setup() { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }
60
GetPath()61 base::FilePath GetPath() {
62 return temp_dir_.GetPath().Append(FILE_PATH_LITERAL("proto"));
63 }
64
ClearDisk()65 void ClearDisk() {
66 base::DeleteFile(GetPath());
67 ASSERT_FALSE(base::PathExists(GetPath()));
68 }
69
70 // Read the file at GetPath and parse it as a KeyProto.
ReadFromDisk()71 KeyProto ReadFromDisk() {
72 std::string proto_str;
73 CHECK(base::ReadFileToString(GetPath(), &proto_str));
74 KeyProto proto;
75 CHECK(proto.ParseFromString(proto_str));
76 return proto;
77 }
78
WriteToDisk(const KeyProto & proto)79 void WriteToDisk(const KeyProto& proto) { WriteToDisk(GetPath(), proto); }
80
WriteToDisk(const base::FilePath & path,const KeyProto & proto)81 void WriteToDisk(const base::FilePath& path, const KeyProto& proto) {
82 ASSERT_TRUE(base::WriteFile(path, proto.SerializeAsString()));
83 }
84
OnRead(const ReadStatus status)85 void OnRead(const ReadStatus status) {
86 read_status_ = status;
87 ++read_count_;
88 }
89
ReadCallback()90 base::OnceCallback<void(ReadStatus)> ReadCallback() {
91 return base::BindOnce(&TestCase::OnRead, base::Unretained(this));
92 }
93
OnWrite(const WriteStatus status)94 void OnWrite(const WriteStatus status) {
95 ASSERT_EQ(status, WriteStatus::kOk);
96 ++write_count_;
97 }
98
WriteCallback()99 base::RepeatingCallback<void(WriteStatus)> WriteCallback() {
100 return base::BindRepeating(&TestCase::OnWrite, base::Unretained(this));
101 }
102
103 // Constructs the proto of type T.
104 T BuildTestProto();
105
106 // Records the information passed to the callbacks for later expectation.
107 ReadStatus read_status_;
108 int read_count_ = 0;
109 int write_count_ = 0;
110 base::ScopedTempDir temp_dir_;
111 };
112
113 template <typename T>
BuildTestProto()114 T TestCase<T>::BuildTestProto() {
115 ASSERT_TRUE(false)
116 << "Invalid type parameter, please implement BuildTestProto for T";
117 }
118
119 template <>
120 PersistentProto<KeyProto>
BuildTestProto()121 TestCase<PersistentProto<KeyProto>>::BuildTestProto() {
122 return PersistentProto<KeyProto>(GetPath(), WriteDelay(), ReadCallback(),
123 WriteCallback());
124 }
125
126 template <>
127 ArenaPersistentProto<KeyProto>
BuildTestProto()128 TestCase<ArenaPersistentProto<KeyProto>>::BuildTestProto() {
129 return ArenaPersistentProto<KeyProto>(GetPath(), WriteDelay(), ReadCallback(),
130 WriteCallback());
131 }
132
133 } // namespace
134
135 // Testing suite for any class that is a persistent proto. This is a series of
136 // tests needed by any PersistentProtoInternal implementation. Currently this
137 // includes: PersistentProto and ArenaPersistentProto.
138 template <typename T>
139 class PersistentProtoTest : public testing::Test {
140 public:
Wait()141 void Wait() { task_environment_.RunUntilIdle(); }
142
BuildTestProto()143 T BuildTestProto() { return test_.BuildTestProto(); }
144
145 base::test::TaskEnvironment task_environment_{
146 base::test::TaskEnvironment::MainThreadType::UI,
147 base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED};
148
149 TestCase<T> test_;
150 };
151
152 using Implementations =
153 testing::Types<PersistentProto<KeyProto>, ArenaPersistentProto<KeyProto>>;
154 TYPED_TEST_SUITE(PersistentProtoTest, Implementations);
155
156 // Test that the underlying proto is nullptr until a read is complete, and isn't
157 // after that.
TYPED_TEST(PersistentProtoTest,Initialization)158 TYPED_TEST(PersistentProtoTest, Initialization) {
159 auto pproto = this->BuildTestProto();
160
161 EXPECT_EQ(pproto.get(), nullptr);
162 this->Wait();
163 EXPECT_NE(pproto.get(), nullptr);
164 }
165
166 // Test bool conversion and has_value.
TYPED_TEST(PersistentProtoTest,BoolTests)167 TYPED_TEST(PersistentProtoTest, BoolTests) {
168 auto pproto = this->BuildTestProto();
169 EXPECT_EQ(pproto.get(), nullptr);
170 EXPECT_FALSE(pproto);
171 EXPECT_FALSE(pproto.has_value());
172 this->Wait();
173 EXPECT_NE(pproto.get(), nullptr);
174 EXPECT_TRUE(pproto);
175 EXPECT_TRUE(pproto.has_value());
176 }
177
178 // Test -> and *.
TYPED_TEST(PersistentProtoTest,Getters)179 TYPED_TEST(PersistentProtoTest, Getters) {
180 auto pproto = this->BuildTestProto();
181 this->Wait();
182 // We're really just checking these don't crash.
183 EXPECT_EQ(pproto->last_rotation(), 0);
184 KeyProto val = *pproto;
185 }
186
187 // Test that the pproto correctly saves the in-memory proto to disk.
TYPED_TEST(PersistentProtoTest,Read)188 TYPED_TEST(PersistentProtoTest, Read) {
189 auto pproto = this->BuildTestProto();
190
191 // Underlying proto should be nullptr until read is complete.
192 EXPECT_EQ(pproto.get(), nullptr);
193
194 this->Wait();
195 EXPECT_EQ(this->test_.read_status_, ReadStatus::kMissing);
196 EXPECT_EQ(this->test_.read_count_, 1);
197 EXPECT_EQ(this->test_.write_count_, 1);
198
199 PopulateTestProto(pproto.get());
200 pproto.StartWriteForTesting();
201 this->Wait();
202 EXPECT_EQ(this->test_.write_count_, 2);
203
204 KeyProto written = this->test_.ReadFromDisk();
205 EXPECT_TRUE(ProtoEquals(&written, pproto.get()));
206 }
207
208 // Test that invalid files on disk are handled correctly.
TYPED_TEST(PersistentProtoTest,ReadInvalidProto)209 TYPED_TEST(PersistentProtoTest, ReadInvalidProto) {
210 ASSERT_TRUE(
211 base::WriteFile(this->test_.GetPath(), "this isn't a valid proto"));
212
213 auto pproto = this->BuildTestProto();
214
215 this->Wait();
216 EXPECT_EQ(this->test_.read_status_, ReadStatus::kParseError);
217 EXPECT_EQ(this->test_.read_count_, 1);
218 EXPECT_EQ(this->test_.write_count_, 1);
219 }
220
221 // Test that the pproto correctly loads an on-disk proto into memory.
TYPED_TEST(PersistentProtoTest,Write)222 TYPED_TEST(PersistentProtoTest, Write) {
223 const auto test_proto = MakeTestProto();
224 this->test_.WriteToDisk(test_proto);
225
226 auto pproto = this->BuildTestProto();
227
228 EXPECT_EQ(pproto.get(), nullptr);
229
230 this->Wait();
231 EXPECT_EQ(this->test_.read_status_, ReadStatus::kOk);
232 EXPECT_EQ(this->test_.read_count_, 1);
233 EXPECT_EQ(this->test_.write_count_, 0);
234 EXPECT_NE(pproto.get(), nullptr);
235 EXPECT_TRUE(ProtoEquals(pproto.get(), &test_proto));
236 }
237
238 // Test that several saves all happen correctly.
TYPED_TEST(PersistentProtoTest,MultipleWrites)239 TYPED_TEST(PersistentProtoTest, MultipleWrites) {
240 auto pproto = this->BuildTestProto();
241
242 EXPECT_EQ(pproto.get(), nullptr);
243
244 this->Wait();
245 EXPECT_EQ(this->test_.write_count_, 1);
246
247 for (int i = 1; i <= 10; ++i) {
248 pproto.get()->set_last_rotation(i * i);
249 pproto.StartWriteForTesting();
250 this->Wait();
251 EXPECT_EQ(this->test_.write_count_, i + 1);
252
253 KeyProto written = this->test_.ReadFromDisk();
254 ASSERT_EQ(written.last_rotation(), i * i);
255 }
256 }
257
258 // Test that many calls to QueueWrite get batched, leading to only one real
259 // write.
TYPED_TEST(PersistentProtoTest,QueueWrites)260 TYPED_TEST(PersistentProtoTest, QueueWrites) {
261 auto pproto = this->BuildTestProto();
262
263 this->Wait();
264 EXPECT_EQ(this->test_.write_count_, 1);
265
266 // Three successive StartWrite calls result in three writes.
267 this->test_.write_count_ = 0;
268 for (int i = 0; i < 3; ++i) {
269 pproto.StartWriteForTesting();
270 }
271 this->Wait();
272 EXPECT_EQ(this->test_.write_count_, 3);
273
274 // Three successive QueueWrite calls results in one write.
275 this->test_.write_count_ = 0;
276 for (int i = 0; i < 3; ++i) {
277 pproto.QueueWrite();
278 }
279 this->Wait();
280 EXPECT_EQ(this->test_.write_count_, 1);
281 }
282
TYPED_TEST(PersistentProtoTest,ClearContents)283 TYPED_TEST(PersistentProtoTest, ClearContents) {
284 const auto test_proto = MakeTestProto();
285 this->test_.WriteToDisk(test_proto);
286
287 {
288 auto pproto = this->BuildTestProto();
289
290 EXPECT_EQ(pproto.get(), nullptr);
291
292 this->Wait();
293 EXPECT_EQ(this->test_.read_status_, ReadStatus::kOk);
294 EXPECT_EQ(this->test_.read_count_, 1);
295 EXPECT_EQ(this->test_.write_count_, 0);
296
297 pproto->Clear();
298 pproto.QueueWrite();
299 }
300
301 this->Wait();
302
303 std::string empty_proto;
304 KeyProto().SerializeToString(&empty_proto);
305
306 std::optional<int64_t> size = base::GetFileSize(this->test_.GetPath());
307 ASSERT_TRUE(size.has_value());
308 EXPECT_EQ(size.value(), static_cast<int64_t>(empty_proto.size()));
309 }
310
TYPED_TEST(PersistentProtoTest,UpdatePath)311 TYPED_TEST(PersistentProtoTest, UpdatePath) {
312 const base::FilePath new_path =
313 this->test_.temp_dir_.GetPath().Append(FILE_PATH_LITERAL("new_proto"));
314 const int64_t kNewLastRotation = 10;
315
316 const auto test_proto = MakeTestProto();
317 this->test_.WriteToDisk(test_proto);
318
319 auto test_proto2 = MakeTestProto();
320 test_proto2.set_last_rotation(kNewLastRotation);
321 this->test_.WriteToDisk(new_path, test_proto2);
322
323 auto pproto = this->BuildTestProto();
324
325 // Underlying proto should be nullptr until read is complete.
326 EXPECT_EQ(pproto.get(), nullptr);
327
328 this->Wait();
329 EXPECT_EQ(this->test_.read_status_, ReadStatus::kOk);
330 EXPECT_EQ(this->test_.read_count_, 1);
331 EXPECT_EQ(this->test_.write_count_, 0);
332
333 const KeyProto* ptr = pproto.get();
334
335 pproto.UpdatePath(new_path, this->test_.ReadCallback(),
336 /*remove_existing=*/true);
337 this->Wait();
338
339 EXPECT_EQ(this->test_.read_status_, ReadStatus::kOk);
340 EXPECT_EQ(this->test_.read_count_, 2);
341 EXPECT_EQ(this->test_.write_count_, 1);
342
343 // It is expected that the underlying proto doesn't change.
344 EXPECT_EQ(ptr, pproto.get());
345
346 // Check the content of the updated proto.
347 EXPECT_EQ(ptr->key(), test_proto.key());
348 EXPECT_EQ(ptr->rotation_period(), test_proto.rotation_period());
349 EXPECT_EQ(ptr->last_rotation(), kNewLastRotation);
350
351 // Check the state of the files are what we expect.
352 ASSERT_FALSE(base::PathExists(this->test_.GetPath()));
353 ASSERT_TRUE(base::PathExists(new_path));
354 }
355
356 } // namespace metrics::structured
357