// Copyright 2014 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. #import "ui/gfx/test/ui_cocoa_test_helper.h" #include "base/debug/debugger.h" #include "base/logging.h" #include "base/stl_util.h" #include "base/test/mock_chrome_application_mac.h" #include "base/test/test_timeouts.h" namespace { // Some AppKit function leak intentionally, e.g. for caching purposes. // Force those leaks here, so there can be a unique calling path, allowing // to flag intentional leaks without having to suppress all calls to // potentially leaky functions. void NOINLINE ForceSystemLeaks() { // If a test suite hasn't already initialized NSApp, register the mock one // now. if (!NSApp) mock_cr_app::RegisterMockCrApp(); // First NSCursor push always leaks. [[NSCursor openHandCursor] push]; [NSCursor pop]; } } // namespace. @implementation CocoaTestHelperWindow - (id)initWithContentRect:(NSRect)contentRect { return [self initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]; } - (id)init { return [self initWithContentRect:NSMakeRect(0, 0, 800, 600)]; } - (void)dealloc { // Just a good place to put breakpoints when having problems with // unittests and CocoaTestHelperWindow. [super dealloc]; } - (void)makePretendKeyWindowAndSetFirstResponder:(NSResponder*)responder { EXPECT_TRUE([self makeFirstResponder:responder]); [self setPretendIsKeyWindow:YES]; } - (void)clearPretendKeyWindowAndFirstResponder { [self setPretendIsKeyWindow:NO]; EXPECT_TRUE([self makeFirstResponder:NSApp]); } - (void)setPretendIsKeyWindow:(BOOL)flag { pretendIsKeyWindow_ = flag; } - (BOOL)isKeyWindow { return pretendIsKeyWindow_; } @end namespace ui { CocoaTest::CocoaTest() : called_tear_down_(false), test_window_(nil) { ForceSystemLeaks(); Init(); } CocoaTest::~CocoaTest() { // Must call CocoaTest's teardown from your overrides. DCHECK(called_tear_down_); } void CocoaTest::Init() { // Set the duration of AppKit-evaluated animations (such as frame changes) // to zero for testing purposes. That way they take effect immediately. [[NSAnimationContext currentContext] setDuration:0.0]; // The above does not affect window-resize time, such as for an // attached sheet dropping in. Set that duration for the current // process (this is not persisted). Empirically, the value of 0.0 // is ignored. NSDictionary* dict = [NSDictionary dictionaryWithObject:@"0.01" forKey:@"NSWindowResizeTime"]; [[NSUserDefaults standardUserDefaults] registerDefaults:dict]; // Collect the list of windows that were open when the test started so // that we don't wait for them to close in TearDown. Has to be done // after BootstrapCocoa is called. initial_windows_ = ApplicationWindows(); } void CocoaTest::TearDown() { called_tear_down_ = true; // Call close on our test_window to clean it up if one was opened. [test_window_ clearPretendKeyWindowAndFirstResponder]; [test_window_ close]; test_window_ = nil; // Recycle the pool to clean up any stuff that was put on the // autorelease pool due to window or windowcontroller closures. pool_.Recycle(); // Some controls (NSTextFields, NSComboboxes etc) use // performSelector:withDelay: to clean up drag handlers and other // things (Radar 5851458 "Closing a window with a NSTextView in it // should get rid of it immediately"). The event loop must be spun // to get everything cleaned up correctly. It normally only takes // one to two spins through the event loop to see a change. // NOTE(shess): Under valgrind, -nextEventMatchingMask:* in one test // needed to run twice, once taking .2 seconds, the next time .6 // seconds. The loop exit condition attempts to be scalable. // Get the set of windows which weren't present when the test // started. std::set windows_left(WindowsLeft()); while (!windows_left.empty()) { // Cover delayed actions by spinning the loop at least once after // this timeout. const NSTimeInterval kCloseTimeoutSeconds = TestTimeouts::action_timeout().InSecondsF(); // Cover chains of delayed actions by spinning the loop at least // this many times. const int kCloseSpins = 3; // Track the set of remaining windows so that everything can be // reset if progress is made. std::set still_left = windows_left; NSDate* start_date = [NSDate date]; bool one_more_time = true; int spins = 0; while (still_left.size() == windows_left.size() && (spins < kCloseSpins || one_more_time)) { // Check the timeout before pumping events, so that we'll spin // the loop once after the timeout. one_more_time = ([start_date timeIntervalSinceNow] > -kCloseTimeoutSeconds); // Autorelease anything thrown up by the event loop. { base::mac::ScopedNSAutoreleasePool pool; ++spins; NSEvent *next_event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:nil inMode:NSDefaultRunLoopMode dequeue:YES]; [NSApp sendEvent:next_event]; [NSApp updateWindows]; } // Refresh the outstanding windows. still_left = WindowsLeft(); } // If no progress is being made, log a failure and continue. if (still_left.size() == windows_left.size()) { // NOTE(shess): Failing this expectation means that the test // opened windows which have not been fully released. Either // there is a leak, or perhaps one of |kCloseTimeoutSeconds| or // |kCloseSpins| needs adjustment. EXPECT_EQ(0U, windows_left.size()); for (std::set::iterator iter = windows_left.begin(); iter != windows_left.end(); ++iter) { const char* desc = [[*iter description] UTF8String]; LOG(WARNING) << "Didn't close window " << desc; } break; } windows_left = still_left; } PlatformTest::TearDown(); } std::set CocoaTest::ApplicationWindows() { // This must NOT retain the windows it is returning. std::set windows; // Must create a pool here because [NSApp windows] has created an array // with retains on all the windows in it. base::mac::ScopedNSAutoreleasePool pool; NSArray *appWindows = [NSApp windows]; for (NSWindow *window in appWindows) { windows.insert(window); } return windows; } std::set CocoaTest::WindowsLeft() { const std::set windows(ApplicationWindows()); std::set windows_left = base::STLSetDifference >(windows, initial_windows_); return windows_left; } CocoaTestHelperWindow* CocoaTest::test_window() { if (!test_window_) { test_window_ = [[CocoaTestHelperWindow alloc] init]; if (base::debug::BeingDebugged()) { [test_window_ orderFront:nil]; } else { [test_window_ orderBack:nil]; } } return test_window_; } } // namespace ui