| // Copyright (c) 2012 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 <UIKit/UIKit.h> |
| |
| #include "base/debug/debugger.h" |
| #include "base/logging.h" |
| #include "base/mac/scoped_nsautorelease_pool.h" |
| #include "base/mac/scoped_nsobject.h" |
| #include "base/message_loop/message_loop.h" |
| #include "base/message_loop/message_pump_default.h" |
| #include "base/test/test_suite.h" |
| #include "testing/coverage_util_ios.h" |
| |
| // Springboard will kill any iOS app that fails to check in after launch within |
| // a given time. Starting a UIApplication before invoking TestSuite::Run |
| // prevents this from happening. |
| |
| // InitIOSRunHook saves the TestSuite and argc/argv, then invoking |
| // RunTestsFromIOSApp calls UIApplicationMain(), providing an application |
| // delegate class: ChromeUnitTestDelegate. The delegate implements |
| // application:didFinishLaunchingWithOptions: to invoke the TestSuite's Run |
| // method. |
| |
| // Since the executable isn't likely to be a real iOS UI, the delegate puts up a |
| // window displaying the app name. If a bunch of apps using MainHook are being |
| // run in a row, this provides an indication of which one is currently running. |
| |
| static base::TestSuite* g_test_suite = NULL; |
| static int g_argc; |
| static char** g_argv; |
| |
| @interface UIApplication (Testing) |
| - (void)_terminateWithStatus:(int)status; |
| @end |
| |
| #if TARGET_IPHONE_SIMULATOR |
| // Xcode 6 introduced behavior in the iOS Simulator where the software |
| // keyboard does not appear if a hardware keyboard is connected. The following |
| // declaration allows this behavior to be overriden when the app starts up. |
| @interface UIKeyboardImpl |
| + (instancetype)sharedInstance; |
| - (void)setAutomaticMinimizationEnabled:(BOOL)enabled; |
| - (void)setSoftwareKeyboardShownByTouch:(BOOL)enabled; |
| @end |
| #endif // TARGET_IPHONE_SIMULATOR |
| |
| @interface ChromeUnitTestDelegate : NSObject { |
| base::scoped_nsobject<UIWindow> _window; |
| } |
| - (void)runTests; |
| @end |
| |
| @implementation ChromeUnitTestDelegate |
| |
| - (BOOL)application:(UIApplication *)application |
| didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { |
| |
| #if TARGET_IPHONE_SIMULATOR |
| // Xcode 6 introduced behavior in the iOS Simulator where the software |
| // keyboard does not appear if a hardware keyboard is connected. The following |
| // calls override this behavior by ensuring that the software keyboard is |
| // always shown. |
| [[UIKeyboardImpl sharedInstance] setAutomaticMinimizationEnabled:NO]; |
| [[UIKeyboardImpl sharedInstance] setSoftwareKeyboardShownByTouch:YES]; |
| #endif // TARGET_IPHONE_SIMULATOR |
| |
| CGRect bounds = [[UIScreen mainScreen] bounds]; |
| |
| // Yes, this is leaked, it's just to make what's running visible. |
| _window.reset([[UIWindow alloc] initWithFrame:bounds]); |
| [_window setBackgroundColor:[UIColor whiteColor]]; |
| [_window makeKeyAndVisible]; |
| |
| // Add a label with the app name. |
| UILabel* label = [[[UILabel alloc] initWithFrame:bounds] autorelease]; |
| label.text = [[NSProcessInfo processInfo] processName]; |
| label.textAlignment = NSTextAlignmentCenter; |
| [_window addSubview:label]; |
| |
| // An NSInternalInconsistencyException is thrown if the app doesn't have a |
| // root view controller. Set an empty one here. |
| [_window setRootViewController:[[[UIViewController alloc] init] autorelease]]; |
| |
| if ([self shouldRedirectOutputToFile]) |
| [self redirectOutput]; |
| |
| // Queue up the test run. |
| [self performSelector:@selector(runTests) |
| withObject:nil |
| afterDelay:0.1]; |
| return YES; |
| } |
| |
| // Returns true if the gtest output should be redirected to a file, then sent |
| // to NSLog when compleete. This redirection is used because gtest only writes |
| // output to stdout, but results must be written to NSLog in order to show up in |
| // the device log that is retrieved from the device by the host. |
| - (BOOL)shouldRedirectOutputToFile { |
| #if !TARGET_IPHONE_SIMULATOR |
| return !base::debug::BeingDebugged(); |
| #endif // TARGET_IPHONE_SIMULATOR |
| return NO; |
| } |
| |
| // Returns the path to the directory to store gtest output files. |
| - (NSString*)outputPath { |
| NSArray* searchPath = |
| NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, |
| NSUserDomainMask, |
| YES); |
| CHECK([searchPath count] > 0) << "Failed to get the Documents folder"; |
| return [searchPath objectAtIndex:0]; |
| } |
| |
| // Returns the path to file that stdout is redirected to. |
| - (NSString*)stdoutPath { |
| return [[self outputPath] stringByAppendingPathComponent:@"stdout.log"]; |
| } |
| |
| // Returns the path to file that stderr is redirected to. |
| - (NSString*)stderrPath { |
| return [[self outputPath] stringByAppendingPathComponent:@"stderr.log"]; |
| } |
| |
| // Redirects stdout and stderr to files in the Documents folder in the app's |
| // sandbox. |
| - (void)redirectOutput { |
| freopen([[self stdoutPath] UTF8String], "w+", stdout); |
| freopen([[self stderrPath] UTF8String], "w+", stderr); |
| } |
| |
| // Reads the redirected gtest output from a file and writes it to NSLog. |
| - (void)writeOutputToNSLog { |
| // Close the redirected stdout and stderr files so that the content written to |
| // NSLog doesn't end up in these files. |
| fclose(stdout); |
| fclose(stderr); |
| for (NSString* path in @[ [self stdoutPath], [self stderrPath]]) { |
| NSString* content = [NSString stringWithContentsOfFile:path |
| encoding:NSUTF8StringEncoding |
| error:NULL]; |
| NSArray* lines = [content componentsSeparatedByCharactersInSet: |
| [NSCharacterSet newlineCharacterSet]]; |
| |
| NSLog(@"Writing contents of %@ to NSLog", path); |
| for (NSString* line in lines) { |
| NSLog(@"%@", line); |
| } |
| } |
| } |
| |
| - (void)runTests { |
| coverage_util::ConfigureCoverageReportPath(); |
| |
| int exitStatus = g_test_suite->Run(); |
| |
| if ([self shouldRedirectOutputToFile]) |
| [self writeOutputToNSLog]; |
| |
| // If a test app is too fast, it will exit before Instruments has has a |
| // a chance to initialize and no test results will be seen. |
| // TODO(crbug.com/137010): Figure out how much time is actually needed, and |
| // sleep only to make sure that much time has elapsed since launch. |
| [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]]; |
| _window.reset(); |
| |
| // Use the hidden selector to try and cleanly take down the app (otherwise |
| // things can think the app crashed even on a zero exit status). |
| UIApplication* application = [UIApplication sharedApplication]; |
| [application _terminateWithStatus:exitStatus]; |
| |
| exit(exitStatus); |
| } |
| |
| @end |
| |
| namespace { |
| |
| std::unique_ptr<base::MessagePump> CreateMessagePumpForUIForTests() { |
| // A default MessagePump will do quite nicely in tests. |
| return std::unique_ptr<base::MessagePump>(new base::MessagePumpDefault()); |
| } |
| |
| } // namespace |
| |
| namespace base { |
| |
| void InitIOSTestMessageLoop() { |
| MessageLoop::InitMessagePumpForUIFactory(&CreateMessagePumpForUIForTests); |
| } |
| |
| void InitIOSRunHook(TestSuite* suite, int argc, char* argv[]) { |
| g_test_suite = suite; |
| g_argc = argc; |
| g_argv = argv; |
| } |
| |
| void RunTestsFromIOSApp() { |
| // When TestSuite::Run is invoked it calls RunTestsFromIOSApp(). On the first |
| // invocation, this method fires up an iOS app via UIApplicationMain. Since |
| // UIApplicationMain does not return until the app exits, control does not |
| // return to the initial TestSuite::Run invocation, so the app invokes |
| // TestSuite::Run a second time and since |ran_hook| is true at this point, |
| // this method is a no-op and control returns to TestSuite:Run so that test |
| // are executed. Once the app exits, RunTestsFromIOSApp calls exit() so that |
| // control is not returned to the initial invocation of TestSuite::Run. |
| static bool ran_hook = false; |
| if (!ran_hook) { |
| ran_hook = true; |
| mac::ScopedNSAutoreleasePool pool; |
| int exit_status = UIApplicationMain(g_argc, g_argv, nil, |
| @"ChromeUnitTestDelegate"); |
| exit(exit_status); |
| } |
| } |
| |
| } // namespace base |