1 // Copyright (c) 2012 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 <UIKit/UIKit.h> 6 7 #include "base/debug/debugger.h" 8 #include "base/logging.h" 9 #include "base/mac/scoped_nsautorelease_pool.h" 10 #include "base/mac/scoped_nsobject.h" 11 #include "base/message_loop/message_loop.h" 12 #include "base/message_loop/message_pump_default.h" 13 #include "base/test/test_suite.h" 14 15 // Springboard will kill any iOS app that fails to check in after launch within 16 // a given time. Starting a UIApplication before invoking TestSuite::Run 17 // prevents this from happening. 18 19 // InitIOSRunHook saves the TestSuite and argc/argv, then invoking 20 // RunTestsFromIOSApp calls UIApplicationMain(), providing an application 21 // delegate class: ChromeUnitTestDelegate. The delegate implements 22 // application:didFinishLaunchingWithOptions: to invoke the TestSuite's Run 23 // method. 24 25 // Since the executable isn't likely to be a real iOS UI, the delegate puts up a 26 // window displaying the app name. If a bunch of apps using MainHook are being 27 // run in a row, this provides an indication of which one is currently running. 28 29 static base::TestSuite* g_test_suite = NULL; 30 static int g_argc; 31 static char** g_argv; 32 33 @interface UIApplication (Testing) 34 - (void) _terminateWithStatus:(int)status; 35 @end 36 37 #if TARGET_IPHONE_SIMULATOR 38 // Xcode 6 introduced behavior in the iOS Simulator where the software 39 // keyboard does not appear if a hardware keyboard is connected. The following 40 // declaration allows this behavior to be overriden when the app starts up. 41 @interface UIKeyboardImpl 42 + (instancetype)sharedInstance; 43 - (void)setAutomaticMinimizationEnabled:(BOOL)enabled; 44 - (void)setSoftwareKeyboardShownByTouch:(BOOL)enabled; 45 @end 46 #endif // TARGET_IPHONE_SIMULATOR 47 48 @interface ChromeUnitTestDelegate : NSObject { 49 @private 50 base::scoped_nsobject<UIWindow> window_; 51 } 52 - (void)runTests; 53 @end 54 55 @implementation ChromeUnitTestDelegate 56 57 - (BOOL)application:(UIApplication *)application 58 didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 59 60 #if TARGET_IPHONE_SIMULATOR 61 // Xcode 6 introduced behavior in the iOS Simulator where the software 62 // keyboard does not appear if a hardware keyboard is connected. The following 63 // calls override this behavior by ensuring that the software keyboard is 64 // always shown. 65 [[UIKeyboardImpl sharedInstance] setAutomaticMinimizationEnabled:NO]; 66 [[UIKeyboardImpl sharedInstance] setSoftwareKeyboardShownByTouch:YES]; 67 #endif // TARGET_IPHONE_SIMULATOR 68 69 CGRect bounds = [[UIScreen mainScreen] bounds]; 70 71 // Yes, this is leaked, it's just to make what's running visible. 72 window_.reset([[UIWindow alloc] initWithFrame:bounds]); 73 [window_ makeKeyAndVisible]; 74 75 // Add a label with the app name. 76 UILabel* label = [[[UILabel alloc] initWithFrame:bounds] autorelease]; 77 label.text = [[NSProcessInfo processInfo] processName]; 78 label.textAlignment = NSTextAlignmentCenter; 79 [window_ addSubview:label]; 80 81 if ([self shouldRedirectOutputToFile]) 82 [self redirectOutput]; 83 84 // Queue up the test run. 85 [self performSelector:@selector(runTests) 86 withObject:nil 87 afterDelay:0.1]; 88 return YES; 89 } 90 91 // Returns true if the gtest output should be redirected to a file, then sent 92 // to NSLog when compleete. This redirection is used because gtest only writes 93 // output to stdout, but results must be written to NSLog in order to show up in 94 // the device log that is retrieved from the device by the host. 95 - (BOOL)shouldRedirectOutputToFile { 96 #if !TARGET_IPHONE_SIMULATOR 97 return !base::debug::BeingDebugged(); 98 #endif // TARGET_IPHONE_SIMULATOR 99 return NO; 100 } 101 102 // Returns the path to the directory to store gtest output files. 103 - (NSString*)outputPath { 104 NSArray* searchPath = 105 NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, 106 NSUserDomainMask, 107 YES); 108 CHECK([searchPath count] > 0) << "Failed to get the Documents folder"; 109 return [searchPath objectAtIndex:0]; 110 } 111 112 // Returns the path to file that stdout is redirected to. 113 - (NSString*)stdoutPath { 114 return [[self outputPath] stringByAppendingPathComponent:@"stdout.log"]; 115 } 116 117 // Returns the path to file that stderr is redirected to. 118 - (NSString*)stderrPath { 119 return [[self outputPath] stringByAppendingPathComponent:@"stderr.log"]; 120 } 121 122 // Redirects stdout and stderr to files in the Documents folder in the app's 123 // sandbox. 124 - (void)redirectOutput { 125 freopen([[self stdoutPath] UTF8String], "w+", stdout); 126 freopen([[self stderrPath] UTF8String], "w+", stderr); 127 } 128 129 // Reads the redirected gtest output from a file and writes it to NSLog. 130 - (void)writeOutputToNSLog { 131 // Close the redirected stdout and stderr files so that the content written to 132 // NSLog doesn't end up in these files. 133 fclose(stdout); 134 fclose(stderr); 135 for (NSString* path in @[ [self stdoutPath], [self stderrPath]]) { 136 NSString* content = [NSString stringWithContentsOfFile:path 137 encoding:NSUTF8StringEncoding 138 error:NULL]; 139 NSArray* lines = [content componentsSeparatedByCharactersInSet: 140 [NSCharacterSet newlineCharacterSet]]; 141 142 NSLog(@"Writing contents of %@ to NSLog", path); 143 for (NSString* line in lines) { 144 NSLog(@"%@", line); 145 } 146 } 147 } 148 149 - (void)runTests { 150 int exitStatus = g_test_suite->Run(); 151 152 if ([self shouldRedirectOutputToFile]) 153 [self writeOutputToNSLog]; 154 155 // If a test app is too fast, it will exit before Instruments has has a 156 // a chance to initialize and no test results will be seen. 157 // TODO(ios): crbug.com/137010 Figure out how much time is actually needed, 158 // and sleep only to make sure that much time has elapsed since launch. 159 [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]]; 160 window_.reset(); 161 162 // Use the hidden selector to try and cleanly take down the app (otherwise 163 // things can think the app crashed even on a zero exit status). 164 UIApplication* application = [UIApplication sharedApplication]; 165 [application _terminateWithStatus:exitStatus]; 166 167 exit(exitStatus); 168 } 169 170 @end 171 172 namespace { 173 174 scoped_ptr<base::MessagePump> CreateMessagePumpForUIForTests() { 175 // A default MessagePump will do quite nicely in tests. 176 return scoped_ptr<base::MessagePump>(new base::MessagePumpDefault()); 177 } 178 179 } // namespace 180 181 namespace base { 182 183 void InitIOSTestMessageLoop() { 184 MessageLoop::InitMessagePumpForUIFactory(&CreateMessagePumpForUIForTests); 185 } 186 187 void InitIOSRunHook(TestSuite* suite, int argc, char* argv[]) { 188 g_test_suite = suite; 189 g_argc = argc; 190 g_argv = argv; 191 } 192 193 void RunTestsFromIOSApp() { 194 // When TestSuite::Run is invoked it calls RunTestsFromIOSApp(). On the first 195 // invocation, this method fires up an iOS app via UIApplicationMain. Since 196 // UIApplicationMain does not return until the app exits, control does not 197 // return to the initial TestSuite::Run invocation, so the app invokes 198 // TestSuite::Run a second time and since |ran_hook| is true at this point, 199 // this method is a no-op and control returns to TestSuite:Run so that test 200 // are executed. Once the app exits, RunTestsFromIOSApp calls exit() so that 201 // control is not returned to the initial invocation of TestSuite::Run. 202 static bool ran_hook = false; 203 if (!ran_hook) { 204 ran_hook = true; 205 mac::ScopedNSAutoreleasePool pool; 206 int exit_status = UIApplicationMain(g_argc, g_argv, nil, 207 @"ChromeUnitTestDelegate"); 208 exit(exit_status); 209 } 210 } 211 212 } // namespace base 213