• 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/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