// Copyright 2018 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "base/message_loop/message_pump.h" #include #include "base/functional/bind.h" #include "base/logging.h" #include "base/memory/raw_ptr.h" #include "base/message_loop/message_pump_for_io.h" #include "base/message_loop/message_pump_for_ui.h" #include "base/message_loop/message_pump_type.h" #include "base/run_loop.h" #include "base/task/single_thread_task_executor.h" #include "base/test/bind.h" #include "base/test/scoped_feature_list.h" #include "base/test/test_timeouts.h" #include "base/threading/thread.h" #include "build/build_config.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #if BUILDFLAG(IS_ANDROID) #include "base/android/input_hint_checker.h" #include "base/test/test_support_android.h" #endif #if BUILDFLAG(IS_WIN) #include #endif #if BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_NACL) #include "base/message_loop/message_pump_libevent.h" #endif using ::testing::_; using ::testing::AnyNumber; using ::testing::AtMost; using ::testing::Invoke; using ::testing::Return; namespace base { namespace { // On most platforms, the MessagePump impl controls when native work (e.g. // handling input messages) gets its turn. Tests below verify that by expecting // OnBeginWorkItem() calls that cover native work. In some configurations // however, the platform owns the message loop and is the one yielding to // Chrome's MessagePump to DoWork(). Under those configurations, it is not // possible to precisely account for OnBeginWorkItem() calls as they can occur // nondeterministically. For example, on some versions of iOS, the native loop // can surprisingly go through multiple cycles of // kCFRunLoopAfterWaiting=>kCFRunLoopBeforeWaiting before invoking Chrome's // RunWork() for the first time, triggering multiple ScopedDoWorkItem 's for // potential native work before the first DoWork(). constexpr bool ChromeControlsNativeEventProcessing(MessagePumpType pump_type) { #if BUILDFLAG(IS_MAC) return pump_type != MessagePumpType::UI; #elif BUILDFLAG(IS_IOS) return false; #else return true; #endif } class MockMessagePumpDelegate : public MessagePump::Delegate { public: explicit MockMessagePumpDelegate(MessagePumpType pump_type) : check_work_items_(ChromeControlsNativeEventProcessing(pump_type)), native_work_item_accounting_is_on_( !ChromeControlsNativeEventProcessing(pump_type)) {} ~MockMessagePumpDelegate() override { ValidateNoOpenWorkItems(); } MockMessagePumpDelegate(const MockMessagePumpDelegate&) = delete; MockMessagePumpDelegate& operator=(const MockMessagePumpDelegate&) = delete; void BeforeWait() override {} void BeginNativeWorkBeforeDoWork() override {} MOCK_METHOD0(DoWork, MessagePump::Delegate::NextWorkInfo()); MOCK_METHOD0(DoIdleWork, bool()); // Functions invoked directly by the message pump. void OnBeginWorkItem() override { any_work_begun_ = true; if (check_work_items_) { MockOnBeginWorkItem(); } ++work_item_count_; } void OnEndWorkItem(int run_level_depth) override { if (check_work_items_) { MockOnEndWorkItem(run_level_depth); } EXPECT_EQ(run_level_depth, work_item_count_); --work_item_count_; // It's not possible to close more scopes than there are open ones. EXPECT_GE(work_item_count_, 0); } int RunDepth() override { return work_item_count_; } void ValidateNoOpenWorkItems() { // Upon exiting there cannot be any open scopes. EXPECT_EQ(work_item_count_, 0); if (native_work_item_accounting_is_on_) { // Tests should trigger work beginning at least once except on iOS where // they need a call to MessagePumpUIApplication::Attach() to do so when on // the UI thread. #if !BUILDFLAG(IS_IOS) EXPECT_TRUE(any_work_begun_); #endif } } // Mock functions for asserting. MOCK_METHOD0(MockOnBeginWorkItem, void(void)); MOCK_METHOD1(MockOnEndWorkItem, void(int)); // If native events are covered in the current configuration it's not // possible to precisely test all assertions related to work items. This is // because a number of speculative WorkItems are created during execution of // such loops and it's not possible to determine their number before the // execution of the test. In such configurations the functioning of the // message pump is still verified by looking at the counts of opened and // closed WorkItems. const bool check_work_items_; const bool native_work_item_accounting_is_on_; int work_item_count_ = 0; bool any_work_begun_ = false; }; class MessagePumpTest : public ::testing::TestWithParam { public: MessagePumpTest() : message_pump_(MessagePump::Create(GetParam())) {} protected: #if defined(USE_GLIB) // Because of a GLIB implementation quirk, the pump doesn't do the same things // between each DoWork. In this case, it won't set/clear a ScopedDoWorkItem // because we run a chrome work item in the runloop outside of GLIB's control, // so we oscillate between setting and not setting PreDoWorkExpectations. std::map do_work_counts; #endif void AddPreDoWorkExpectations( testing::StrictMock& delegate) { #if BUILDFLAG(IS_WIN) if (GetParam() == MessagePumpType::UI) { // The Windows MessagePumpForUI may do native work from ::PeekMessage() // and labels itself as such. EXPECT_CALL(delegate, MockOnBeginWorkItem); EXPECT_CALL(delegate, MockOnEndWorkItem); // If the above event was MessagePumpForUI's own kMsgHaveWork internal // event, it will process another event to replace it (ref. // ProcessPumpReplacementMessage). EXPECT_CALL(delegate, MockOnBeginWorkItem).Times(AtMost(1)); EXPECT_CALL(delegate, MockOnEndWorkItem).Times(AtMost(1)); } #endif // BUILDFLAG(IS_WIN) #if defined(USE_GLIB) do_work_counts.try_emplace(&delegate, 0); if (GetParam() == MessagePumpType::UI) { if (++do_work_counts[&delegate] % 2) { // The GLib MessagePump will do native work before chrome work on // startup. EXPECT_CALL(delegate, MockOnBeginWorkItem); EXPECT_CALL(delegate, MockOnEndWorkItem); } } #endif // defined(USE_GLIB) } void AddPostDoWorkExpectations( testing::StrictMock& delegate) { #if BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_NACL) // MessagePumpLibEvent checks for native notifications once after processing // a DoWork() but only instantiates a ScopedDoWorkItem that triggers // MessagePumpLibevent::OnLibeventNotification() which this test does not // so there are no post-work expectations at the moment. #endif #if defined(USE_GLIB) if (GetParam() == MessagePumpType::UI) { // The GLib MessagePump can create and destroy work items between DoWorks // depending on internal state. EXPECT_CALL(delegate, MockOnBeginWorkItem).Times(AtMost(1)); EXPECT_CALL(delegate, MockOnEndWorkItem).Times(AtMost(1)); } #endif // defined(USE_GLIB) } std::unique_ptr message_pump_; }; } // namespace TEST_P(MessagePumpTest, QuitStopsWork) { testing::InSequence sequence; testing::StrictMock delegate(GetParam()); AddPreDoWorkExpectations(delegate); // Not expecting any calls to DoIdleWork after quitting, nor any of the // PostDoWorkExpectations, quitting should be instantaneous. EXPECT_CALL(delegate, DoWork).WillOnce(Invoke([this] { message_pump_->Quit(); return MessagePump::Delegate::NextWorkInfo{TimeTicks::Max()}; })); // MessagePumpGlib uses a work item between a HandleDispatch() call and // passing control back to the chrome loop, which handles the Quit() despite // us not necessarily doing any native work during that time. #if defined(USE_GLIB) if (GetParam() == MessagePumpType::UI) { AddPostDoWorkExpectations(delegate); } #endif EXPECT_CALL(delegate, DoIdleWork()).Times(0); message_pump_->ScheduleWork(); message_pump_->Run(&delegate); } #if BUILDFLAG(IS_ANDROID) class MockInputHintChecker : public android::InputHintChecker { public: MOCK_METHOD(bool, HasInputImplWithThrottling, (), (override)); }; TEST_P(MessagePumpTest, DetectingHasInputYieldsOnUi) { testing::InSequence sequence; MessagePumpType pump_type = GetParam(); testing::StrictMock delegate(pump_type); testing::StrictMock hint_checker_mock; android::InputHintChecker::ScopedOverrideInstance scoped_override_hint( &hint_checker_mock); base::test::ScopedFeatureList scoped_feature_list; scoped_feature_list.InitAndEnableFeature(android::kYieldWithInputHint); android::InputHintChecker::InitializeFeatures(); uint32_t initial_work_enters = GetAndroidNonDelayedWorkEnterCount(); // Override the first DoWork() to return an immediate next. EXPECT_CALL(delegate, DoWork).WillOnce(Invoke([] { auto work_info = MessagePump::Delegate::NextWorkInfo{.delayed_run_time = TimeTicks()}; CHECK(work_info.is_immediate()); return work_info; })); if (pump_type == MessagePumpType::UI) { // Override the following InputHintChecker::HasInput() to return true. EXPECT_CALL(hint_checker_mock, HasInputImplWithThrottling()) .WillOnce(Invoke([] { return true; })); } // Override the second DoWork() to quit the loop. EXPECT_CALL(delegate, DoWork).WillOnce(Invoke([this] { message_pump_->Quit(); return MessagePump::Delegate::NextWorkInfo{.delayed_run_time = TimeTicks::Max()}; })); if (pump_type == MessagePumpType::UI) { EXPECT_CALL(hint_checker_mock, HasInputImplWithThrottling()) .WillOnce(Return(false)); } EXPECT_CALL(delegate, DoIdleWork()).Times(0); message_pump_->Run(&delegate); // Expect two calls to DoNonDelayedLooperWork(). The first one occurs as a // result of MessagePump::Run(). The second one is the result of yielding // after HasInput() returns true. For non-UI MessagePumpType the // MessagePump::Create() does not intercept entering DoNonDelayedLooperWork(), // so it remains 0 instead of 1. uint32_t work_loop_entered = (pump_type == MessagePumpType::UI) ? 2 : 0; EXPECT_EQ(initial_work_enters + work_loop_entered, GetAndroidNonDelayedWorkEnterCount()); } #endif TEST_P(MessagePumpTest, QuitStopsWorkWithNestedRunLoop) { testing::InSequence sequence; testing::StrictMock delegate(GetParam()); testing::StrictMock nested_delegate(GetParam()); AddPreDoWorkExpectations(delegate); // We first schedule a call to DoWork, which runs a nested run loop. After // the nested loop exits, we schedule another DoWork which quits the outer // (original) run loop. The test verifies that there are no extra calls to // DoWork after the outer loop quits. EXPECT_CALL(delegate, DoWork).WillOnce(Invoke([&] { message_pump_->Run(&nested_delegate); // A null NextWorkInfo indicates immediate follow-up work. return MessagePump::Delegate::NextWorkInfo(); })); AddPreDoWorkExpectations(nested_delegate); EXPECT_CALL(nested_delegate, DoWork).WillOnce(Invoke([&] { // Quit the nested run loop. message_pump_->Quit(); // The underlying pump should process the next task in the first run-level // regardless of whether the nested run-level indicates there's no more work // (e.g. can happen when the only remaining tasks are non-nestable). return MessagePump::Delegate::NextWorkInfo{TimeTicks::Max()}; })); // The `nested_delegate` will quit first. AddPostDoWorkExpectations(nested_delegate); // Return a delayed task with |yield_to_native| set, and exit. AddPostDoWorkExpectations(delegate); AddPreDoWorkExpectations(delegate); EXPECT_CALL(delegate, DoWork).WillOnce(Invoke([this] { message_pump_->Quit(); return MessagePump::Delegate::NextWorkInfo{TimeTicks::Max()}; })); message_pump_->ScheduleWork(); message_pump_->Run(&delegate); } TEST_P(MessagePumpTest, YieldToNativeRequestedSmokeTest) { // The handling of the "yield_to_native" boolean in the NextWorkInfo is only // implemented on the MessagePumpAndroid. However since we inject a fake one // for testing this is hard to test. This test ensures that setting this // boolean doesn't cause any MessagePump to explode. testing::StrictMock delegate(GetParam()); testing::InSequence sequence; // Return an immediate task with |yield_to_native| set. AddPreDoWorkExpectations(delegate); EXPECT_CALL(delegate, DoWork).WillOnce(Invoke([] { return MessagePump::Delegate::NextWorkInfo{TimeTicks(), TimeDelta(), TimeTicks(), /* yield_to_native = */ true}; })); AddPostDoWorkExpectations(delegate); AddPreDoWorkExpectations(delegate); // Return a delayed task with |yield_to_native| set, and exit. EXPECT_CALL(delegate, DoWork).WillOnce(Invoke([this] { message_pump_->Quit(); auto now = TimeTicks::Now(); return MessagePump::Delegate::NextWorkInfo{now + Milliseconds(1), TimeDelta(), now, true}; })); EXPECT_CALL(delegate, DoIdleWork()).Times(AnyNumber()); message_pump_->ScheduleWork(); message_pump_->Run(&delegate); } TEST_P(MessagePumpTest, LeewaySmokeTest) { // The handling of the "leeway" in the NextWorkInfo is only implemented on // mac. However since we inject a fake one for testing this is hard to test. // This test ensures that setting this boolean doesn't cause any MessagePump // to explode. testing::StrictMock delegate(GetParam()); testing::InSequence sequence; AddPreDoWorkExpectations(delegate); // Return a delayed task with |yield_to_native| set, and exit. EXPECT_CALL(delegate, DoWork).WillOnce(Invoke([this] { message_pump_->Quit(); auto now = TimeTicks::Now(); return MessagePump::Delegate::NextWorkInfo{now + Milliseconds(1), Milliseconds(8), now}; })); EXPECT_CALL(delegate, DoIdleWork()).Times(AnyNumber()); message_pump_->ScheduleWork(); message_pump_->Run(&delegate); } TEST_P(MessagePumpTest, RunWithoutScheduleWorkInvokesDoWork) { testing::InSequence sequence; testing::StrictMock delegate(GetParam()); AddPreDoWorkExpectations(delegate); EXPECT_CALL(delegate, DoWork).WillOnce(Invoke([this] { message_pump_->Quit(); return MessagePump::Delegate::NextWorkInfo{TimeTicks::Max()}; })); AddPostDoWorkExpectations(delegate); #if BUILDFLAG(IS_IOS) EXPECT_CALL(delegate, DoIdleWork).Times(AnyNumber()); #endif message_pump_->Run(&delegate); } TEST_P(MessagePumpTest, NestedRunWithoutScheduleWorkInvokesDoWork) { testing::InSequence sequence; testing::StrictMock delegate(GetParam()); testing::StrictMock nested_delegate(GetParam()); AddPreDoWorkExpectations(delegate); EXPECT_CALL(delegate, DoWork).WillOnce(Invoke([this, &nested_delegate] { message_pump_->Run(&nested_delegate); message_pump_->Quit(); return MessagePump::Delegate::NextWorkInfo{TimeTicks::Max()}; })); AddPreDoWorkExpectations(nested_delegate); EXPECT_CALL(nested_delegate, DoWork).WillOnce(Invoke([this] { message_pump_->Quit(); return MessagePump::Delegate::NextWorkInfo{TimeTicks::Max()}; })); // We quit `nested_delegate` before `delegate` AddPostDoWorkExpectations(nested_delegate); AddPostDoWorkExpectations(delegate); #if BUILDFLAG(IS_IOS) EXPECT_CALL(nested_delegate, DoIdleWork).Times(AnyNumber()); EXPECT_CALL(delegate, DoIdleWork).Times(AnyNumber()); #endif message_pump_->Run(&delegate); } INSTANTIATE_TEST_SUITE_P(All, MessagePumpTest, ::testing::Values(MessagePumpType::DEFAULT, MessagePumpType::UI, MessagePumpType::IO)); } // namespace base