|  | // Copyright 2017 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 "base/message_loop/message_pump_mac.h" | 
|  |  | 
|  | #include "base/mac/scoped_cftyperef.h" | 
|  | #import "base/mac/scoped_nsobject.h" | 
|  | #include "base/macros.h" | 
|  | #include "base/message_loop/message_loop.h" | 
|  | #include "base/message_loop/message_loop_current.h" | 
|  | #include "base/threading/thread_task_runner_handle.h" | 
|  | #include "testing/gtest/include/gtest/gtest.h" | 
|  |  | 
|  | @interface TestModalAlertCloser : NSObject | 
|  | - (void)runTestThenCloseAlert:(NSAlert*)alert; | 
|  | @end | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | // Internal constants from message_pump_mac.mm. | 
|  | constexpr int kAllModesMask = 0xf; | 
|  | constexpr int kNSApplicationModalSafeModeMask = 0x3; | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | namespace base { | 
|  |  | 
|  | class TestMessagePumpCFRunLoopBase { | 
|  | public: | 
|  | bool TestCanInvalidateTimers() { | 
|  | return MessagePumpCFRunLoopBase::CanInvalidateCFRunLoopTimers(); | 
|  | } | 
|  | static void SetTimerValid(CFRunLoopTimerRef timer, bool valid) { | 
|  | MessagePumpCFRunLoopBase::ChromeCFRunLoopTimerSetValid(timer, valid); | 
|  | } | 
|  |  | 
|  | static void PerformTimerCallback(CFRunLoopTimerRef timer, void* info) { | 
|  | TestMessagePumpCFRunLoopBase* self = | 
|  | static_cast<TestMessagePumpCFRunLoopBase*>(info); | 
|  | self->timer_callback_called_ = true; | 
|  |  | 
|  | if (self->invalidate_timer_in_callback_) { | 
|  | SetTimerValid(timer, false); | 
|  | } | 
|  | } | 
|  |  | 
|  | bool invalidate_timer_in_callback_; | 
|  |  | 
|  | bool timer_callback_called_; | 
|  | }; | 
|  |  | 
|  | TEST(MessagePumpMacTest, TestCanInvalidateTimers) { | 
|  | TestMessagePumpCFRunLoopBase message_pump_test; | 
|  |  | 
|  | // Catch whether or not the use of private API ever starts failing. | 
|  | EXPECT_TRUE(message_pump_test.TestCanInvalidateTimers()); | 
|  | } | 
|  |  | 
|  | TEST(MessagePumpMacTest, TestInvalidatedTimerReuse) { | 
|  | TestMessagePumpCFRunLoopBase message_pump_test; | 
|  |  | 
|  | CFRunLoopTimerContext timer_context = CFRunLoopTimerContext(); | 
|  | timer_context.info = &message_pump_test; | 
|  | const CFTimeInterval kCFTimeIntervalMax = | 
|  | std::numeric_limits<CFTimeInterval>::max(); | 
|  | ScopedCFTypeRef<CFRunLoopTimerRef> test_timer(CFRunLoopTimerCreate( | 
|  | NULL,                // allocator | 
|  | kCFTimeIntervalMax,  // fire time | 
|  | kCFTimeIntervalMax,  // interval | 
|  | 0,                   // flags | 
|  | 0,                   // priority | 
|  | TestMessagePumpCFRunLoopBase::PerformTimerCallback, &timer_context)); | 
|  | CFRunLoopAddTimer(CFRunLoopGetCurrent(), test_timer, | 
|  | kMessageLoopExclusiveRunLoopMode); | 
|  |  | 
|  | // Sanity check. | 
|  | EXPECT_TRUE(CFRunLoopTimerIsValid(test_timer)); | 
|  |  | 
|  | // Confirm that the timer fires as expected, and that it's not a one-time-use | 
|  | // timer (those timers are invalidated after they fire). | 
|  | CFAbsoluteTime next_fire_time = CFAbsoluteTimeGetCurrent() + 0.01; | 
|  | CFRunLoopTimerSetNextFireDate(test_timer, next_fire_time); | 
|  | message_pump_test.timer_callback_called_ = false; | 
|  | message_pump_test.invalidate_timer_in_callback_ = false; | 
|  | CFRunLoopRunInMode(kMessageLoopExclusiveRunLoopMode, 0.02, true); | 
|  | EXPECT_TRUE(message_pump_test.timer_callback_called_); | 
|  | EXPECT_TRUE(CFRunLoopTimerIsValid(test_timer)); | 
|  |  | 
|  | // As a repeating timer, the timer should have a new fire date set in the | 
|  | // future. | 
|  | EXPECT_GT(CFRunLoopTimerGetNextFireDate(test_timer), next_fire_time); | 
|  |  | 
|  | // Try firing the timer, and invalidating it within its callback. | 
|  | next_fire_time = CFAbsoluteTimeGetCurrent() + 0.01; | 
|  | CFRunLoopTimerSetNextFireDate(test_timer, next_fire_time); | 
|  | message_pump_test.timer_callback_called_ = false; | 
|  | message_pump_test.invalidate_timer_in_callback_ = true; | 
|  | CFRunLoopRunInMode(kMessageLoopExclusiveRunLoopMode, 0.02, true); | 
|  | EXPECT_TRUE(message_pump_test.timer_callback_called_); | 
|  | EXPECT_FALSE(CFRunLoopTimerIsValid(test_timer)); | 
|  |  | 
|  | // The CFRunLoop believes the timer is invalid, so it should not have a | 
|  | // fire date. | 
|  | EXPECT_EQ(0, CFRunLoopTimerGetNextFireDate(test_timer)); | 
|  |  | 
|  | // Now mark the timer as valid and confirm that it still fires correctly. | 
|  | TestMessagePumpCFRunLoopBase::SetTimerValid(test_timer, true); | 
|  | EXPECT_TRUE(CFRunLoopTimerIsValid(test_timer)); | 
|  | next_fire_time = CFAbsoluteTimeGetCurrent() + 0.01; | 
|  | CFRunLoopTimerSetNextFireDate(test_timer, next_fire_time); | 
|  | message_pump_test.timer_callback_called_ = false; | 
|  | message_pump_test.invalidate_timer_in_callback_ = false; | 
|  | CFRunLoopRunInMode(kMessageLoopExclusiveRunLoopMode, 0.02, true); | 
|  | EXPECT_TRUE(message_pump_test.timer_callback_called_); | 
|  | EXPECT_TRUE(CFRunLoopTimerIsValid(test_timer)); | 
|  |  | 
|  | // Confirm that the run loop again gave it a new fire date in the future. | 
|  | EXPECT_GT(CFRunLoopTimerGetNextFireDate(test_timer), next_fire_time); | 
|  |  | 
|  | CFRunLoopRemoveTimer(CFRunLoopGetCurrent(), test_timer, | 
|  | kMessageLoopExclusiveRunLoopMode); | 
|  | } | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | // PostedTasks are only executed while the message pump has a delegate. That is, | 
|  | // when a base::RunLoop is running, so in order to test whether posted tasks | 
|  | // are run by CFRunLoopRunInMode and *not* by the regular RunLoop, we need to | 
|  | // be inside a task that is also calling CFRunLoopRunInMode. This task runs the | 
|  | // given |mode| after posting a task to increment a counter, then checks whether | 
|  | // the counter incremented after emptying that run loop mode. | 
|  | void IncrementInModeAndExpect(CFRunLoopMode mode, int result) { | 
|  | // Since this task is "ours" rather than a system task, allow nesting. | 
|  | MessageLoopCurrent::ScopedNestableTaskAllower allow; | 
|  | int counter = 0; | 
|  | auto increment = BindRepeating([](int* i) { ++*i; }, &counter); | 
|  | ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, increment); | 
|  | while (CFRunLoopRunInMode(mode, 0, true) == kCFRunLoopRunHandledSource) | 
|  | ; | 
|  | ASSERT_EQ(result, counter); | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | // Tests the correct behavior of ScopedPumpMessagesInPrivateModes. | 
|  | TEST(MessagePumpMacTest, ScopedPumpMessagesInPrivateModes) { | 
|  | MessageLoopForUI message_loop; | 
|  |  | 
|  | CFRunLoopMode kRegular = kCFRunLoopDefaultMode; | 
|  | CFRunLoopMode kPrivate = CFSTR("NSUnhighlightMenuRunLoopMode"); | 
|  |  | 
|  | // Work is seen when running in the default mode. | 
|  | ThreadTaskRunnerHandle::Get()->PostTask( | 
|  | FROM_HERE, BindOnce(&IncrementInModeAndExpect, kRegular, 1)); | 
|  | EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle()); | 
|  |  | 
|  | // But not seen when running in a private mode. | 
|  | ThreadTaskRunnerHandle::Get()->PostTask( | 
|  | FROM_HERE, BindOnce(&IncrementInModeAndExpect, kPrivate, 0)); | 
|  | EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle()); | 
|  |  | 
|  | { | 
|  | ScopedPumpMessagesInPrivateModes allow_private; | 
|  | // Now the work should be seen. | 
|  | ThreadTaskRunnerHandle::Get()->PostTask( | 
|  | FROM_HERE, BindOnce(&IncrementInModeAndExpect, kPrivate, 1)); | 
|  | EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle()); | 
|  |  | 
|  | // The regular mode should also work the same. | 
|  | ThreadTaskRunnerHandle::Get()->PostTask( | 
|  | FROM_HERE, BindOnce(&IncrementInModeAndExpect, kRegular, 1)); | 
|  | EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle()); | 
|  | } | 
|  |  | 
|  | // And now the scoper is out of scope, private modes should no longer see it. | 
|  | ThreadTaskRunnerHandle::Get()->PostTask( | 
|  | FROM_HERE, BindOnce(&IncrementInModeAndExpect, kPrivate, 0)); | 
|  | EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle()); | 
|  |  | 
|  | // Only regular modes see it. | 
|  | ThreadTaskRunnerHandle::Get()->PostTask( | 
|  | FROM_HERE, BindOnce(&IncrementInModeAndExpect, kRegular, 1)); | 
|  | EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle()); | 
|  | } | 
|  |  | 
|  | // Tests that private message loop modes are not pumped while a modal dialog is | 
|  | // present. | 
|  | TEST(MessagePumpMacTest, ScopedPumpMessagesAttemptWithModalDialog) { | 
|  | MessageLoopForUI message_loop; | 
|  |  | 
|  | { | 
|  | base::ScopedPumpMessagesInPrivateModes allow_private; | 
|  | // No modal window, so all modes should be pumped. | 
|  | EXPECT_EQ(kAllModesMask, allow_private.GetModeMaskForTest()); | 
|  | } | 
|  |  | 
|  | base::scoped_nsobject<NSAlert> alert([[NSAlert alloc] init]); | 
|  | [alert addButtonWithTitle:@"OK"]; | 
|  | base::scoped_nsobject<TestModalAlertCloser> closer( | 
|  | [[TestModalAlertCloser alloc] init]); | 
|  | [closer performSelector:@selector(runTestThenCloseAlert:) | 
|  | withObject:alert | 
|  | afterDelay:0 | 
|  | inModes:@[ NSModalPanelRunLoopMode ]]; | 
|  | NSInteger result = [alert runModal]; | 
|  | EXPECT_EQ(NSAlertFirstButtonReturn, result); | 
|  | } | 
|  |  | 
|  | }  // namespace base | 
|  |  | 
|  | @implementation TestModalAlertCloser | 
|  |  | 
|  | - (void)runTestThenCloseAlert:(NSAlert*)alert { | 
|  | EXPECT_TRUE([NSApp modalWindow]); | 
|  | { | 
|  | base::ScopedPumpMessagesInPrivateModes allow_private; | 
|  | // With a modal window, only safe modes should be pumped. | 
|  | EXPECT_EQ(kNSApplicationModalSafeModeMask, | 
|  | allow_private.GetModeMaskForTest()); | 
|  | } | 
|  | [[alert buttons][0] performClick:nil]; | 
|  | } | 
|  |  | 
|  | @end |