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