// 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 <memory>
#include <numeric>
#include <string>
#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<uint64_t>& 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<uint64_t>& ids) {
ASSERT_EQ(proto.uma_events().size(), static_cast<int>(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<ExternalMetrics>(
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<ExternalMetrics> external_metrics_;
std::optional<EventsProto> 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<int64_t> 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<int64_t> 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