| // 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 |