// 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 "components/metrics/structured/external_metrics.h" #include #include #include #include "base/files/file_util.h" #include "base/files/scoped_temp_dir.h" #include "base/logging.h" #include "base/strings/string_number_conversions.h" #include "base/test/metrics/histogram_tester.h" #include "base/test/scoped_feature_list.h" #include "base/test/task_environment.h" #include "build/build_config.h" #include "components/metrics/structured/histogram_util.h" #include "components/metrics/structured/proto/event_storage.pb.h" #include "components/metrics/structured/structured_metrics_features.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" namespace metrics::structured { namespace { using testing::UnorderedElementsAre; // Make a simple testing proto with one |uma_events| message for each id in // |ids|. EventsProto MakeTestingProto(const std::vector& ids, uint64_t project_name_hash = 0) { EventsProto proto; for (const auto id : ids) { auto* event = proto.add_uma_events(); event->set_project_name_hash(project_name_hash); event->set_profile_event_id(id); } return proto; } // Check that |proto| is consistent with the proto that would be generated by // MakeTestingProto(ids). void AssertEqualsTestingProto(const EventsProto& proto, const std::vector& ids) { ASSERT_EQ(proto.uma_events().size(), static_cast(ids.size())); ASSERT_TRUE(proto.events().empty()); for (size_t i = 0; i < ids.size(); ++i) { const auto& event = proto.uma_events(i); ASSERT_EQ(event.profile_event_id(), ids[i]); ASSERT_FALSE(event.has_event_name_hash()); ASSERT_TRUE(event.metrics().empty()); } } } // namespace class ExternalMetricsTest : public testing::Test { public: void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); } void Init() { // We don't use the scheduling feature when testing ExternalMetrics, instead // we just call CollectMetrics directly. So make up a time interval here // that we'll never reach in a test. const auto one_hour = base::Hours(1); external_metrics_ = std::make_unique( temp_dir_.GetPath(), one_hour, base::BindRepeating(&ExternalMetricsTest::OnEventsCollected, base::Unretained(this))); // For most tests the recording needs to be enabled. EnableRecording(); } void EnableRecording() { external_metrics_->EnableRecording(); } void DisableRecording() { external_metrics_->DisableRecording(); } void CollectEvents() { external_metrics_->CollectEvents(); Wait(); CHECK(proto_.has_value()); } void OnEventsCollected(const EventsProto& proto) { proto_ = std::move(proto); } void WriteToDisk(const std::string& name, const EventsProto& proto) { CHECK(base::WriteFile(temp_dir_.GetPath().Append(name), proto.SerializeAsString())); } void WriteToDisk(const std::string& name, const std::string& str) { CHECK(base::WriteFile(temp_dir_.GetPath().Append(name), str)); } void Wait() { task_environment_.RunUntilIdle(); } base::ScopedTempDir temp_dir_; std::unique_ptr external_metrics_; std::optional proto_; base::test::TaskEnvironment task_environment_{ base::test::TaskEnvironment::MainThreadType::UI, base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED}; base::HistogramTester histogram_tester_; }; TEST_F(ExternalMetricsTest, ReadOneFile) { // Make one proto with three events. WriteToDisk("myproto", MakeTestingProto({111, 222, 333})); Init(); CollectEvents(); // We should have correctly picked up the three events. AssertEqualsTestingProto(proto_.value(), {111, 222, 333}); // And the directory should now be empty. ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath())); } TEST_F(ExternalMetricsTest, ReadManyFiles) { // Make three protos with three events each. WriteToDisk("first", MakeTestingProto({111, 222, 333})); WriteToDisk("second", MakeTestingProto({444, 555, 666})); WriteToDisk("third", MakeTestingProto({777, 888, 999})); Init(); CollectEvents(); // We should have correctly picked up the nine events. Don't check for order, // because we can't guarantee the files will be read from disk in any // particular order. std::vector ids; for (const auto& event : proto_.value().uma_events()) { ids.push_back(event.profile_event_id()); } ASSERT_THAT( ids, UnorderedElementsAre(111, 222, 333, 444, 555, 666, 777, 888, 999)); // The directory should be empty after reading. ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath())); } TEST_F(ExternalMetricsTest, ReadZeroFiles) { Init(); CollectEvents(); // We should have an empty proto. AssertEqualsTestingProto(proto_.value(), {}); // And the directory should be empty too. ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath())); } TEST_F(ExternalMetricsTest, CollectTwice) { Init(); WriteToDisk("first", MakeTestingProto({111, 222, 333})); CollectEvents(); AssertEqualsTestingProto(proto_.value(), {111, 222, 333}); WriteToDisk("first", MakeTestingProto({444})); CollectEvents(); AssertEqualsTestingProto(proto_.value(), {444}); } TEST_F(ExternalMetricsTest, HandleCorruptFile) { Init(); WriteToDisk("invalid", "surprise i'm not a proto"); WriteToDisk("valid", MakeTestingProto({111, 222, 333})); CollectEvents(); AssertEqualsTestingProto(proto_.value(), {111, 222, 333}); // Should have deleted the invalid file too. ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath())); } TEST_F(ExternalMetricsTest, FileNumberReadCappedAndDiscarded) { // Setup feature. base::test::ScopedFeatureList feature_list; const int file_limit = 2; feature_list.InitAndEnableFeatureWithParameters( features::kStructuredMetrics, {{"file_limit", base::NumberToString(file_limit)}}); Init(); // File limit is set to 2. Include third file to test that it is omitted and // deleted. WriteToDisk("first", MakeTestingProto({111})); WriteToDisk("second", MakeTestingProto({222})); WriteToDisk("third", MakeTestingProto({333})); CollectEvents(); // Number of events should be capped to the file limit since above records one // event per file. ASSERT_EQ(proto_.value().uma_events().size(), file_limit); // And the directory should be empty too. ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath())); } TEST_F(ExternalMetricsTest, FilterDisallowedProjects) { Init(); external_metrics_->AddDisallowedProjectForTest(2); // Add 3 events with a project of 1 and 2. WriteToDisk("first", MakeTestingProto({111}, 1)); WriteToDisk("second", MakeTestingProto({222}, 2)); WriteToDisk("third", MakeTestingProto({333}, 1)); CollectEvents(); // The events at second should be filtered. ASSERT_EQ(proto_.value().uma_events().size(), 2); std::vector ids; for (const auto& event : proto_.value().uma_events()) { ids.push_back(event.profile_event_id()); } // Validate that only project 1 remains. ASSERT_THAT(ids, UnorderedElementsAre(111, 333)); // And the directory should be empty too. ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath())); } TEST_F(ExternalMetricsTest, DroppedEventsWhenDisabled) { Init(); DisableRecording(); // Add 3 events with a project of 1 and 2. WriteToDisk("first", MakeTestingProto({111}, 1)); WriteToDisk("second", MakeTestingProto({222}, 2)); WriteToDisk("third", MakeTestingProto({333}, 1)); CollectEvents(); // No events should have been collected. ASSERT_EQ(proto_.value().uma_events().size(), 0); // And the directory should be empty too. ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath())); } TEST_F(ExternalMetricsTest, ProducedAndDroppedEventMetricCollected) { base::test::ScopedFeatureList feature_list; const int file_limit = 5; feature_list.InitAndEnableFeatureWithParameters( features::kStructuredMetrics, {{"file_limit", base::NumberToString(file_limit)}}); Init(); // Generate 9 events. WriteToDisk("event0", MakeTestingProto({0}, UINT64_C(4320592646346933548))); WriteToDisk("event1", MakeTestingProto({1}, UINT64_C(4320592646346933548))); WriteToDisk("event2", MakeTestingProto({2}, UINT64_C(4320592646346933548))); WriteToDisk("event3", MakeTestingProto({3}, UINT64_C(4320592646346933548))); WriteToDisk("event4", MakeTestingProto({4}, UINT64_C(4320592646346933548))); WriteToDisk("event5", MakeTestingProto({5}, UINT64_C(4320592646346933548))); WriteToDisk("event6", MakeTestingProto({6}, UINT64_C(4320592646346933548))); WriteToDisk("event7", MakeTestingProto({7}, UINT64_C(4320592646346933548))); WriteToDisk("event8", MakeTestingProto({8}, UINT64_C(4320592646346933548))); CollectEvents(); // There should be 9 files processed and 4 events dropped. We analyze the // histograms to verify this. EXPECT_EQ(histogram_tester_.GetTotalSum( std::string(kExternalMetricsProducedHistogramPrefix) + "WiFi"), 9); EXPECT_EQ(histogram_tester_.GetTotalSum( std::string(kExternalMetricsDroppedHistogramPrefix) + "WiFi"), 4); // There should |file_limit| events. The rest should have been dropped. ASSERT_EQ(proto_.value().uma_events().size(), file_limit); // The directory should be empty. ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath())); } // TODO(crbug.com/40156926): Add a test for concurrent reading and writing here // once we know the specifics of how the lock in cros is performed. } // namespace metrics::structured