1 // Copyright 2014 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #import "ui/gfx/test/ui_cocoa_test_helper.h" 6 7 #include "base/debug/debugger.h" 8 #include "base/logging.h" 9 #include "base/stl_util.h" 10 #include "base/test/test_timeouts.h" 11 12 namespace { 13 14 // Some AppKit function leak intentionally, e.g. for caching purposes. 15 // Force those leaks here, so there can be a unique calling path, allowing 16 // to flag intentional leaks without having to suppress all calls to 17 // potentially leaky functions. 18 void NOINLINE ForceSystemLeaks() { 19 // First NSCursor push always leaks. 20 [[NSCursor openHandCursor] push]; 21 [NSCursor pop]; 22 } 23 24 } // namespace. 25 26 @implementation CocoaTestHelperWindow 27 28 - (id)initWithContentRect:(NSRect)contentRect { 29 return [self initWithContentRect:contentRect 30 styleMask:NSBorderlessWindowMask 31 backing:NSBackingStoreBuffered 32 defer:NO]; 33 } 34 35 - (id)init { 36 return [self initWithContentRect:NSMakeRect(0, 0, 800, 600)]; 37 } 38 39 - (void)dealloc { 40 // Just a good place to put breakpoints when having problems with 41 // unittests and CocoaTestHelperWindow. 42 [super dealloc]; 43 } 44 45 - (void)makePretendKeyWindowAndSetFirstResponder:(NSResponder*)responder { 46 EXPECT_TRUE([self makeFirstResponder:responder]); 47 [self setPretendIsKeyWindow:YES]; 48 } 49 50 - (void)clearPretendKeyWindowAndFirstResponder { 51 [self setPretendIsKeyWindow:NO]; 52 EXPECT_TRUE([self makeFirstResponder:NSApp]); 53 } 54 55 - (void)setPretendIsKeyWindow:(BOOL)flag { 56 pretendIsKeyWindow_ = flag; 57 } 58 59 - (BOOL)isKeyWindow { 60 return pretendIsKeyWindow_; 61 } 62 63 @end 64 65 namespace ui { 66 67 CocoaTest::CocoaTest() : called_tear_down_(false), test_window_(nil) { 68 ForceSystemLeaks(); 69 Init(); 70 } 71 72 CocoaTest::~CocoaTest() { 73 // Must call CocoaTest's teardown from your overrides. 74 DCHECK(called_tear_down_); 75 } 76 77 void CocoaTest::Init() { 78 // Set the duration of AppKit-evaluated animations (such as frame changes) 79 // to zero for testing purposes. That way they take effect immediately. 80 [[NSAnimationContext currentContext] setDuration:0.0]; 81 82 // The above does not affect window-resize time, such as for an 83 // attached sheet dropping in. Set that duration for the current 84 // process (this is not persisted). Empirically, the value of 0.0 85 // is ignored. 86 NSDictionary* dict = 87 [NSDictionary dictionaryWithObject:@"0.01" forKey:@"NSWindowResizeTime"]; 88 [[NSUserDefaults standardUserDefaults] registerDefaults:dict]; 89 90 // Collect the list of windows that were open when the test started so 91 // that we don't wait for them to close in TearDown. Has to be done 92 // after BootstrapCocoa is called. 93 initial_windows_ = ApplicationWindows(); 94 } 95 96 void CocoaTest::TearDown() { 97 called_tear_down_ = true; 98 // Call close on our test_window to clean it up if one was opened. 99 [test_window_ clearPretendKeyWindowAndFirstResponder]; 100 [test_window_ close]; 101 test_window_ = nil; 102 103 // Recycle the pool to clean up any stuff that was put on the 104 // autorelease pool due to window or windowcontroller closures. 105 pool_.Recycle(); 106 107 // Some controls (NSTextFields, NSComboboxes etc) use 108 // performSelector:withDelay: to clean up drag handlers and other 109 // things (Radar 5851458 "Closing a window with a NSTextView in it 110 // should get rid of it immediately"). The event loop must be spun 111 // to get everything cleaned up correctly. It normally only takes 112 // one to two spins through the event loop to see a change. 113 114 // NOTE(shess): Under valgrind, -nextEventMatchingMask:* in one test 115 // needed to run twice, once taking .2 seconds, the next time .6 116 // seconds. The loop exit condition attempts to be scalable. 117 118 // Get the set of windows which weren't present when the test 119 // started. 120 std::set<NSWindow*> windows_left(WindowsLeft()); 121 122 while (!windows_left.empty()) { 123 // Cover delayed actions by spinning the loop at least once after 124 // this timeout. 125 const NSTimeInterval kCloseTimeoutSeconds = 126 TestTimeouts::action_timeout().InSecondsF(); 127 128 // Cover chains of delayed actions by spinning the loop at least 129 // this many times. 130 const int kCloseSpins = 3; 131 132 // Track the set of remaining windows so that everything can be 133 // reset if progress is made. 134 std::set<NSWindow*> still_left = windows_left; 135 136 NSDate* start_date = [NSDate date]; 137 bool one_more_time = true; 138 int spins = 0; 139 while (still_left.size() == windows_left.size() && 140 (spins < kCloseSpins || one_more_time)) { 141 // Check the timeout before pumping events, so that we'll spin 142 // the loop once after the timeout. 143 one_more_time = 144 ([start_date timeIntervalSinceNow] > -kCloseTimeoutSeconds); 145 146 // Autorelease anything thrown up by the event loop. 147 { 148 base::mac::ScopedNSAutoreleasePool pool; 149 ++spins; 150 NSEvent *next_event = [NSApp nextEventMatchingMask:NSAnyEventMask 151 untilDate:nil 152 inMode:NSDefaultRunLoopMode 153 dequeue:YES]; 154 [NSApp sendEvent:next_event]; 155 [NSApp updateWindows]; 156 } 157 158 // Refresh the outstanding windows. 159 still_left = WindowsLeft(); 160 } 161 162 // If no progress is being made, log a failure and continue. 163 if (still_left.size() == windows_left.size()) { 164 // NOTE(shess): Failing this expectation means that the test 165 // opened windows which have not been fully released. Either 166 // there is a leak, or perhaps one of |kCloseTimeoutSeconds| or 167 // |kCloseSpins| needs adjustment. 168 EXPECT_EQ(0U, windows_left.size()); 169 for (std::set<NSWindow*>::iterator iter = windows_left.begin(); 170 iter != windows_left.end(); ++iter) { 171 const char* desc = [[*iter description] UTF8String]; 172 LOG(WARNING) << "Didn't close window " << desc; 173 } 174 break; 175 } 176 177 windows_left = still_left; 178 } 179 PlatformTest::TearDown(); 180 } 181 182 std::set<NSWindow*> CocoaTest::ApplicationWindows() { 183 // This must NOT retain the windows it is returning. 184 std::set<NSWindow*> windows; 185 186 // Must create a pool here because [NSApp windows] has created an array 187 // with retains on all the windows in it. 188 base::mac::ScopedNSAutoreleasePool pool; 189 NSArray *appWindows = [NSApp windows]; 190 for (NSWindow *window in appWindows) { 191 windows.insert(window); 192 } 193 return windows; 194 } 195 196 std::set<NSWindow*> CocoaTest::WindowsLeft() { 197 const std::set<NSWindow*> windows(ApplicationWindows()); 198 std::set<NSWindow*> windows_left = 199 base::STLSetDifference<std::set<NSWindow*> >(windows, initial_windows_); 200 return windows_left; 201 } 202 203 CocoaTestHelperWindow* CocoaTest::test_window() { 204 if (!test_window_) { 205 test_window_ = [[CocoaTestHelperWindow alloc] init]; 206 if (base::debug::BeingDebugged()) { 207 [test_window_ orderFront:nil]; 208 } else { 209 [test_window_ orderBack:nil]; 210 } 211 } 212 return test_window_; 213 } 214 215 } // namespace ui 216