xref: /aosp_15_r20/external/cronet/base/test/test_support_ios.mm (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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