diff --git a/build/ios/Default.png b/build/ios/Default.png
new file mode 100644
index 00000000..8c9089d5
Binary files /dev/null and b/build/ios/Default.png differ
diff --git a/build/ios/Unittest-Info.plist b/build/ios/Unittest-Info.plist
new file mode 100644
index 00000000..9256fb44
--- /dev/null
+++ b/build/ios/Unittest-Info.plist
@@ -0,0 +1,10 @@
+
+
+
+
+ CFBundleIdentifier
+ ${IOS_BUNDLE_ID_PREFIX}.gtest.${GTEST_BUNDLE_ID_SUFFIX:rfc1034identifier}
+ UIApplicationDelegate
+ CrashpadUnitTestDelegate
+
+
diff --git a/build/test.gni b/build/test.gni
index f46520b7..24c343d7 100644
--- a/build/test.gni
+++ b/build/test.gni
@@ -18,9 +18,35 @@ if (crashpad_is_in_chromium) {
import("//testing/test.gni")
} else {
template("test") {
- executable(target_name) {
- testonly = true
- forward_variables_from(invoker, "*")
+ if (crashpad_is_ios) {
+ import("//third_party/mini_chromium/mini_chromium/build/ios/rules.gni")
+
+ _launch_image_bundle_target = target_name + "_launch_image"
+ bundle_data(_launch_image_bundle_target) {
+ forward_variables_from(invoker, [ "testonly" ])
+ sources = [
+ "//build/ios/Default.png",
+ ]
+ outputs = [
+ "{{bundle_contents_dir}}/{{source_file_part}}",
+ ]
+ }
+
+ ios_app_bundle(target_name) {
+ testonly = true
+ info_plist = "//build/ios/Unittest-Info.plist"
+ extra_substitutions = [ "GTEST_BUNDLE_ID_SUFFIX=$target_name" ]
+ forward_variables_from(invoker, "*")
+ if (!defined(deps)) {
+ deps = []
+ }
+ deps += [ ":$_launch_image_bundle_target" ]
+ }
+ } else {
+ executable(target_name) {
+ testonly = true
+ forward_variables_from(invoker, "*")
+ }
}
}
}
diff --git a/test/BUILD.gn b/test/BUILD.gn
index 5159744b..f9c81324 100644
--- a/test/BUILD.gn
+++ b/test/BUILD.gn
@@ -243,6 +243,21 @@ if (!crashpad_is_ios) {
}
}
+if (crashpad_is_ios) {
+ source_set("test_runner_ios") {
+ testonly = true
+ sources = [
+ "gtest_runner_ios.h",
+ "gtest_runner_ios.mm",
+ ]
+ configs += [ "..:crashpad_config" ]
+ deps = [
+ "../third_party/gtest:gtest",
+ ]
+ libs = [ "UIKit.framework" ]
+ }
+}
+
static_library("gmock_main") {
testonly = true
sources = [
@@ -257,6 +272,9 @@ static_library("gmock_main") {
"../third_party/mini_chromium:base",
"../third_party/mini_chromium:base_test_support",
]
+ if (crashpad_is_ios) {
+ deps += [ ":test_runner_ios" ]
+ }
}
static_library("gtest_main") {
@@ -272,4 +290,7 @@ static_library("gtest_main") {
"../third_party/mini_chromium:base",
"../third_party/mini_chromium:base_test_support",
]
+ if (crashpad_is_ios) {
+ deps += [ ":test_runner_ios" ]
+ }
}
diff --git a/test/gtest_main.cc b/test/gtest_main.cc
index 0a7a2d3a..f6b6a69e 100644
--- a/test/gtest_main.cc
+++ b/test/gtest_main.cc
@@ -21,6 +21,10 @@
#include "gmock/gmock.h"
#endif // CRASHPAD_TEST_LAUNCHER_GMOCK
+#if defined(OS_IOS)
+#include "test/gtest_runner_ios.h"
+#endif
+
#if defined(OS_WIN)
#include "test/win/win_child_process.h"
#endif // OS_WIN
@@ -93,5 +97,12 @@ int main(int argc, char* argv[]) {
#error #define CRASHPAD_TEST_LAUNCHER_GTEST or CRASHPAD_TEST_LAUNCHER_GMOCK
#endif // CRASHPAD_TEST_LAUNCHER_GMOCK
+#if defined(OS_IOS)
+ // iOS needs to run tests within the context of an app, so call a helper that
+ // invokes UIApplicationMain(). The application delegate will call
+ // RUN_ALL_TESTS() and exit before returning control to this function.
+ crashpad::test::IOSLaunchApplicationAndRunTests(argc, argv);
+#else
return RUN_ALL_TESTS();
+#endif
}
diff --git a/test/gtest_runner_ios.h b/test/gtest_runner_ios.h
new file mode 100644
index 00000000..564fe5c2
--- /dev/null
+++ b/test/gtest_runner_ios.h
@@ -0,0 +1,33 @@
+// Copyright 2019 The Crashpad Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef CRASHPAD_TEST_GTEST_RUNNER_IOS_
+#define CRASHPAD_TEST_GTEST_RUNNER_IOS_
+
+namespace crashpad {
+namespace test {
+
+//! \brief Runs all registered tests in the context of a UIKit application.
+//!
+//! Invokes UIApplicationMain() to launch the iOS application and runs all
+//! registered tests after the application finishes
+//! launching. UIApplicationMain() brings up the main runloop and never returns,
+//! so therefore this function never returns either. It invokes _exit() to
+//! terminate the application after tests have completed.
+void IOSLaunchApplicationAndRunTests(int argc, char* argv[]);
+
+} // namespace test
+} // namespace crashpad
+
+#endif // CRASHPAD_TEST_GTEST_RUNNER_IOS_
diff --git a/test/gtest_runner_ios.mm b/test/gtest_runner_ios.mm
new file mode 100644
index 00000000..8738fcb9
--- /dev/null
+++ b/test/gtest_runner_ios.mm
@@ -0,0 +1,114 @@
+// Copyright 2019 The Crashpad Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "test/gtest_runner_ios.h"
+
+#import
+
+#include "gtest/gtest.h"
+
+@interface UIApplication (Testing)
+- (void)_terminateWithStatus:(int)status;
+@end
+
+namespace {
+
+// The iOS watchdog timer will kill an app that doesn't spin the main event
+// loop often enough. This uses a Gtest TestEventListener to spin the current
+// loop after each test finishes. However, if any individual test takes too
+// long, it is still possible that the app will get killed.
+class IOSRunLoopListener : public testing::EmptyTestEventListener {
+ public:
+ virtual void OnTestEnd(const testing::TestInfo& test_info) {
+ @autoreleasepool {
+ // At the end of the test, spin the default loop for a moment.
+ NSDate* stop_date = [NSDate dateWithTimeIntervalSinceNow:0.001];
+ [[NSRunLoop currentRunLoop] runUntilDate:stop_date];
+ }
+ }
+};
+
+void RegisterTestEndListener() {
+ testing::TestEventListeners& listeners =
+ testing::UnitTest::GetInstance()->listeners();
+ listeners.Append(new IOSRunLoopListener);
+}
+
+} // namespace
+
+@interface CrashpadUnitTestDelegate : NSObject
+@property(nonatomic, readwrite, strong) UIWindow* window;
+- (void)runTests;
+@end
+
+@implementation CrashpadUnitTestDelegate
+
+- (BOOL)application:(UIApplication*)application
+ didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
+ self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
+ self.window.backgroundColor = UIColor.whiteColor;
+ [self.window makeKeyAndVisible];
+
+ UIViewController* controller = [[UIViewController alloc] init];
+ [self.window setRootViewController:controller];
+
+ // Add a label with the app name.
+ UILabel* label = [[UILabel alloc] initWithFrame:controller.view.bounds];
+ label.text = [[NSProcessInfo processInfo] processName];
+ label.textAlignment = NSTextAlignmentCenter;
+ label.textColor = UIColor.blackColor;
+ [controller.view addSubview:label];
+
+ // Queue up the test run.
+ [self performSelector:@selector(runTests) withObject:nil afterDelay:0.1];
+
+ return YES;
+}
+
+- (void)runTests {
+ RegisterTestEndListener();
+
+ int exitStatus = RUN_ALL_TESTS();
+
+ // If a test app is too fast, it will exit before Instruments has has a
+ // a chance to initialize and no test results will be seen.
+ // TODO(crbug.com/137010): Figure out how much time is actually needed, and
+ // sleep only to make sure that much time has elapsed since launch.
+ [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]];
+ self.window = nil;
+
+ // Use the hidden selector to try and cleanly take down the app (otherwise
+ // things can think the app crashed even on a zero exit status).
+ UIApplication* application = [UIApplication sharedApplication];
+ [application _terminateWithStatus:exitStatus];
+
+ exit(exitStatus);
+}
+
+@end
+
+
+namespace crashpad {
+namespace test {
+
+void IOSLaunchApplicationAndRunTests(int argc, char* argv[]) {
+ @autoreleasepool {
+ int exit_status =
+ UIApplicationMain(argc, argv, nil, @"CrashpadUnitTestDelegate");
+ exit(exit_status);
+ }
+}
+
+} // namespace crashpad
+} // namespace test