1// Copyright 2012 The Chromium Authors 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 "base/test/test_support_ios.h" 6 7#import <UIKit/UIKit.h> 8 9#include "base/check.h" 10#include "base/command_line.h" 11#include "base/debug/debugger.h" 12#include "base/mac/scoped_nsobject.h" 13#include "base/message_loop/message_pump.h" 14#include "base/message_loop/message_pump_mac.h" 15#import "base/test/ios/google_test_runner_delegate.h" 16#include "base/test/test_suite.h" 17#include "base/test/test_switches.h" 18#include "build/blink_buildflags.h" 19#include "testing/coverage_util_ios.h" 20 21// Springboard will kill any iOS app that fails to check in after launch within 22// a given time. Starting a UIApplication before invoking TestSuite::Run 23// prevents this from happening. 24 25// InitIOSRunHook saves the TestSuite and argc/argv, then invoking 26// RunTestsFromIOSApp calls UIApplicationMain(), providing an application 27// delegate class: ChromeUnitTestDelegate. The delegate implements 28// application:didFinishLaunchingWithOptions: to invoke the TestSuite's Run 29// method. 30 31// Since the executable isn't likely to be a real iOS UI, the delegate puts up a 32// window displaying the app name. If a bunch of apps using MainHook are being 33// run in a row, this provides an indication of which one is currently running. 34 35static base::RunTestSuiteCallback g_test_suite_callback; 36static int g_argc; 37static char** g_argv; 38 39namespace { 40void PopulateUIWindow(UIWindow* window) { 41 [window setBackgroundColor:[UIColor whiteColor]]; 42 [window makeKeyAndVisible]; 43 CGRect bounds = [[UIScreen mainScreen] bounds]; 44 // Add a label with the app name. 45 UILabel* label = [[[UILabel alloc] initWithFrame:bounds] autorelease]; 46 label.text = [[NSProcessInfo processInfo] processName]; 47 label.textAlignment = NSTextAlignmentCenter; 48 [window addSubview:label]; 49 50 // An NSInternalInconsistencyException is thrown if the app doesn't have a 51 // root view controller. Set an empty one here. 52 [window setRootViewController:[[[UIViewController alloc] init] autorelease]]; 53} 54 55bool IsSceneStartupEnabled() { 56 return [[NSBundle mainBundle].infoDictionary 57 objectForKey:@"UIApplicationSceneManifest"]; 58} 59} 60 61@interface UIApplication (Testing) 62- (void)_terminateWithStatus:(int)status; 63@end 64 65#if TARGET_IPHONE_SIMULATOR 66// Xcode 6 introduced behavior in the iOS Simulator where the software 67// keyboard does not appear if a hardware keyboard is connected. The following 68// declaration allows this behavior to be overriden when the app starts up. 69@interface UIKeyboardImpl 70+ (instancetype)sharedInstance; 71- (void)setAutomaticMinimizationEnabled:(BOOL)enabled; 72- (void)setSoftwareKeyboardShownByTouch:(BOOL)enabled; 73@end 74#endif // TARGET_IPHONE_SIMULATOR 75 76// No-op scene delegate for unit tests. Note that this is created along with 77// the application delegate, so they need to be separate objects (the same 78// object can't be both the app and scene delegate, since new scene delegates 79// are created for each scene). 80@interface ChromeUnitTestSceneDelegate : NSObject <UIWindowSceneDelegate> { 81 base::scoped_nsobject<UIWindow> _window; 82} 83 84@end 85 86@interface ChromeUnitTestDelegate : NSObject <GoogleTestRunnerDelegate> { 87 base::scoped_nsobject<UIWindow> _window; 88} 89- (void)runTests; 90@end 91 92@implementation ChromeUnitTestSceneDelegate 93 94- (void)scene:(UIScene*)scene 95 willConnectToSession:(UISceneSession*)session 96 options:(UISceneConnectionOptions*)connectionOptions 97 API_AVAILABLE(ios(13)) { 98 // Yes, this is leaked, it's just to make what's running visible. 99 _window.reset([[UIWindow alloc] 100 initWithWindowScene:static_cast<UIWindowScene*>(scene)]); 101 PopulateUIWindow(_window); 102} 103 104- (void)sceneDidDisconnect:(UIScene*)scene API_AVAILABLE(ios(13)) { 105 _window.reset(); 106} 107 108@end 109 110@implementation ChromeUnitTestDelegate 111 112- (BOOL)application:(UIApplication *)application 113 didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 114 115#if TARGET_IPHONE_SIMULATOR 116 // Xcode 6 introduced behavior in the iOS Simulator where the software 117 // keyboard does not appear if a hardware keyboard is connected. The following 118 // calls override this behavior by ensuring that the software keyboard is 119 // always shown. 120 [[UIKeyboardImpl sharedInstance] setAutomaticMinimizationEnabled:NO]; 121 if (@available(iOS 15, *)) { 122 } else { 123 [[UIKeyboardImpl sharedInstance] setSoftwareKeyboardShownByTouch:YES]; 124 } 125#endif // TARGET_IPHONE_SIMULATOR 126 127 if (!IsSceneStartupEnabled()) { 128 CGRect bounds = [[UIScreen mainScreen] bounds]; 129 130 // Yes, this is leaked, it's just to make what's running visible. 131 _window.reset([[UIWindow alloc] initWithFrame:bounds]); 132 PopulateUIWindow(_window); 133 } 134 135 if ([self shouldRedirectOutputToFile]) 136 [self redirectOutput]; 137 138 // Queue up the test run. 139 if (!base::ShouldRunIOSUnittestsWithXCTest()) { 140 // When running in XCTest mode, XCTest will invoke |runGoogleTest| directly. 141 // Otherwise, schedule a call to |runTests|. 142 [self performSelector:@selector(runTests) withObject:nil afterDelay:0.1]; 143 } 144 145 return YES; 146} 147 148// Returns true if the gtest output should be redirected to a file, then sent 149// to NSLog when complete. This redirection is used because gtest only writes 150// output to stdout, but results must be written to NSLog in order to show up in 151// the device log that is retrieved from the device by the host. 152- (BOOL)shouldRedirectOutputToFile { 153#if !TARGET_IPHONE_SIMULATOR 154 // Tests in XCTest mode don't need to redirect output to a file because the 155 // test result parser analyzes console output. 156 return !base::ShouldRunIOSUnittestsWithXCTest() && 157 !base::debug::BeingDebugged(); 158#else 159 return NO; 160#endif // TARGET_IPHONE_SIMULATOR 161} 162 163// Returns the path to the directory to store gtest output files. 164- (NSString*)outputPath { 165 NSArray* searchPath = 166 NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, 167 NSUserDomainMask, 168 YES); 169 CHECK([searchPath count] > 0) << "Failed to get the Documents folder"; 170 return [searchPath objectAtIndex:0]; 171} 172 173// Returns the path to file that stdout is redirected to. 174- (NSString*)stdoutPath { 175 return [[self outputPath] stringByAppendingPathComponent:@"stdout.log"]; 176} 177 178// Returns the path to file that stderr is redirected to. 179- (NSString*)stderrPath { 180 return [[self outputPath] stringByAppendingPathComponent:@"stderr.log"]; 181} 182 183// Redirects stdout and stderr to files in the Documents folder in the app's 184// sandbox. 185- (void)redirectOutput { 186 freopen([[self stdoutPath] UTF8String], "w+", stdout); 187 freopen([[self stderrPath] UTF8String], "w+", stderr); 188} 189 190// Reads the redirected gtest output from a file and writes it to NSLog. 191- (void)writeOutputToNSLog { 192 // Close the redirected stdout and stderr files so that the content written to 193 // NSLog doesn't end up in these files. 194 fclose(stdout); 195 fclose(stderr); 196 for (NSString* path in @[ [self stdoutPath], [self stderrPath]]) { 197 NSString* content = [NSString stringWithContentsOfFile:path 198 encoding:NSUTF8StringEncoding 199 error:NULL]; 200 NSArray* lines = [content componentsSeparatedByCharactersInSet: 201 [NSCharacterSet newlineCharacterSet]]; 202 203 NSLog(@"Writing contents of %@ to NSLog", path); 204 for (NSString* line in lines) { 205 NSLog(@"%@", line); 206 } 207 } 208} 209 210- (BOOL)supportsRunningGoogleTests { 211 return base::ShouldRunIOSUnittestsWithXCTest(); 212} 213 214- (int)runGoogleTests { 215 coverage_util::ConfigureCoverageReportPath(); 216 217 int exitStatus = std::move(g_test_suite_callback).Run(); 218 219 if ([self shouldRedirectOutputToFile]) 220 [self writeOutputToNSLog]; 221 222 return exitStatus; 223} 224 225- (void)runTests { 226 DCHECK(!base::ShouldRunIOSUnittestsWithXCTest()); 227 228 int exitStatus = [self runGoogleTests]; 229 230 // The blink code path uses a spawning test launcher and this wait isn't 231 // really necessary for that code path. 232#if !BUILDFLAG(USE_BLINK) 233 // If a test app is too fast, it will exit before Instruments has has a 234 // a chance to initialize and no test results will be seen. 235 [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]]; 236#endif 237 _window.reset(); 238 239 // Use the hidden selector to try and cleanly take down the app (otherwise 240 // things can think the app crashed even on a zero exit status). 241 UIApplication* application = [UIApplication sharedApplication]; 242 [application _terminateWithStatus:exitStatus]; 243 244 exit(exitStatus); 245} 246 247@end 248 249namespace { 250 251std::unique_ptr<base::MessagePump> CreateMessagePumpForUIForTests() { 252 // A basic MessagePump will do quite nicely in tests. 253 return std::unique_ptr<base::MessagePump>(new base::MessagePumpCFRunLoop()); 254} 255 256} // namespace 257 258namespace base { 259 260void InitIOSTestMessageLoop() { 261 MessagePump::OverrideMessagePumpForUIFactory(&CreateMessagePumpForUIForTests); 262} 263 264void InitIOSRunHook(RunTestSuiteCallback callback) { 265 g_test_suite_callback = std::move(callback); 266} 267 268void InitIOSArgs(int argc, char* argv[]) { 269 g_argc = argc; 270 g_argv = argv; 271} 272 273int RunTestsFromIOSApp() { 274 // When LaunchUnitTests is invoked it calls RunTestsFromIOSApp(). On its 275 // invocation, this method fires up an iOS app via UIApplicationMain. The 276 // TestSuite::Run will have be passed via InitIOSRunHook which will execute 277 // the TestSuite once the UIApplication is ready. 278 @autoreleasepool { 279 return UIApplicationMain(g_argc, g_argv, nil, @"ChromeUnitTestDelegate"); 280 } 281} 282 283bool ShouldRunIOSUnittestsWithXCTest() { 284 return base::CommandLine::ForCurrentProcess()->HasSwitch( 285 switches::kEnableRunIOSUnittestsWithXCTest); 286} 287 288} // namespace base 289