// 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 #include #include #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 { public: TestEventBuffer() : EventBuffer(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 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(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& ids) { TestEventBuffer buffer; for (int id : ids) { EXPECT_EQ(buffer.AddEvent(BuildTestEvent(id)), Result::kOk); } return buffer; } EventsProto BuildTestEvents(const std::vector& 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 key) { EXPECT_TRUE(key.has_value()); EXPECT_TRUE(base::PathExists(base::FilePath(key->path))); })); Wait(); const std::vector& keys = map.keys(); EXPECT_EQ(keys.size(), 1ul); std::optional file_size = base::GetFileSize(keys[0].path); ASSERT_TRUE(file_size.has_value()); EXPECT_EQ(keys[0].size, static_cast(file_size.value())); } 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 key) { EXPECT_TRUE(key.has_value()); EXPECT_TRUE(base::PathExists(base::FilePath(key->path))); })); Wait(); auto key = map.keys().front(); std::optional 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 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 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 key) { EXPECT_TRUE(key.has_value()); EXPECT_TRUE(base::PathExists(key->path)); })); Wait(); const std::vector& 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& keys = map.keys(); EXPECT_EQ(keys.size(), 2ul); // The order the files are loaded in is unknown. std::vector 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 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 key) { EXPECT_FALSE(key.has_value()); const FlushError err = key.error(); EXPECT_EQ(err, kQuotaExceeded); })); Wait(); } } // namespace metrics::structured