blob: 8941a39da7017ab2b7cd6d2def9cd7258574907f [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <string>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/logging.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "components/metrics/structured/lib/arena_persistent_proto.h"
#include "components/metrics/structured/lib/persistent_proto.h"
#include "components/metrics/structured/lib/persistent_proto_internal.h"
#include "components/metrics/structured/lib/proto/key.pb.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace metrics::structured {
namespace {
// Populate |proto| with some test data.
void PopulateTestProto(KeyProto* proto) {
proto->set_key("abcdefghijkl");
proto->set_last_rotation(12345);
proto->set_rotation_period(54321);
}
// Make a proto with test data.
KeyProto MakeTestProto() {
KeyProto proto;
PopulateTestProto(&proto);
return proto;
}
// Returns whether |actual| and |expected| are equal.
bool ProtoEquals(const KeyProto* actual, const KeyProto* expected) {
bool equal = true;
equal &= actual->key() == expected->key();
equal &= actual->last_rotation() == expected->last_rotation();
equal &= actual->rotation_period() == expected->rotation_period();
return equal;
}
base::TimeDelta WriteDelay() {
return base::Seconds(0);
}
template <typename T>
class TestCase {
public:
using PProtoType = T;
TestCase() { Setup(); }
TestCase(const TestCase&) = delete;
~TestCase() = default;
void Setup() { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }
base::FilePath GetPath() {
return temp_dir_.GetPath().Append(FILE_PATH_LITERAL("proto"));
}
void ClearDisk() {
base::DeleteFile(GetPath());
ASSERT_FALSE(base::PathExists(GetPath()));
}
// Read the file at GetPath and parse it as a KeyProto.
KeyProto ReadFromDisk() {
std::string proto_str;
CHECK(base::ReadFileToString(GetPath(), &proto_str));
KeyProto proto;
CHECK(proto.ParseFromString(proto_str));
return proto;
}
void WriteToDisk(const KeyProto& proto) { WriteToDisk(GetPath(), proto); }
void WriteToDisk(const base::FilePath& path, const KeyProto& proto) {
ASSERT_TRUE(base::WriteFile(path, proto.SerializeAsString()));
}
void OnRead(const ReadStatus status) {
read_status_ = status;
++read_count_;
}
base::OnceCallback<void(ReadStatus)> ReadCallback() {
return base::BindOnce(&TestCase::OnRead, base::Unretained(this));
}
void OnWrite(const WriteStatus status) {
ASSERT_EQ(status, WriteStatus::kOk);
++write_count_;
}
base::RepeatingCallback<void(WriteStatus)> WriteCallback() {
return base::BindRepeating(&TestCase::OnWrite, base::Unretained(this));
}
// Constructs the proto of type T.
T BuildTestProto();
// Records the information passed to the callbacks for later expectation.
ReadStatus read_status_;
int read_count_ = 0;
int write_count_ = 0;
base::ScopedTempDir temp_dir_;
};
template <typename T>
T TestCase<T>::BuildTestProto() {
ASSERT_TRUE(false)
<< "Invalid type parameter, please implement BuildTestProto for T";
}
template <>
PersistentProto<KeyProto>
TestCase<PersistentProto<KeyProto>>::BuildTestProto() {
return PersistentProto<KeyProto>(GetPath(), WriteDelay(), ReadCallback(),
WriteCallback());
}
template <>
ArenaPersistentProto<KeyProto>
TestCase<ArenaPersistentProto<KeyProto>>::BuildTestProto() {
return ArenaPersistentProto<KeyProto>(GetPath(), WriteDelay(), ReadCallback(),
WriteCallback());
}
} // namespace
// Testing suite for any class that is a persistent proto. This is a series of
// tests needed by any PersistentProtoInternal implementation. Currently this
// includes: PersistentProto and ArenaPersistentProto.
template <typename T>
class PersistentProtoTest : public testing::Test {
public:
void Wait() { task_environment_.RunUntilIdle(); }
T BuildTestProto() { return test_.BuildTestProto(); }
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::MainThreadType::UI,
base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED};
TestCase<T> test_;
};
using Implementations =
testing::Types<PersistentProto<KeyProto>, ArenaPersistentProto<KeyProto>>;
TYPED_TEST_SUITE(PersistentProtoTest, Implementations);
// Test that the underlying proto is nullptr until a read is complete, and isn't
// after that.
TYPED_TEST(PersistentProtoTest, Initialization) {
auto pproto = this->BuildTestProto();
EXPECT_EQ(pproto.get(), nullptr);
this->Wait();
EXPECT_NE(pproto.get(), nullptr);
}
// Test bool conversion and has_value.
TYPED_TEST(PersistentProtoTest, BoolTests) {
auto pproto = this->BuildTestProto();
EXPECT_EQ(pproto.get(), nullptr);
EXPECT_FALSE(pproto);
EXPECT_FALSE(pproto.has_value());
this->Wait();
EXPECT_NE(pproto.get(), nullptr);
EXPECT_TRUE(pproto);
EXPECT_TRUE(pproto.has_value());
}
// Test -> and *.
TYPED_TEST(PersistentProtoTest, Getters) {
auto pproto = this->BuildTestProto();
this->Wait();
// We're really just checking these don't crash.
EXPECT_EQ(pproto->last_rotation(), 0);
KeyProto val = *pproto;
}
// Test that the pproto correctly saves the in-memory proto to disk.
TYPED_TEST(PersistentProtoTest, Read) {
auto pproto = this->BuildTestProto();
// Underlying proto should be nullptr until read is complete.
EXPECT_EQ(pproto.get(), nullptr);
this->Wait();
EXPECT_EQ(this->test_.read_status_, ReadStatus::kMissing);
EXPECT_EQ(this->test_.read_count_, 1);
EXPECT_EQ(this->test_.write_count_, 1);
PopulateTestProto(pproto.get());
pproto.StartWriteForTesting();
this->Wait();
EXPECT_EQ(this->test_.write_count_, 2);
KeyProto written = this->test_.ReadFromDisk();
EXPECT_TRUE(ProtoEquals(&written, pproto.get()));
}
// Test that invalid files on disk are handled correctly.
TYPED_TEST(PersistentProtoTest, ReadInvalidProto) {
ASSERT_TRUE(
base::WriteFile(this->test_.GetPath(), "this isn't a valid proto"));
auto pproto = this->BuildTestProto();
this->Wait();
EXPECT_EQ(this->test_.read_status_, ReadStatus::kParseError);
EXPECT_EQ(this->test_.read_count_, 1);
EXPECT_EQ(this->test_.write_count_, 1);
}
// Test that the pproto correctly loads an on-disk proto into memory.
TYPED_TEST(PersistentProtoTest, Write) {
const auto test_proto = MakeTestProto();
this->test_.WriteToDisk(test_proto);
auto pproto = this->BuildTestProto();
EXPECT_EQ(pproto.get(), nullptr);
this->Wait();
EXPECT_EQ(this->test_.read_status_, ReadStatus::kOk);
EXPECT_EQ(this->test_.read_count_, 1);
EXPECT_EQ(this->test_.write_count_, 0);
EXPECT_NE(pproto.get(), nullptr);
EXPECT_TRUE(ProtoEquals(pproto.get(), &test_proto));
}
// Test that several saves all happen correctly.
TYPED_TEST(PersistentProtoTest, MultipleWrites) {
auto pproto = this->BuildTestProto();
EXPECT_EQ(pproto.get(), nullptr);
this->Wait();
EXPECT_EQ(this->test_.write_count_, 1);
for (int i = 1; i <= 10; ++i) {
pproto.get()->set_last_rotation(i * i);
pproto.StartWriteForTesting();
this->Wait();
EXPECT_EQ(this->test_.write_count_, i + 1);
KeyProto written = this->test_.ReadFromDisk();
ASSERT_EQ(written.last_rotation(), i * i);
}
}
// Test that many calls to QueueWrite get batched, leading to only one real
// write.
TYPED_TEST(PersistentProtoTest, QueueWrites) {
auto pproto = this->BuildTestProto();
this->Wait();
EXPECT_EQ(this->test_.write_count_, 1);
// Three successive StartWrite calls result in three writes.
this->test_.write_count_ = 0;
for (int i = 0; i < 3; ++i) {
pproto.StartWriteForTesting();
}
this->Wait();
EXPECT_EQ(this->test_.write_count_, 3);
// Three successive QueueWrite calls results in one write.
this->test_.write_count_ = 0;
for (int i = 0; i < 3; ++i) {
pproto.QueueWrite();
}
this->Wait();
EXPECT_EQ(this->test_.write_count_, 1);
}
TYPED_TEST(PersistentProtoTest, ClearContents) {
const auto test_proto = MakeTestProto();
this->test_.WriteToDisk(test_proto);
{
auto pproto = this->BuildTestProto();
EXPECT_EQ(pproto.get(), nullptr);
this->Wait();
EXPECT_EQ(this->test_.read_status_, ReadStatus::kOk);
EXPECT_EQ(this->test_.read_count_, 1);
EXPECT_EQ(this->test_.write_count_, 0);
pproto->Clear();
pproto.QueueWrite();
}
this->Wait();
std::string empty_proto;
KeyProto().SerializeToString(&empty_proto);
std::optional<int64_t> size = base::GetFileSize(this->test_.GetPath());
ASSERT_TRUE(size.has_value());
EXPECT_EQ(size.value(), static_cast<int64_t>(empty_proto.size()));
}
TYPED_TEST(PersistentProtoTest, UpdatePath) {
const base::FilePath new_path =
this->test_.temp_dir_.GetPath().Append(FILE_PATH_LITERAL("new_proto"));
const int64_t kNewLastRotation = 10;
const auto test_proto = MakeTestProto();
this->test_.WriteToDisk(test_proto);
auto test_proto2 = MakeTestProto();
test_proto2.set_last_rotation(kNewLastRotation);
this->test_.WriteToDisk(new_path, test_proto2);
auto pproto = this->BuildTestProto();
// Underlying proto should be nullptr until read is complete.
EXPECT_EQ(pproto.get(), nullptr);
this->Wait();
EXPECT_EQ(this->test_.read_status_, ReadStatus::kOk);
EXPECT_EQ(this->test_.read_count_, 1);
EXPECT_EQ(this->test_.write_count_, 0);
const KeyProto* ptr = pproto.get();
pproto.UpdatePath(new_path, this->test_.ReadCallback(),
/*remove_existing=*/true);
this->Wait();
EXPECT_EQ(this->test_.read_status_, ReadStatus::kOk);
EXPECT_EQ(this->test_.read_count_, 2);
EXPECT_EQ(this->test_.write_count_, 1);
// It is expected that the underlying proto doesn't change.
EXPECT_EQ(ptr, pproto.get());
// Check the content of the updated proto.
EXPECT_EQ(ptr->key(), test_proto.key());
EXPECT_EQ(ptr->rotation_period(), test_proto.rotation_period());
EXPECT_EQ(ptr->last_rotation(), kNewLastRotation);
// Check the state of the files are what we expect.
ASSERT_FALSE(base::PathExists(this->test_.GetPath()));
ASSERT_TRUE(base::PathExists(new_path));
}
} // namespace metrics::structured