// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/metrics/structured/flushed_map.h"
#include <cstdint>
#include <optional>
#include <vector>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "components/metrics/structured/lib/event_buffer.h"
#include "components/metrics/structured/proto/event_storage.pb.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/metrics_proto/structured_data.pb.h"
namespace metrics::structured {
namespace {
class TestEventBuffer : public EventBuffer<StructuredEventProto> {
public:
TestEventBuffer() : EventBuffer<StructuredEventProto>(ResourceInfo(1024)) {}
// EventBuffer:
Result AddEvent(StructuredEventProto event) override {
events_.mutable_events()->Add(std::move(event));
return Result::kOk;
}
void Purge() override { events_.Clear(); }
google::protobuf::RepeatedPtrField<StructuredEventProto> Serialize()
override {
return events_.events();
}
void Flush(const base::FilePath& path, FlushedCallback callback) override {
std::string content;
EXPECT_TRUE(events_.SerializeToString(&content));
EXPECT_TRUE(base::WriteFile(path, content));
std::move(callback).Run(FlushedKey{
.size = static_cast<int32_t>(content.size()),
.path = path,
.creation_time = base::Time::Now(),
});
}
uint64_t Size() override { return events_.events_size(); }
const EventsProto& events() const { return events_; }
private:
EventsProto events_;
};
StructuredEventProto BuildTestEvent(int id) {
StructuredEventProto event;
event.set_device_project_id(id);
return event;
}
TestEventBuffer BuildTestBuffer(const std::vector<int>& ids) {
TestEventBuffer buffer;
for (int id : ids) {
EXPECT_EQ(buffer.AddEvent(BuildTestEvent(id)), Result::kOk);
}
return buffer;
}
EventsProto BuildTestEvents(const std::vector<int>& ids) {
EventsProto events;
for (int id : ids) {
events.mutable_events()->Add(BuildTestEvent(id));
}
return events;
}
} // namespace
class FlushedMapTest : public testing::Test {
public:
FlushedMapTest() = default;
~FlushedMapTest() override = default;
void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }
base::FilePath GetDir() const {
return temp_dir_.GetPath().Append(FILE_PATH_LITERAL("events"));
}
FlushedMap BuildFlushedMap(int32_t max_size = 2048) {
return FlushedMap(GetDir(), max_size);
}
EventsProto ReadEventsProto(const base::FilePath& path) {
std::string content;
EXPECT_TRUE(base::ReadFileToString(path, &content));
EventsProto events;
EXPECT_TRUE(events.MergeFromString(content));
return events;
}
void WriteToDisk(const base::FilePath& path, EventsProto&& events) {
std::string content;
EXPECT_TRUE(events.SerializeToString(&content));
EXPECT_TRUE(base::WriteFile(path, content));
}
void Wait() { task_environment_.RunUntilIdle(); }
private:
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::MainThreadType::UI,
base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED};
base::ScopedTempDir temp_dir_;
protected:
};
TEST_F(FlushedMapTest, FlushFile) {
FlushedMap map = BuildFlushedMap();
Wait();
auto buffer = BuildTestBuffer({1, 2, 3});
map.Flush(buffer,
base::BindLambdaForTesting(
[&](base::expected<FlushedKey, FlushError> key) {
EXPECT_TRUE(key.has_value());
EXPECT_TRUE(base::PathExists(base::FilePath(key->path)));
}));
Wait();
const std::vector<FlushedKey>& keys = map.keys();
EXPECT_EQ(keys.size(), 1ul);
int64_t file_size;
EXPECT_TRUE(base::GetFileSize(keys[0].path, &file_size));
EXPECT_EQ(keys[0].size, static_cast<int32_t>(file_size));
}
TEST_F(FlushedMapTest, ReadFile) {
FlushedMap map = BuildFlushedMap();
Wait();
auto buffer = BuildTestBuffer({1, 2, 3});
const EventsProto& events = buffer.events();
map.Flush(buffer,
base::BindLambdaForTesting(
[&](base::expected<FlushedKey, FlushError> key) {
EXPECT_TRUE(key.has_value());
EXPECT_TRUE(base::PathExists(base::FilePath(key->path)));
}));
Wait();
auto key = map.keys().front();
std::optional<EventsProto> read_events = map.ReadKey(key);
EXPECT_EQ(read_events->events_size(), events.events_size());
for (int i = 0; i < read_events->events_size(); ++i) {
EXPECT_EQ(read_events->events(i).device_project_id(),
events.events(i).device_project_id());
}
}
TEST_F(FlushedMapTest, UniqueFlushes) {
FlushedMap map = BuildFlushedMap();
Wait();
auto events = BuildTestBuffer({1, 2, 3});
map.Flush(events,
base::BindLambdaForTesting(
[&](base::expected<FlushedKey, FlushError> key) {
EXPECT_TRUE(key.has_value());
EXPECT_TRUE(base::PathExists(base::FilePath(key->path)));
}));
Wait();
auto events2 = BuildTestBuffer({4, 5, 6});
map.Flush(events2,
base::BindLambdaForTesting(
[&](base::expected<FlushedKey, FlushError> key) {
EXPECT_TRUE(key.has_value());
EXPECT_TRUE(base::PathExists(base::FilePath(key->path)));
}));
Wait();
EXPECT_EQ(map.keys().size(), 2ul);
const auto& key1 = map.keys()[0];
const auto& key2 = map.keys()[1];
EXPECT_NE(key1.path, key2.path);
}
TEST_F(FlushedMapTest, DeleteKey) {
FlushedMap map = BuildFlushedMap();
Wait();
auto events = BuildTestBuffer({1, 2, 3});
map.Flush(events, base::BindLambdaForTesting(
[&](base::expected<FlushedKey, FlushError> key) {
EXPECT_TRUE(key.has_value());
EXPECT_TRUE(base::PathExists(key->path));
}));
Wait();
const std::vector<FlushedKey>& keys = map.keys();
auto key = map.keys().front();
EXPECT_EQ(keys.size(), 1ul);
EXPECT_TRUE(base::PathExists(key.path));
map.DeleteKey(key);
Wait();
EXPECT_FALSE(base::PathExists(key.path));
}
TEST_F(FlushedMapTest, LoadPreviousSessionKeys) {
EXPECT_TRUE(base::CreateDirectory(GetDir()));
auto events = BuildTestEvents({1, 2, 3});
auto events2 = BuildTestEvents({4, 5, 6});
base::FilePath path1 = GetDir().Append(FILE_PATH_LITERAL("events"));
base::FilePath path2 = GetDir().Append(FILE_PATH_LITERAL("events2"));
WriteToDisk(path1, std::move(events));
// Force a small difference in the creation time of the two files.
base::PlatformThreadBase::Sleep(base::Seconds(1));
WriteToDisk(path2, std::move(events2));
FlushedMap map = BuildFlushedMap();
Wait();
const std::vector<FlushedKey>& keys = map.keys();
EXPECT_EQ(keys.size(), 2ul);
// The order the files are loaded in is unknown.
std::vector<base::FilePath> paths;
for (const auto& key : keys) {
paths.push_back(base::FilePath(key.path));
}
EXPECT_THAT(paths, testing::ElementsAre(path1, path2));
}
TEST_F(FlushedMapTest, ExceedQuota) {
FlushedMap map = BuildFlushedMap(/*max_size=*/64);
Wait();
auto events = BuildTestBuffer({1, 2, 3});
map.Flush(events, base::BindLambdaForTesting(
[&](base::expected<FlushedKey, FlushError> key) {
EXPECT_TRUE(key.has_value());
EXPECT_TRUE(base::PathExists(key->path));
}));
Wait();
auto events2 = BuildTestBuffer({1, 2, 3});
map.Flush(events2, base::BindLambdaForTesting(
[&](base::expected<FlushedKey, FlushError> key) {
EXPECT_FALSE(key.has_value());
const FlushError err = key.error();
EXPECT_EQ(err, kQuotaExceeded);
}));
Wait();
}
} // namespace metrics::structured