// Copyright 2020 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include #include #include #include #include #include "base/fuchsia/scoped_service_binding.h" #include "base/fuchsia/test_component_context_for_process.h" #include "base/metrics/histogram_macros.h" #include "base/test/bind.h" #include "base/test/task_environment.h" #include "base/threading/thread_task_runner_handle.h" #include "base/time/time.h" #include "fuchsia/base/legacymetrics_client.h" #include "fuchsia/base/legacymetrics_histogram_flattener.h" #include "fuchsia/base/result_receiver.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" namespace cr_fuchsia { namespace { using ::testing::Property; using ::testing::UnorderedElementsAreArray; constexpr base::TimeDelta kReportInterval = base::TimeDelta::FromMinutes(1); constexpr base::TimeDelta kShortDuration = base::TimeDelta::FromSeconds(1); class TestMetricsRecorder : public fuchsia::legacymetrics::testing::MetricsRecorder_TestBase { public: TestMetricsRecorder() = default; ~TestMetricsRecorder() override = default; bool IsRecordInFlight() const { return ack_callback_.has_value(); } std::vector WaitForEvents() { if (recorded_events_.empty()) { base::RunLoop run_loop; on_record_cb_ = run_loop.QuitClosure(); run_loop.Run(); } return std::move(recorded_events_); } void DropAck() { ack_callback_ = base::nullopt; } void SendAck() { (*ack_callback_)(); ack_callback_ = base::nullopt; } void set_expect_ack_dropped(bool expect_dropped) { expect_ack_dropped_ = expect_dropped; } // fuchsia::legacymetrics::MetricsRecorder implementation. void Record(std::vector events, RecordCallback callback) override { std::move(events.begin(), events.end(), std::back_inserter(recorded_events_)); // Received a call to Record() before the previous one was acknowledged, // which can happen in some cases (e.g. flushing). if (ack_callback_) EXPECT_TRUE(expect_ack_dropped_); ack_callback_ = std::move(callback); if (on_record_cb_) std::move(on_record_cb_).Run(); } void NotImplemented_(const std::string& name) override { FAIL() << name; } private: std::vector recorded_events_; base::OnceClosure on_record_cb_; base::Optional ack_callback_; bool expect_ack_dropped_ = false; }; class LegacyMetricsClientTest : public testing::Test { public: LegacyMetricsClientTest() : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME, base::test::TaskEnvironment::MainThreadType::IO) {} ~LegacyMetricsClientTest() override = default; void SetUp() override { service_binding_ = MakeServiceBinding(); base::SetRecordActionTaskRunner(base::ThreadTaskRunnerHandle::Get()); // Flush any dirty histograms from previous test runs in this process. GetLegacyMetricsDeltas(); } std::unique_ptr> MakeServiceBinding() { return std::make_unique>( test_context_.additional_services(), &test_recorder_); } void StartClientAndExpectConnection() { client_.Start(kReportInterval); base::RunLoop().RunUntilIdle(); EXPECT_TRUE(service_binding_->has_clients()); } // Disconnects the service side of the metrics FIDL channel and replaces the // binding with a new instance. void DisconnectAndRestartMetricsService() { service_binding_.reset(); service_binding_ = MakeServiceBinding(); base::RunLoop().RunUntilIdle(); } void ExpectReconnectAfterDelay(const base::TimeDelta& delay) { // Just before the expected delay, the client shouldn't reconnect yet. task_environment_.FastForwardBy(delay - kShortDuration); EXPECT_FALSE(service_binding_->has_clients()) << "Expected delay: " << delay; // Complete the full expected reconnect delay. Client should reconnect. task_environment_.FastForwardBy(kShortDuration); EXPECT_TRUE(service_binding_->has_clients()) << "Expected delay: " << delay; } protected: base::test::TaskEnvironment task_environment_; base::TestComponentContextForProcess test_context_; TestMetricsRecorder test_recorder_; std::unique_ptr> service_binding_; LegacyMetricsClient client_; }; TEST_F(LegacyMetricsClientTest, ReportIntervalBoundary) { client_.Start(kReportInterval); task_environment_.FastForwardBy(kReportInterval - base::TimeDelta::FromSeconds(1)); EXPECT_FALSE(test_recorder_.IsRecordInFlight()); UMA_HISTOGRAM_COUNTS_1M("foo", 20); task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(1)); EXPECT_TRUE(test_recorder_.IsRecordInFlight()); } void PopulateAdditionalEvents( base::OnceCallback)> callback) { fuchsia::legacymetrics::ImplementationDefinedEvent impl_event; impl_event.set_name("baz"); fuchsia::legacymetrics::Event event; event.set_impl_defined_event(std::move(impl_event)); std::vector events; events.push_back(std::move(event)); std::move(callback).Run(std::move(events)); } TEST_F(LegacyMetricsClientTest, AllTypes) { client_.SetReportAdditionalMetricsCallback( base::BindRepeating(&PopulateAdditionalEvents)); client_.Start(kReportInterval); UMA_HISTOGRAM_COUNTS_1M("foo", 20); base::RecordComputedAction("bar"); task_environment_.FastForwardBy(kReportInterval); EXPECT_TRUE(test_recorder_.IsRecordInFlight()); auto events = test_recorder_.WaitForEvents(); EXPECT_EQ(3u, events.size()); EXPECT_EQ("baz", events[0].impl_defined_event().name()); EXPECT_EQ("foo", events[1].histogram().name()); EXPECT_EQ("bar", events[2].user_action_event().name()); } TEST_F(LegacyMetricsClientTest, DisconnectWhileCollectingAdditionalEvents) { // Hold the completion callback for later execution. base::OnceCallback)> on_report_done; client_.SetReportAdditionalMetricsCallback(base::BindRepeating( [](base::OnceCallback)>* stored_on_report_done, base::OnceCallback)> on_report_done) { *stored_on_report_done = std::move(on_report_done); }, base::Unretained(&on_report_done))); client_.Start(kReportInterval); task_environment_.FastForwardBy(kReportInterval); // Disconnect the service. service_binding_.reset(); base::RunLoop().RunUntilIdle(); // Fulfill the report additional metrics callback. std::move(on_report_done).Run({}); } TEST_F(LegacyMetricsClientTest, ReportSkippedNoEvents) { client_.Start(kReportInterval); // Verify that Record() is not invoked if there is no data to report. task_environment_.FastForwardBy(kReportInterval); EXPECT_FALSE(test_recorder_.IsRecordInFlight()); // Add some events and allow the interval to lapse. Verify that the data is // reported. UMA_HISTOGRAM_COUNTS_1M("foo", 20); task_environment_.FastForwardBy(kReportInterval); EXPECT_TRUE(test_recorder_.IsRecordInFlight()); test_recorder_.SendAck(); // Verify that Record() is skipped again for no-data. task_environment_.FastForwardBy(kReportInterval); EXPECT_FALSE(test_recorder_.IsRecordInFlight()); } TEST_F(LegacyMetricsClientTest, MultipleReports) { client_.Start(kReportInterval); UMA_HISTOGRAM_COUNTS_1M("foo", 20); task_environment_.FastForwardBy(kReportInterval); EXPECT_TRUE(test_recorder_.IsRecordInFlight()); test_recorder_.SendAck(); EXPECT_FALSE(test_recorder_.IsRecordInFlight()); UMA_HISTOGRAM_COUNTS_1M("foo", 20); task_environment_.FastForwardBy(kReportInterval); EXPECT_TRUE(test_recorder_.IsRecordInFlight()); test_recorder_.SendAck(); } TEST_F(LegacyMetricsClientTest, NoReportIfNeverAcked) { client_.Start(kReportInterval); UMA_HISTOGRAM_COUNTS_1M("foo", 20); task_environment_.FastForwardBy(kReportInterval); EXPECT_TRUE(test_recorder_.IsRecordInFlight()); test_recorder_.DropAck(); EXPECT_FALSE(test_recorder_.IsRecordInFlight()); UMA_HISTOGRAM_COUNTS_1M("foo", 20); task_environment_.FastForwardBy(kReportInterval); EXPECT_FALSE(test_recorder_.IsRecordInFlight()); } TEST_F(LegacyMetricsClientTest, ReconnectAfterServiceDisconnect) { StartClientAndExpectConnection(); DisconnectAndRestartMetricsService(); EXPECT_FALSE(service_binding_->has_clients()); task_environment_.FastForwardBy(LegacyMetricsClient::kInitialReconnectDelay); EXPECT_TRUE(service_binding_->has_clients()); } TEST_F(LegacyMetricsClientTest, ReconnectConsecutivelyWithoutRecordBacksOffExponentially) { StartClientAndExpectConnection(); for (base::TimeDelta expected_delay = LegacyMetricsClient::kInitialReconnectDelay; expected_delay <= LegacyMetricsClient::kMaxReconnectDelay; expected_delay *= LegacyMetricsClient::kReconnectBackoffFactor) { DisconnectAndRestartMetricsService(); ExpectReconnectAfterDelay(expected_delay); } } TEST_F(LegacyMetricsClientTest, ReconnectDelayNeverExceedsMax) { StartClientAndExpectConnection(); // Find the theoretical maximum number of consecutive failed connections. Also // add a few extra iterations to ensure that we never exceed the max delay. const size_t num_iterations = 3 + log(LegacyMetricsClient::kMaxReconnectDelay / LegacyMetricsClient::kInitialReconnectDelay) / log(LegacyMetricsClient::kReconnectBackoffFactor); // As a heuristic, starting with 1 second and a factor of 2 reaches 24 hours // in about 17 iterations. So the expected number of iterations needed to // reach the maximum delay should be less than about 20. EXPECT_LE(num_iterations, 20u); for (size_t i = 0; i < num_iterations; i++) { DisconnectAndRestartMetricsService(); EXPECT_FALSE(service_binding_->has_clients()) << "Iteration " << i; task_environment_.FastForwardBy(LegacyMetricsClient::kMaxReconnectDelay); EXPECT_TRUE(service_binding_->has_clients()) << "Iteration " << i; } } TEST_F(LegacyMetricsClientTest, RecordCompletionResetsReconnectDelay) { StartClientAndExpectConnection(); // First reconnect has initial delay. DisconnectAndRestartMetricsService(); ExpectReconnectAfterDelay(LegacyMetricsClient::kInitialReconnectDelay); // Another reconnect without a successful Record() call increases the delay. DisconnectAndRestartMetricsService(); ExpectReconnectAfterDelay(LegacyMetricsClient::kInitialReconnectDelay * LegacyMetricsClient::kReconnectBackoffFactor); // Record and report an event, invoking a FIDL Record(). base::RecordComputedAction("ArbitraryEvent"); task_environment_.FastForwardBy(kReportInterval); EXPECT_TRUE(test_recorder_.IsRecordInFlight()); test_recorder_.SendAck(); base::RunLoop().RunUntilIdle(); // Reconnect after a successful Record() uses the initial delay again. DisconnectAndRestartMetricsService(); ExpectReconnectAfterDelay(LegacyMetricsClient::kInitialReconnectDelay); } TEST_F(LegacyMetricsClientTest, ContinueRecordingUserActionsAfterDisconnect) { StartClientAndExpectConnection(); base::RecordComputedAction("BeforeDisconnect"); DisconnectAndRestartMetricsService(); base::RecordComputedAction("DuringDisconnect"); ExpectReconnectAfterDelay(LegacyMetricsClient::kInitialReconnectDelay); base::RecordComputedAction("AfterReconnect"); // Fast forward to report metrics. task_environment_.FastForwardBy(kReportInterval); EXPECT_TRUE(test_recorder_.IsRecordInFlight()); auto events = test_recorder_.WaitForEvents(); EXPECT_THAT( events, UnorderedElementsAreArray({ Property(&fuchsia::legacymetrics::Event::user_action_event, Property(&fuchsia::legacymetrics::UserActionEvent::name, "BeforeDisconnect")), Property(&fuchsia::legacymetrics::Event::user_action_event, Property(&fuchsia::legacymetrics::UserActionEvent::name, "DuringDisconnect")), Property(&fuchsia::legacymetrics::Event::user_action_event, Property(&fuchsia::legacymetrics::UserActionEvent::name, "AfterReconnect")), })); } TEST_F(LegacyMetricsClientTest, Batching) { client_.Start(kReportInterval); // Log enough actions that the list will be split across multiple batches. // Batches are read out in reverse order, so even though it is being logged // first, it will be emitted in the final batch. base::RecordComputedAction("batch2"); for (size_t i = 0; i < LegacyMetricsClient::kMaxBatchSize; ++i) base::RecordComputedAction("batch1"); task_environment_.FastForwardBy(kReportInterval); EXPECT_TRUE(test_recorder_.IsRecordInFlight()); // First batch. auto events = test_recorder_.WaitForEvents(); EXPECT_EQ(LegacyMetricsClient::kMaxBatchSize, events.size()); for (const auto& event : events) EXPECT_EQ(event.user_action_event().name(), "batch1"); test_recorder_.SendAck(); // Second batch (remainder). events = test_recorder_.WaitForEvents(); EXPECT_EQ(1u, events.size()); for (const auto& event : events) EXPECT_EQ(event.user_action_event().name(), "batch2"); test_recorder_.SendAck(); } TEST_F(LegacyMetricsClientTest, FlushWithPending) { client_.Start(kReportInterval); base::RunLoop().RunUntilIdle(); UMA_HISTOGRAM_COUNTS_1M("foo", 20); EXPECT_FALSE(test_recorder_.IsRecordInFlight()); service_binding_->events().OnCloseSoon(); base::RunLoop().RunUntilIdle(); EXPECT_TRUE(test_recorder_.IsRecordInFlight()); // The service should be unbound once all data is drained. EXPECT_TRUE(service_binding_->has_clients()); auto events = test_recorder_.WaitForEvents(); test_recorder_.SendAck(); base::RunLoop().RunUntilIdle(); EXPECT_EQ(1u, events.size()); EXPECT_EQ("foo", events[0].histogram().name()); EXPECT_FALSE(service_binding_->has_clients()); } TEST_F(LegacyMetricsClientTest, FlushNoData) { client_.Start(kReportInterval); base::RunLoop().RunUntilIdle(); EXPECT_TRUE(service_binding_->has_clients()); EXPECT_FALSE(test_recorder_.IsRecordInFlight()); service_binding_->events().OnCloseSoon(); base::RunLoop().RunUntilIdle(); EXPECT_FALSE(service_binding_->has_clients()); } TEST_F(LegacyMetricsClientTest, FlushWithOutstandingAck) { client_.Start(kReportInterval); base::RunLoop().RunUntilIdle(); // Send "foo", but don't ack. UMA_HISTOGRAM_COUNTS_1M("foo", 20); task_environment_.FastForwardBy(kReportInterval); EXPECT_TRUE(test_recorder_.IsRecordInFlight()); // Allow the flush operation to call Record() without waiting for a prior ack. test_recorder_.set_expect_ack_dropped(true); // Buffer another event and trigger a flush. UMA_HISTOGRAM_COUNTS_1M("bar", 20); EXPECT_TRUE(service_binding_->has_clients()); service_binding_->events().OnCloseSoon(); // Simulate an asynchronous ack from the recorder, which be delivered around // the same time as the flush's Record() call. The ack should be gracefully // ignored by the client. test_recorder_.SendAck(); base::RunLoop().RunUntilIdle(); auto events = test_recorder_.WaitForEvents(); test_recorder_.SendAck(); base::RunLoop().RunUntilIdle(); EXPECT_EQ(2u, events.size()); EXPECT_EQ("foo", events[0].histogram().name()); EXPECT_EQ("bar", events[1].histogram().name()); EXPECT_FALSE(service_binding_->has_clients()); } TEST_F(LegacyMetricsClientTest, ExternalFlushSignal) { ResultReceiver flush_receiver; client_.SetNotifyFlushCallback(flush_receiver.GetReceiveCallback()); client_.Start(kReportInterval); base::RunLoop().RunUntilIdle(); UMA_HISTOGRAM_COUNTS_1M("foo", 20); // Verify that reporting does not start until the flush completion callback is // run. EXPECT_FALSE(test_recorder_.IsRecordInFlight()); service_binding_->events().OnCloseSoon(); base::RunLoop().RunUntilIdle(); EXPECT_FALSE(test_recorder_.IsRecordInFlight()); // Verify that invoking the completion callback unblocks reporting. EXPECT_TRUE(flush_receiver.has_value()); std::move(*flush_receiver).Run(); base::RunLoop().RunUntilIdle(); EXPECT_TRUE(test_recorder_.IsRecordInFlight()); } TEST_F(LegacyMetricsClientTest, ExplicitFlush) { client_.Start(kReportInterval); base::RecordComputedAction("bar"); base::RunLoop().RunUntilIdle(); EXPECT_FALSE(test_recorder_.IsRecordInFlight()); bool called = false; client_.FlushAndDisconnect( base::BindLambdaForTesting([&called] { called = true; })); base::RunLoop().RunUntilIdle(); EXPECT_TRUE(test_recorder_.IsRecordInFlight()); EXPECT_FALSE(called); auto events = test_recorder_.WaitForEvents(); EXPECT_EQ(1u, events.size()); EXPECT_EQ("bar", events[0].user_action_event().name()); test_recorder_.SendAck(); base::RunLoop().RunUntilIdle(); EXPECT_TRUE(called); } TEST_F(LegacyMetricsClientTest, ExplicitFlushMultipleBatches) { const size_t kSizeForMultipleBatches = LegacyMetricsClient::kMaxBatchSize * 2; client_.Start(kReportInterval); for (size_t i = 0; i < kSizeForMultipleBatches; ++i) base::RecordComputedAction("bar"); client_.FlushAndDisconnect(base::DoNothing::Once()); base::RunLoop().RunUntilIdle(); test_recorder_.SendAck(); base::RunLoop().RunUntilIdle(); auto events = test_recorder_.WaitForEvents(); EXPECT_EQ(kSizeForMultipleBatches, events.size()); for (size_t i = 0; i < kSizeForMultipleBatches; ++i) EXPECT_EQ("bar", events[i].user_action_event().name()); } } // namespace } // namespace cr_fuchsia