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/message_loop/message_pump.h" 13#include "base/message_loop/message_pump_apple.h" 14#import "base/test/ios/google_test_runner_delegate.h" 15#include "base/test/test_suite.h" 16#include "base/test/test_switches.h" 17#include "build/blink_buildflags.h" 18#include "build/ios_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.backgroundColor = 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]; 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.rootViewController = [[UIViewController alloc] init]; 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 overridden 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// Can be used to easily check if the current application is being used for 77// running tests. 78@interface ChromeUnitTestApplication : UIApplication 79- (BOOL)isRunningTests; 80@end 81 82@implementation ChromeUnitTestApplication 83- (BOOL)isRunningTests { 84 return YES; 85} 86@end 87 88// No-op scene delegate for unit tests. Note that this is created along with 89// the application delegate, so they need to be separate objects (the same 90// object can't be both the app and scene delegate, since new scene delegates 91// are created for each scene). 92@interface ChromeUnitTestSceneDelegate : NSObject <UIWindowSceneDelegate> { 93 UIWindow* __strong _window; 94} 95 96@end 97 98@interface ChromeUnitTestDelegate : NSObject <GoogleTestRunnerDelegate> { 99 UIWindow* __strong _window; 100} 101- (void)runTests; 102@end 103 104@implementation ChromeUnitTestSceneDelegate 105 106- (void)scene:(UIScene*)scene 107 willConnectToSession:(UISceneSession*)session 108 options:(UISceneConnectionOptions*)connectionOptions 109 API_AVAILABLE(ios(13)) { 110 _window = 111 [[UIWindow alloc] initWithWindowScene:static_cast<UIWindowScene*>(scene)]; 112 PopulateUIWindow(_window); 113} 114 115- (void)sceneDidDisconnect:(UIScene*)scene API_AVAILABLE(ios(13)) { 116 _window = nil; 117} 118 119@end 120 121@implementation ChromeUnitTestDelegate 122 123- (BOOL)application:(UIApplication*)application 124 didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { 125#if TARGET_IPHONE_SIMULATOR 126 // Xcode 6 introduced behavior in the iOS Simulator where the software 127 // keyboard does not appear if a hardware keyboard is connected. The following 128 // calls override this behavior by ensuring that the software keyboard is 129 // always shown. 130 [[UIKeyboardImpl sharedInstance] setAutomaticMinimizationEnabled:NO]; 131 if (@available(iOS 15, *)) { 132 } else { 133 [[UIKeyboardImpl sharedInstance] setSoftwareKeyboardShownByTouch:YES]; 134 } 135#endif // TARGET_IPHONE_SIMULATOR 136 137 if (!IsSceneStartupEnabled()) { 138 CGRect bounds = UIScreen.mainScreen.bounds; 139 140 _window = [[UIWindow alloc] initWithFrame:bounds]; 141 PopulateUIWindow(_window); 142 } 143 144 if ([self shouldRedirectOutputToFile]) 145 [self redirectOutput]; 146 147 // Queue up the test run. 148 if (!base::ShouldRunIOSUnittestsWithXCTest()) { 149 // When running in XCTest mode, XCTest will invoke |runGoogleTest| directly. 150 // Otherwise, schedule a call to |runTests|. 151 [self performSelector:@selector(runTests) withObject:nil afterDelay:0.1]; 152 } 153 154 return YES; 155} 156 157// Returns true if the gtest output should be redirected to a file, then sent 158// to NSLog when complete. This redirection is used because gtest only writes 159// output to stdout, but results must be written to NSLog in order to show up in 160// the device log that is retrieved from the device by the host. 161- (BOOL)shouldRedirectOutputToFile { 162#if !TARGET_IPHONE_SIMULATOR 163 // Tests in XCTest mode don't need to redirect output to a file because the 164 // test result parser analyzes console output. 165 return !base::ShouldRunIOSUnittestsWithXCTest() && 166 !base::debug::BeingDebugged(); 167#else 168 return NO; 169#endif // TARGET_IPHONE_SIMULATOR 170} 171 172// Returns the path to the directory to store gtest output files. 173- (NSString*)outputPath { 174 NSArray* searchPath = 175 NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, 176 NSUserDomainMask, 177 YES); 178 CHECK(searchPath.count > 0) << "Failed to get the Documents folder"; 179 return searchPath[0]; 180} 181 182// Returns the path to file that stdout is redirected to. 183- (NSString*)stdoutPath { 184 return [[self outputPath] stringByAppendingPathComponent:@"stdout.log"]; 185} 186 187// Returns the path to file that stderr is redirected to. 188- (NSString*)stderrPath { 189 return [[self outputPath] stringByAppendingPathComponent:@"stderr.log"]; 190} 191 192// Redirects stdout and stderr to files in the Documents folder in the app's 193// sandbox. 194- (void)redirectOutput { 195 freopen([[self stdoutPath] UTF8String], "w+", stdout); 196 freopen([[self stderrPath] UTF8String], "w+", stderr); 197} 198 199// Reads the redirected gtest output from a file and writes it to NSLog. 200- (void)writeOutputToNSLog { 201 // Close the redirected stdout and stderr files so that the content written to 202 // NSLog doesn't end up in these files. 203 fclose(stdout); 204 fclose(stderr); 205 for (NSString* path in @[ [self stdoutPath], [self stderrPath]]) { 206 NSString* content = [NSString stringWithContentsOfFile:path 207 encoding:NSUTF8StringEncoding 208 error:nil]; 209 NSArray* lines = 210 [content componentsSeparatedByCharactersInSet:NSCharacterSet 211 .newlineCharacterSet]; 212 213 NSLog(@"Writing contents of %@ to NSLog", path); 214 for (NSString* line in lines) { 215 NSLog(@"%@", line); 216 } 217 } 218} 219 220- (BOOL)supportsRunningGoogleTests { 221 return base::ShouldRunIOSUnittestsWithXCTest(); 222} 223 224- (int)runGoogleTests { 225 coverage_util::ConfigureCoverageReportPath(); 226 227 int exitStatus = std::move(g_test_suite_callback).Run(); 228 229 if ([self shouldRedirectOutputToFile]) 230 [self writeOutputToNSLog]; 231 232 return exitStatus; 233} 234 235- (void)runTests { 236 DCHECK(!base::ShouldRunIOSUnittestsWithXCTest()); 237 238 int exitStatus = [self runGoogleTests]; 239 240 // The Blink code path uses a spawning test launcher and this wait isn't 241 // really necessary for that code path. 242#if !BUILDFLAG(USE_BLINK) 243 // If a test app is too fast, it will exit before Instruments has has a 244 // a chance to initialize and no test results will be seen. 245 [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]]; 246#endif 247 _window = nil; 248 249#if !BUILDFLAG(IS_IOS_APP_EXTENSION) 250 // Use the hidden selector to try and cleanly take down the app (otherwise 251 // things can think the app crashed even on a zero exit status). 252 UIApplication* application = [UIApplication sharedApplication]; 253 [application _terminateWithStatus:exitStatus]; 254#endif 255 256 exit(exitStatus); 257} 258 259@end 260 261namespace { 262 263std::unique_ptr<base::MessagePump> CreateMessagePumpForUIForTests() { 264 // A basic MessagePump will do quite nicely in tests. 265 return std::unique_ptr<base::MessagePump>(new base::MessagePumpCFRunLoop()); 266} 267 268} // namespace 269 270namespace base { 271 272void InitIOSTestMessageLoop() { 273 MessagePump::OverrideMessagePumpForUIFactory(&CreateMessagePumpForUIForTests); 274} 275 276void InitIOSRunHook(RunTestSuiteCallback callback) { 277 g_test_suite_callback = std::move(callback); 278} 279 280void InitIOSArgs(int argc, char* argv[]) { 281 g_argc = argc; 282 g_argv = argv; 283} 284 285int RunTestsFromIOSApp() { 286 // When LaunchUnitTests is invoked it calls RunTestsFromIOSApp(). On its 287 // invocation, this method fires up an iOS app via UIApplicationMain. The 288 // TestSuite::Run will have be passed via InitIOSRunHook which will execute 289 // the TestSuite once the UIApplication is ready. 290 @autoreleasepool { 291 return UIApplicationMain(g_argc, g_argv, @"ChromeUnitTestApplication", 292 @"ChromeUnitTestDelegate"); 293 } 294} 295 296bool ShouldRunIOSUnittestsWithXCTest() { 297 return base::CommandLine::ForCurrentProcess()->HasSwitch( 298 switches::kEnableRunIOSUnittestsWithXCTest); 299} 300 301} // namespace base 302