Adds support for running GTests on iOS.

iOS needs to run tests from within the context of a UIApplication, and
it needs to periodically spin the runloop to ensure that the watchdog
does not kill the app for being unresponsive.

BUG=crashpad:31

Change-Id: Ia1d881e478d4f83c236b475a21529760c06100c7
Reviewed-on: https://chromium-review.googlesource.com/c/crashpad/crashpad/+/1904226
Commit-Queue: Rohit Rao <rohitrao@chromium.org>
Reviewed-by: Mark Mentovai <mark@chromium.org>
This commit is contained in:
Rohit Rao 2020-01-07 11:14:27 -05:00 committed by Commit Bot
parent 558b7ea43f
commit 54bbd7d0d5
7 changed files with 218 additions and 3 deletions

BIN
build/ios/Default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>${IOS_BUNDLE_ID_PREFIX}.gtest.${GTEST_BUNDLE_ID_SUFFIX:rfc1034identifier}</string>
<key>UIApplicationDelegate</key>
<string>CrashpadUnitTestDelegate</string>
</dict>
</plist>

View File

@ -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, "*")
}
}
}
}

View File

@ -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" ]
}
}

View File

@ -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
}

33
test/gtest_runner_ios.h Normal file
View File

@ -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_

114
test/gtest_runner_ios.mm Normal file
View File

@ -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 <UIKit/UIKit.h>
#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