// 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 "chrome/browser/metrics/structured/arena_event_buffer.h"
#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/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/metrics/structured/lib/event_buffer.h"
#include "components/metrics/structured/structured_metrics_features.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/metrics_proto/structured_data.pb.h"
namespace metrics::structured {
namespace {
using ::google::protobuf::RepeatedPtrField;
// Creates an event for testing. The serialized size of this event is about 9
// bytes.
StructuredEventProto TestEvent(uint64_t id) {
StructuredEventProto event;
event.set_device_project_id(id);
return event;
}
EventsProto ReadEvents(const base::FilePath& path) {
std::string content;
EXPECT_TRUE(base::ReadFileToString(path, &content));
EventsProto proto;
EXPECT_TRUE(proto.MergeFromString(content));
return proto;
}
} // namespace
class ArenaEventBufferTest : public testing::Test {
public:
const base::TimeDelta kWriteDelay = base::Seconds(0);
ArenaEventBufferTest()
: profile_manager_(TestingBrowserProcess::GetGlobal()) {}
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
ASSERT_TRUE(profile_manager_.SetUp());
}
base::FilePath GetPath() {
return temp_dir_.GetPath().Append(FILE_PATH_LITERAL("proto_file"));
}
base::FilePath GetAltPath() {
return temp_dir_.GetPath().Append(FILE_PATH_LITERAL("alt_proto_file"));
}
std::unique_ptr<ArenaEventBuffer> CreateTestBuffer(uint64_t max_size) {
return std::make_unique<ArenaEventBuffer>(GetPath(), kWriteDelay, max_size);
}
void Wait() { task_environment_.RunUntilIdle(); }
TestingProfile* AddProfile() {
return profile_manager_.CreateTestingProfile("p1");
}
protected:
content::BrowserTaskEnvironment task_environment_{
content::BrowserTaskEnvironment::TimeSource::MOCK_TIME};
base::ScopedTempDir temp_dir_;
TestingProfileManager profile_manager_;
};
TEST_F(ArenaEventBufferTest, OkEvent) {
std::unique_ptr<ArenaEventBuffer> buffer = CreateTestBuffer(/*max_size=*/128);
Wait();
EXPECT_EQ(buffer->AddEvent(TestEvent(1)), Result::kOk);
}
TEST_F(ArenaEventBufferTest, FullEvent) {
std::unique_ptr<ArenaEventBuffer> buffer = CreateTestBuffer(/*max_size=*/128);
Wait();
EXPECT_EQ(buffer->AddEvent(TestEvent(1)), Result::kOk);
// Create an event that is larger then the heuristic.
auto event2 = TestEvent(2);
// Add 10 metrics
for (int i = 0; i < 10; ++i) {
auto* metric = event2.add_metrics();
metric->set_name_hash(i);
metric->set_value_string("metric value");
}
EXPECT_EQ(buffer->AddEvent(event2), Result::kFull);
}
TEST_F(ArenaEventBufferTest, Purge) {
std::unique_ptr<ArenaEventBuffer> buffer = CreateTestBuffer(/*max_size=*/128);
Wait();
EXPECT_EQ(buffer->AddEvent(TestEvent(1)), Result::kOk);
buffer->Purge();
Wait();
EXPECT_EQ(buffer->resource_info().used_size_bytes, 0ul);
EXPECT_FALSE(base::PathExists(GetPath()));
}
TEST_F(ArenaEventBufferTest, UpdatePath) {
EventsProto events;
events.mutable_events()->Add(TestEvent(2));
std::string content;
ASSERT_TRUE(events.SerializeToString(&content));
ASSERT_TRUE(base::WriteFile(GetAltPath(), content));
std::unique_ptr<ArenaEventBuffer> buffer = CreateTestBuffer(/*max_size=*/512);
Wait();
EXPECT_EQ(buffer->AddEvent(TestEvent(1)), Result::kOk);
EXPECT_EQ(buffer->proto()->events_size(), 1);
buffer->UpdatePath(GetAltPath());
Wait();
EXPECT_EQ(buffer->proto()->events_size(), 2);
EXPECT_EQ(buffer->AddEvent(TestEvent(1)), Result::kOk);
EXPECT_EQ(buffer->proto()->events_size(), 3);
}
TEST_F(ArenaEventBufferTest, PeriodicEventBackup) {
base::test::ScopedFeatureList features;
features.InitAndEnableFeatureWithParameters(kEventStorageManager,
{{
"event_backup_time_s",
"3" // seconds
}});
std::unique_ptr<ArenaEventBuffer> buffer = CreateTestBuffer(/*max_size=*/512);
Wait();
// Add an event.
buffer->AddEvent(TestEvent(1));
EXPECT_EQ(buffer->proto()->events_size(), 1);
// Wait for 3 seconds for the timer to trigger a backup.
task_environment_.FastForwardBy(base::Seconds(3));
Wait();
// Read the events from disk to see if the file has the expected content.
EventsProto events = ReadEvents(GetPath());
ASSERT_EQ(events.events_size(), 1);
const auto& event = events.events(0);
EXPECT_EQ(event.device_project_id(), 1ul);
}
TEST_F(ArenaEventBufferTest, Serialize) {
std::unique_ptr<ArenaEventBuffer> buffer = CreateTestBuffer(/*max_size=*/512);
Wait();
EXPECT_EQ(buffer->AddEvent(TestEvent(1)), Result::kOk);
EXPECT_EQ(buffer->AddEvent(TestEvent(2)), Result::kOk);
EXPECT_EQ(buffer->AddEvent(TestEvent(3)), Result::kOk);
RepeatedPtrField<StructuredEventProto> events = buffer->Serialize();
// Expect |events| to not be associated with an arena.
EXPECT_EQ(events.GetArena(), nullptr);
ASSERT_EQ(events.size(), 3);
for (size_t i = 0; i < static_cast<size_t>(events.size()); ++i) {
EXPECT_EQ(events[i].device_project_id(), i + 1);
}
// Serialize is a copy.
EXPECT_EQ(buffer->proto()->events_size(), 3);
}
TEST_F(ArenaEventBufferTest, Flush) {
std::unique_ptr<ArenaEventBuffer> buffer = CreateTestBuffer(/*max_size=*/512);
Wait();
EXPECT_EQ(buffer->AddEvent(TestEvent(1)), Result::kOk);
EXPECT_EQ(buffer->AddEvent(TestEvent(2)), Result::kOk);
EXPECT_EQ(buffer->AddEvent(TestEvent(3)), Result::kOk);
buffer->proto().QueueWrite();
Wait();
const base::FilePath& path = GetPath();
base::File::Info info;
CHECK(base::GetFileInfo(path, &info));
const base::FilePath new_path =
temp_dir_.GetPath().Append(FILE_PATH_LITERAL("new_proto_file"));
bool flushed = false;
buffer->Flush(new_path, base::BindLambdaForTesting(
[&](base::expected<FlushedKey, FlushError> key) {
flushed = true;
EXPECT_TRUE(key.has_value());
EXPECT_EQ(key->size, info.size);
EXPECT_EQ(key->path, new_path);
// A new file is created, the creation time must
// be different.
EXPECT_GE(key->creation_time,
info.creation_time);
EXPECT_EQ(buffer->proto()->events_size(), 0);
EXPECT_EQ(
buffer->resource_info().used_size_bytes,
0ul);
}));
Wait();
EXPECT_TRUE(flushed);
}
// ArenaEventBuffer functions the same regardless of the path, so there is only
// one test for this.
TEST_F(ArenaEventBufferTest, ProfileUpdatePath) {
std::unique_ptr<ArenaEventBuffer> buffer = CreateTestBuffer(/*max_size=*/512);
Wait();
EXPECT_EQ(buffer->AddEvent(TestEvent(1)), Result::kOk);
AddProfile();
Wait();
EXPECT_NE(GetPath(), buffer->proto().path());
EXPECT_EQ(buffer->AddEvent(TestEvent(2)), Result::kOk);
EXPECT_EQ(buffer->AddEvent(TestEvent(3)), Result::kOk);
// Write to disk at the new path.
buffer->proto().StartWriteForTesting();
Wait();
// Expect the flushed size ot be the same as the serialized size of the
// in-memory content.
int64_t size = 0;
EXPECT_TRUE(base::GetFileSize(buffer->proto().path(), &size));
EXPECT_EQ(static_cast<uint64_t>(size), buffer->proto()->ByteSizeLong());
}
TEST_F(ArenaEventBufferTest, PreInitEvents) {
std::unique_ptr<ArenaEventBuffer> buffer = CreateTestBuffer(/*max_size=*/512);
EXPECT_EQ(buffer->AddEvent(TestEvent(1)), Result::kOk);
EXPECT_EQ(buffer->AddEvent(TestEvent(2)), Result::kOk);
EXPECT_EQ(buffer->AddEvent(TestEvent(3)), Result::kOk);
Wait();
EXPECT_EQ(buffer->proto()->events_size(), 3);
}
} // namespace metrics::structured