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