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 // Arena instance for ArenaPersistentProto test cases.
113 google::protobuf::Arena arena_;
114 };
115
116 template <typename T>
BuildTestProto()117 T TestCase<T>::BuildTestProto() {
118 ASSERT_TRUE(false)
119 << "Invalid type parameter, please implement BuildTestProto for T";
120 }
121
122 template <>
123 PersistentProto<KeyProto>
BuildTestProto()124 TestCase<PersistentProto<KeyProto>>::BuildTestProto() {
125 return PersistentProto<KeyProto>(GetPath(), WriteDelay(), ReadCallback(),
126 WriteCallback());
127 }
128
129 template <>
130 ArenaPersistentProto<KeyProto>
BuildTestProto()131 TestCase<ArenaPersistentProto<KeyProto>>::BuildTestProto() {
132 return ArenaPersistentProto<KeyProto>(&arena_, GetPath(), WriteDelay(),
133 ReadCallback(), WriteCallback());
134 }
135
136 } // namespace
137
138 // Testing suite for any class that is a persistent proto. This is a series of
139 // tests needed by any PersistentProtoInternal implementation. Currently this
140 // includes: PersistentProto and ArenaPersistentProto.
141 template <typename T>
142 class PersistentProtoTest : public testing::Test {
143 public:
Wait()144 void Wait() { task_environment_.RunUntilIdle(); }
145
BuildTestProto()146 T BuildTestProto() { return test_.BuildTestProto(); }
147
148 base::test::TaskEnvironment task_environment_{
149 base::test::TaskEnvironment::MainThreadType::UI,
150 base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED};
151
152 TestCase<T> test_;
153 };
154
155 using Implementations =
156 testing::Types<PersistentProto<KeyProto>, ArenaPersistentProto<KeyProto>>;
157 TYPED_TEST_SUITE(PersistentProtoTest, Implementations);
158
159 // Test that the underlying proto is nullptr until a read is complete, and isn't
160 // after that.
TYPED_TEST(PersistentProtoTest,Initialization)161 TYPED_TEST(PersistentProtoTest, Initialization) {
162 auto pproto = this->BuildTestProto();
163
164 EXPECT_EQ(pproto.get(), nullptr);
165 this->Wait();
166 EXPECT_NE(pproto.get(), nullptr);
167 }
168
169 // Test bool conversion and has_value.
TYPED_TEST(PersistentProtoTest,BoolTests)170 TYPED_TEST(PersistentProtoTest, BoolTests) {
171 auto pproto = this->BuildTestProto();
172 EXPECT_EQ(pproto.get(), nullptr);
173 EXPECT_FALSE(pproto);
174 EXPECT_FALSE(pproto.has_value());
175 this->Wait();
176 EXPECT_NE(pproto.get(), nullptr);
177 EXPECT_TRUE(pproto);
178 EXPECT_TRUE(pproto.has_value());
179 }
180
181 // Test -> and *.
TYPED_TEST(PersistentProtoTest,Getters)182 TYPED_TEST(PersistentProtoTest, Getters) {
183 auto pproto = this->BuildTestProto();
184 this->Wait();
185 // We're really just checking these don't crash.
186 EXPECT_EQ(pproto->last_rotation(), 0);
187 KeyProto val = *pproto;
188 }
189
190 // Test that the pproto correctly saves the in-memory proto to disk.
TYPED_TEST(PersistentProtoTest,Read)191 TYPED_TEST(PersistentProtoTest, Read) {
192 auto pproto = this->BuildTestProto();
193
194 // Underlying proto should be nullptr until read is complete.
195 EXPECT_EQ(pproto.get(), nullptr);
196
197 this->Wait();
198 EXPECT_EQ(this->test_.read_status_, ReadStatus::kMissing);
199 EXPECT_EQ(this->test_.read_count_, 1);
200 EXPECT_EQ(this->test_.write_count_, 1);
201
202 PopulateTestProto(pproto.get());
203 pproto.StartWriteForTesting();
204 this->Wait();
205 EXPECT_EQ(this->test_.write_count_, 2);
206
207 KeyProto written = this->test_.ReadFromDisk();
208 EXPECT_TRUE(ProtoEquals(&written, pproto.get()));
209 }
210
211 // Test that invalid files on disk are handled correctly.
TYPED_TEST(PersistentProtoTest,ReadInvalidProto)212 TYPED_TEST(PersistentProtoTest, ReadInvalidProto) {
213 ASSERT_TRUE(
214 base::WriteFile(this->test_.GetPath(), "this isn't a valid proto"));
215
216 auto pproto = this->BuildTestProto();
217
218 this->Wait();
219 EXPECT_EQ(this->test_.read_status_, ReadStatus::kParseError);
220 EXPECT_EQ(this->test_.read_count_, 1);
221 EXPECT_EQ(this->test_.write_count_, 1);
222 }
223
224 // Test that the pproto correctly loads an on-disk proto into memory.
TYPED_TEST(PersistentProtoTest,Write)225 TYPED_TEST(PersistentProtoTest, Write) {
226 const auto test_proto = MakeTestProto();
227 this->test_.WriteToDisk(test_proto);
228
229 auto pproto = this->BuildTestProto();
230
231 EXPECT_EQ(pproto.get(), nullptr);
232
233 this->Wait();
234 EXPECT_EQ(this->test_.read_status_, ReadStatus::kOk);
235 EXPECT_EQ(this->test_.read_count_, 1);
236 EXPECT_EQ(this->test_.write_count_, 0);
237 EXPECT_NE(pproto.get(), nullptr);
238 EXPECT_TRUE(ProtoEquals(pproto.get(), &test_proto));
239 }
240
241 // Test that several saves all happen correctly.
TYPED_TEST(PersistentProtoTest,MultipleWrites)242 TYPED_TEST(PersistentProtoTest, MultipleWrites) {
243 auto pproto = this->BuildTestProto();
244
245 EXPECT_EQ(pproto.get(), nullptr);
246
247 this->Wait();
248 EXPECT_EQ(this->test_.write_count_, 1);
249
250 for (int i = 1; i <= 10; ++i) {
251 pproto.get()->set_last_rotation(i * i);
252 pproto.StartWriteForTesting();
253 this->Wait();
254 EXPECT_EQ(this->test_.write_count_, i + 1);
255
256 KeyProto written = this->test_.ReadFromDisk();
257 ASSERT_EQ(written.last_rotation(), i * i);
258 }
259 }
260
261 // Test that many calls to QueueWrite get batched, leading to only one real
262 // write.
TYPED_TEST(PersistentProtoTest,QueueWrites)263 TYPED_TEST(PersistentProtoTest, QueueWrites) {
264 auto pproto = this->BuildTestProto();
265
266 this->Wait();
267 EXPECT_EQ(this->test_.write_count_, 1);
268
269 // Three successive StartWrite calls result in three writes.
270 this->test_.write_count_ = 0;
271 for (int i = 0; i < 3; ++i) {
272 pproto.StartWriteForTesting();
273 }
274 this->Wait();
275 EXPECT_EQ(this->test_.write_count_, 3);
276
277 // Three successive QueueWrite calls results in one write.
278 this->test_.write_count_ = 0;
279 for (int i = 0; i < 3; ++i) {
280 pproto.QueueWrite();
281 }
282 this->Wait();
283 EXPECT_EQ(this->test_.write_count_, 1);
284 }
285
TYPED_TEST(PersistentProtoTest,ClearContents)286 TYPED_TEST(PersistentProtoTest, ClearContents) {
287 const auto test_proto = MakeTestProto();
288 this->test_.WriteToDisk(test_proto);
289
290 {
291 auto pproto = this->BuildTestProto();
292
293 EXPECT_EQ(pproto.get(), nullptr);
294
295 this->Wait();
296 EXPECT_EQ(this->test_.read_status_, ReadStatus::kOk);
297 EXPECT_EQ(this->test_.read_count_, 1);
298 EXPECT_EQ(this->test_.write_count_, 0);
299
300 pproto->Clear();
301 pproto.QueueWrite();
302 }
303
304 this->Wait();
305
306 int64_t size = 0;
307 std::string empty_proto;
308 KeyProto().SerializeToString(&empty_proto);
309
310 ASSERT_TRUE(base::GetFileSize(this->test_.GetPath(), &size));
311 EXPECT_EQ(size, static_cast<int64_t>(empty_proto.size()));
312 }
313
TYPED_TEST(PersistentProtoTest,UpdatePath)314 TYPED_TEST(PersistentProtoTest, UpdatePath) {
315 const base::FilePath new_path =
316 this->test_.temp_dir_.GetPath().Append(FILE_PATH_LITERAL("new_proto"));
317 const int64_t kNewLastRotation = 10;
318
319 const auto test_proto = MakeTestProto();
320 this->test_.WriteToDisk(test_proto);
321
322 auto test_proto2 = MakeTestProto();
323 test_proto2.set_last_rotation(kNewLastRotation);
324 this->test_.WriteToDisk(new_path, test_proto2);
325
326 auto pproto = this->BuildTestProto();
327
328 // Underlying proto should be nullptr until read is complete.
329 EXPECT_EQ(pproto.get(), nullptr);
330
331 this->Wait();
332 EXPECT_EQ(this->test_.read_status_, ReadStatus::kOk);
333 EXPECT_EQ(this->test_.read_count_, 1);
334 EXPECT_EQ(this->test_.write_count_, 0);
335
336 const KeyProto* ptr = pproto.get();
337
338 pproto.UpdatePath(new_path, this->test_.ReadCallback(),
339 /*remove_existing=*/true);
340 this->Wait();
341
342 EXPECT_EQ(this->test_.read_status_, ReadStatus::kOk);
343 EXPECT_EQ(this->test_.read_count_, 2);
344 EXPECT_EQ(this->test_.write_count_, 1);
345
346 // It is expected that the underlying proto doesn't change.
347 EXPECT_EQ(ptr, pproto.get());
348
349 // Check the content of the updated proto.
350 EXPECT_EQ(ptr->key(), test_proto.key());
351 EXPECT_EQ(ptr->rotation_period(), test_proto.rotation_period());
352 EXPECT_EQ(ptr->last_rotation(), kNewLastRotation);
353
354 // Check the state of the files are what we expect.
355 ASSERT_FALSE(base::PathExists(this->test_.GetPath()));
356 ASSERT_TRUE(base::PathExists(new_path));
357 }
358
359 } // namespace metrics::structured
360