Home | History | Annotate | Download | only in test
      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