blob: f24f827157e74aa18a623897a7a54a635422a2ae [file]
// Copyright 2017 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_apple.h"
#import <AppKit/AppKit.h>
#include <utility>
#include "base/cancelable_callback.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/task/current_thread.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/scoped_run_loop_timeout.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "testing/gtest/include/gtest/gtest.h"
@interface TestModalAlertCloser : NSObject
- (void)runTestThenCloseAlert:(NSAlert*)alert;
@end
namespace {
// Internal constants from message_pump_apple.mm.
constexpr int kAllModesMask = 0b0000'0111;
constexpr int kNSApplicationModalSafeModeMask = 0b0000'0001;
} // namespace
namespace base {
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 function posts |task| and runs the given |mode|.
void RunTaskInMode(CFRunLoopMode mode, OnceClosure task) {
// Since this task is "ours" rather than a system task, allow nesting.
CurrentThread::ScopedAllowApplicationTasksInNativeNestedLoop allow;
CancelableOnceClosure cancelable(std::move(task));
SingleThreadTaskRunner::GetCurrentDefault()->PostTask(FROM_HERE,
cancelable.callback());
while (CFRunLoopRunInMode(mode, 0, true) == kCFRunLoopRunHandledSource)
;
}
} // namespace
// Tests the correct behavior of ScopedPumpMessagesInPrivateModes.
TEST(MessagePumpAppleTest, ScopedPumpMessagesInPrivateModes) {
test::SingleThreadTaskEnvironment task_environment(
test::SingleThreadTaskEnvironment::MainThreadType::UI);
CFRunLoopMode kRegular = kCFRunLoopDefaultMode;
CFRunLoopMode kPrivate = CFSTR("NSUnhighlightMenuRunLoopMode");
// Work is seen when running in the default mode.
SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
BindOnce(&RunTaskInMode, kRegular, MakeExpectedRunClosure(FROM_HERE)));
EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle());
// But not seen when running in a private mode.
SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
BindOnce(&RunTaskInMode, kPrivate, MakeExpectedNotRunClosure(FROM_HERE)));
EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle());
{
ScopedPumpMessagesInPrivateModes allow_private;
// Now the work should be seen.
SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
BindOnce(&RunTaskInMode, kPrivate, MakeExpectedRunClosure(FROM_HERE)));
EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle());
// The regular mode should also work the same.
SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
BindOnce(&RunTaskInMode, kRegular, MakeExpectedRunClosure(FROM_HERE)));
EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle());
}
// And now the scoper is out of scope, private modes should no longer see it.
SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
BindOnce(&RunTaskInMode, kPrivate, MakeExpectedNotRunClosure(FROM_HERE)));
EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle());
// Only regular modes see it.
SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
BindOnce(&RunTaskInMode, kRegular, MakeExpectedRunClosure(FROM_HERE)));
EXPECT_NO_FATAL_FAILURE(RunLoop().RunUntilIdle());
}
// Tests that private message loop modes are not pumped while a modal dialog is
// present.
TEST(MessagePumpAppleTest, ScopedPumpMessagesAttemptWithModalDialog) {
test::SingleThreadTaskEnvironment task_environment(
test::SingleThreadTaskEnvironment::MainThreadType::UI);
{
base::ScopedPumpMessagesInPrivateModes allow_private;
// No modal window, so all modes should be pumped.
EXPECT_EQ(kAllModesMask, allow_private.GetModeMaskForTest());
}
NSAlert* alert = [[NSAlert alloc] init];
[alert addButtonWithTitle:@"OK"];
TestModalAlertCloser* closer = [[TestModalAlertCloser alloc] init];
[closer performSelector:@selector(runTestThenCloseAlert:)
withObject:alert
afterDelay:0
inModes:@[ NSModalPanelRunLoopMode ]];
NSInteger result = [alert runModal];
EXPECT_EQ(NSAlertFirstButtonReturn, result);
}
TEST(MessagePumpAppleTest, QuitWithModalWindow) {
test::SingleThreadTaskEnvironment task_environment(
test::SingleThreadTaskEnvironment::MainThreadType::UI);
NSWindow* window =
[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 100, 100)
styleMask:NSWindowStyleMaskBorderless
backing:NSBackingStoreBuffered
defer:NO];
window.releasedWhenClosed = NO;
// Check that quitting the run loop while a modal window is shown applies to
// |run_loop| rather than the internal NSApplication modal run loop.
RunLoop run_loop;
SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindLambdaForTesting([&] {
CurrentThread::ScopedAllowApplicationTasksInNativeNestedLoop allow;
ScopedPumpMessagesInPrivateModes pump_private;
[NSApp runModalForWindow:window];
}));
SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindLambdaForTesting([&] {
[NSApp stopModal];
run_loop.Quit();
}));
EXPECT_NO_FATAL_FAILURE(run_loop.Run());
}
// Regression test for a crash where a nested run loop started from a
// PreWaitObserver callback (e.g. DoIdleWork) caused an imbalance in the work
// item scope stack. The crash happened because the nested loop's Entry/Exit
// observers pushed/popped scopes correctly, but the nested loop's
// AfterWaitObserver (if triggered) would push a scope that wasn't popped by a
// corresponding PreWaitObserver (because the outer PreWaitObserver popped the
// outer scope, not the nested one). The fix involves tracking which nesting
// levels have actually slept.
TEST(MessagePumpAppleTest, DirectNestedRunInIdleWork) {
auto pump = MessagePump::Create(MessagePumpType::UI);
class NestedDelegate : public MessagePump::Delegate {
public:
explicit NestedDelegate(MessagePump* pump) : pump_(pump) {}
void BeforeWait() override {}
void BeginNativeWorkBeforeDoWork() override {}
int RunDepth() override { return is_nested_ ? 2 : 1; }
void OnBeginWorkItem() override {}
void OnEndWorkItem(int) override {}
NextWorkInfo DoWork() override {
if (is_nested_) {
pump_->Quit();
}
return NextWorkInfo{TimeTicks::Max()};
}
void DoIdleWork() override {
if (!was_nested_) {
was_nested_ = true;
is_nested_ = true;
pump_->Run(this);
is_nested_ = false;
pump_->Quit();
}
}
raw_ptr<MessagePump> pump_;
bool was_nested_ = false;
bool is_nested_ = false;
} delegate(pump.get());
pump->Run(&delegate);
EXPECT_TRUE(delegate.was_nested_);
}
} // 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