Commit b6c231b5 authored by Rohit Rao's avatar Rohit Rao Committed by Commit Bot

[ios] Adds the ability to run unittests using XCTest.

This new functionality is hidden behind both a GN arg
(enable_run_ios_unittests_with_xctest) and a commandline switch
(--enable-run-unittests-with-xctest), in order to default it to off
until the bots are updated to properly run XCTest-based unittests.

The iOS test runner is updated to run in one of two modes.  When
--enable-run-unittests-with-xctest is false (the default), behavior is
unchanged; TestSuite::Run() calls UIApplicationMain(), then the
UIApplicationDelegate calls TestSuite::Run() again, which actually runs
the tests when invoked the second time.  When the switch is set to true,
the second invocation of TestSuite::Run() is made by our XCTestCase
subclass rather than by the application delegate.

Xcode provides the ability to run XCTests and XCUITests from the
commandline, but does not provide any other way to install and run an
app outside of this test-based workflow.  (We had an alternative that
used third party libraries, but they no longer work on iOS 13.)  This
makes it difficult to install and run GoogleTest-based tests on iOS
devices, since they run as a single self-contained application, but it
would not be practical to drop GoogleTest support on iOS.  Instead, we
are exploring invoking these tests via XCTest, which would allow us to
use Xcode's tooling but still run the same GoogleTest-based tests as on
other platforms.

BUG=635509

Change-Id: I26c67d9c7e16a744f43a20f2d8c5839ca8b3c31a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1787593Reviewed-by: default avatarDirk Pranke <dpranke@chromium.org>
Reviewed-by: default avatarMark Mentovai <mark@chromium.org>
Reviewed-by: default avatarJustin Cohen <justincohen@chromium.org>
Commit-Queue: Rohit Rao <rohitrao@chromium.org>
Cr-Commit-Position: refs/heads/master@{#694749}
parent 53e009b1
...@@ -228,6 +228,7 @@ static_library("test_support") { ...@@ -228,6 +228,7 @@ static_library("test_support") {
set_sources_assignment_filter([]) set_sources_assignment_filter([])
sources += [ "test_file_util_mac.cc" ] sources += [ "test_file_util_mac.cc" ]
set_sources_assignment_filter(sources_assignment_filter) set_sources_assignment_filter(sources_assignment_filter)
deps += [ ":google_test_runner_shared_headers" ]
} }
if (is_mac) { if (is_mac) {
...@@ -513,6 +514,29 @@ if (is_android) { ...@@ -513,6 +514,29 @@ if (is_android) {
} }
} }
if (is_ios) {
source_set("google_test_runner_shared_headers") {
sources = [
"ios/google_test_runner_delegate.h",
]
}
source_set("google_test_runner") {
sources = [
"ios/google_test_runner.mm",
]
deps = [
":google_test_runner_shared_headers",
"//base",
]
libs = [ "UIKit.framework" ]
configs += [
"//build/config/compiler:enable_arc",
"//build/config/ios:xctest_config",
]
}
}
# Trivial executable which outputs space-delimited argv to stdout, # Trivial executable which outputs space-delimited argv to stdout,
# used for testing. # used for testing.
executable("test_child_process") { executable("test_child_process") {
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#include "base/logging.h"
#import "base/test/ios/google_test_runner_delegate.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
@interface GoogleTestRunner : XCTestCase
@end
@implementation GoogleTestRunner
- (void)testRunGoogleTests {
id appDelegate = UIApplication.sharedApplication.delegate;
DCHECK([appDelegate conformsToProtocol:@protocol(GoogleTestRunnerDelegate)]);
id<GoogleTestRunnerDelegate> runnerDelegate =
static_cast<id<GoogleTestRunnerDelegate>>(appDelegate);
DCHECK(runnerDelegate.supportsRunningGoogleTests);
XCTAssertTrue([runnerDelegate runGoogleTests] == 0);
}
@end
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef BASE_TEST_IOS_GOOGLE_TEST_RUNNER_DELEGATE_H_
#define BASE_TEST_IOS_GOOGLE_TEST_RUNNER_DELEGATE_H_
@protocol GoogleTestRunnerDelegate
// Returns YES if this delegate supports running GoogleTests via a call to
// |runGoogleTests|.
@property(nonatomic, readonly, assign) BOOL supportsRunningGoogleTests;
// Runs GoogleTests and returns the final exit code.
- (int)runGoogleTests;
@end
#endif // BASE_TEST_IOS_GOOGLE_TEST_RUNNER_DELEGATE_H_
...@@ -19,6 +19,9 @@ void InitIOSRunHook(TestSuite* suite, int argc, char* argv[]); ...@@ -19,6 +19,9 @@ void InitIOSRunHook(TestSuite* suite, int argc, char* argv[]);
// InitIOSRunHook. // InitIOSRunHook.
void RunTestsFromIOSApp(); void RunTestsFromIOSApp();
// Returns true if unittests should be run by the XCTest runnner.
bool ShouldRunIOSUnittestsWithXCTest();
} // namespace base } // namespace base
#endif // BASE_TEST_TEST_SUPPORT_IOS_H_ #endif // BASE_TEST_TEST_SUPPORT_IOS_H_
...@@ -2,14 +2,19 @@ ...@@ -2,14 +2,19 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
#import "base/test/test_support_ios.h"
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#include "base/command_line.h"
#include "base/debug/debugger.h" #include "base/debug/debugger.h"
#include "base/logging.h" #include "base/logging.h"
#include "base/mac/scoped_nsobject.h" #include "base/mac/scoped_nsobject.h"
#include "base/message_loop/message_pump.h" #include "base/message_loop/message_pump.h"
#include "base/message_loop/message_pump_default.h" #include "base/message_loop/message_pump_default.h"
#import "base/test/ios/google_test_runner_delegate.h"
#include "base/test/test_suite.h" #include "base/test/test_suite.h"
#include "base/test/test_switches.h"
#include "testing/coverage_util_ios.h" #include "testing/coverage_util_ios.h"
// Springboard will kill any iOS app that fails to check in after launch within // Springboard will kill any iOS app that fails to check in after launch within
...@@ -45,7 +50,7 @@ static char** g_argv; ...@@ -45,7 +50,7 @@ static char** g_argv;
@end @end
#endif // TARGET_IPHONE_SIMULATOR #endif // TARGET_IPHONE_SIMULATOR
@interface ChromeUnitTestDelegate : NSObject { @interface ChromeUnitTestDelegate : NSObject <GoogleTestRunnerDelegate> {
base::scoped_nsobject<UIWindow> _window; base::scoped_nsobject<UIWindow> _window;
} }
- (void)runTests; - (void)runTests;
...@@ -86,9 +91,12 @@ static char** g_argv; ...@@ -86,9 +91,12 @@ static char** g_argv;
[self redirectOutput]; [self redirectOutput];
// Queue up the test run. // Queue up the test run.
[self performSelector:@selector(runTests) if (!base::ShouldRunIOSUnittestsWithXCTest()) {
withObject:nil // When running in XCTest mode, XCTest will invoke |runGoogleTest| directly.
afterDelay:0.1]; // Otherwise, schedule a call to |runTests|.
[self performSelector:@selector(runTests) withObject:nil afterDelay:0.1];
}
return YES; return YES;
} }
...@@ -150,7 +158,11 @@ static char** g_argv; ...@@ -150,7 +158,11 @@ static char** g_argv;
} }
} }
- (void)runTests { - (BOOL)supportsRunningGoogleTests {
return base::ShouldRunIOSUnittestsWithXCTest();
}
- (int)runGoogleTests {
coverage_util::ConfigureCoverageReportPath(); coverage_util::ConfigureCoverageReportPath();
int exitStatus = g_test_suite->Run(); int exitStatus = g_test_suite->Run();
...@@ -158,6 +170,14 @@ static char** g_argv; ...@@ -158,6 +170,14 @@ static char** g_argv;
if ([self shouldRedirectOutputToFile]) if ([self shouldRedirectOutputToFile])
[self writeOutputToNSLog]; [self writeOutputToNSLog];
return exitStatus;
}
- (void)runTests {
DCHECK(!base::ShouldRunIOSUnittestsWithXCTest());
int exitStatus = [self runGoogleTests];
// If a test app is too fast, it will exit before Instruments has has a // 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. // a chance to initialize and no test results will be seen.
// TODO(crbug.com/137010): Figure out how much time is actually needed, and // TODO(crbug.com/137010): Figure out how much time is actually needed, and
...@@ -216,4 +236,9 @@ void RunTestsFromIOSApp() { ...@@ -216,4 +236,9 @@ void RunTestsFromIOSApp() {
} }
} }
bool ShouldRunIOSUnittestsWithXCTest() {
return base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kEnableRunIOSUnittestsWithXCTest);
}
} // namespace base } // namespace base
...@@ -84,3 +84,9 @@ const char switches::kTestLauncherTrace[] = "test-launcher-trace"; ...@@ -84,3 +84,9 @@ const char switches::kTestLauncherTrace[] = "test-launcher-trace";
const char switches::kTestTinyTimeout[] = "test-tiny-timeout"; const char switches::kTestTinyTimeout[] = "test-tiny-timeout";
const char switches::kUiTestActionTimeout[] = "ui-test-action-timeout"; const char switches::kUiTestActionTimeout[] = "ui-test-action-timeout";
const char switches::kUiTestActionMaxTimeout[] = "ui-test-action-max-timeout"; const char switches::kUiTestActionMaxTimeout[] = "ui-test-action-max-timeout";
#if defined(OS_IOS)
// If enabled, runs unittests using the XCTest test runner.
const char switches::kEnableRunIOSUnittestsWithXCTest[] =
"enable-run-ios-unittests-with-xctest";
#endif
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
#ifndef BASE_TEST_TEST_SWITCHES_H_ #ifndef BASE_TEST_TEST_SWITCHES_H_
#define BASE_TEST_TEST_SWITCHES_H_ #define BASE_TEST_TEST_SWITCHES_H_
#include "build/build_config.h"
namespace switches { namespace switches {
// All switches in alphabetical order. The switches should be documented // All switches in alphabetical order. The switches should be documented
...@@ -32,6 +34,10 @@ extern const char kTestTinyTimeout[]; ...@@ -32,6 +34,10 @@ extern const char kTestTinyTimeout[];
extern const char kUiTestActionTimeout[]; extern const char kUiTestActionTimeout[];
extern const char kUiTestActionMaxTimeout[]; extern const char kUiTestActionMaxTimeout[];
#if defined(OS_IOS)
extern const char kEnableRunIOSUnittestsWithXCTest[];
#endif
} // namespace switches } // namespace switches
#endif // BASE_TEST_TEST_SWITCHES_H_ #endif // BASE_TEST_TEST_SWITCHES_H_
...@@ -1808,13 +1808,20 @@ template("ios_xctest_test") { ...@@ -1808,13 +1808,20 @@ template("ios_xctest_test") {
_host_target = _target_name _host_target = _target_name
_host_output = _output_name _host_output = _output_name
_xctest_shell_source_target = _xctest_target + "shell_source" # Allow invokers to specify their own target for the xctest module, but
source_set(_xctest_shell_source_target) { # fall back to a default (empty) module otherwise.
sources = [ if (defined(invoker.xctest_module_target)) {
"//build/config/ios/xctest_shell.mm", _xctest_module_target = invoker.xctest_module_target
] } else {
_xctest_module_target_name = _xctest_target + "shell_source"
_xctest_module_target = ":$_xctest_module_target_name"
source_set(_xctest_module_target_name) {
sources = [
"//build/config/ios/xctest_shell.mm",
]
configs += [ "//build/config/ios:xctest_config" ] configs += [ "//build/config/ios:xctest_config" ]
}
} }
ios_xctest_bundle(_xctest_target) { ios_xctest_bundle(_xctest_target) {
...@@ -1824,7 +1831,7 @@ template("ios_xctest_test") { ...@@ -1824,7 +1831,7 @@ template("ios_xctest_test") {
xcode_test_application_name = _host_output xcode_test_application_name = _host_output
deps = [ deps = [
":$_xctest_shell_source_target", _xctest_module_target,
] ]
} }
......
...@@ -293,6 +293,14 @@ template("test") { ...@@ -293,6 +293,14 @@ template("test") {
import("//build/config/ios/ios_sdk.gni") import("//build/config/ios/ios_sdk.gni")
import("//build/config/ios/rules.gni") import("//build/config/ios/rules.gni")
declare_args() {
# Keep the unittest-as-xctest functionality defaulted to off until the
# bots are updated to handle it properly.
# TODO(crbug.com/1001667): Remove this arg once the iOS test runner
# supports running unittests with xctest.
enable_run_ios_unittests_with_xctest = false
}
_test_target = target_name _test_target = target_name
_resources_bundle_data = target_name + "_resources_bundle_data" _resources_bundle_data = target_name + "_resources_bundle_data"
...@@ -306,9 +314,19 @@ template("test") { ...@@ -306,9 +314,19 @@ template("test") {
] ]
} }
ios_app_bundle(_test_target) { if (enable_run_ios_unittests_with_xctest) {
ios_test_target_type = "ios_xctest_test"
} else {
ios_test_target_type = "ios_app_bundle"
}
target(ios_test_target_type, _test_target) {
testonly = true testonly = true
if (enable_run_ios_unittests_with_xctest) {
xctest_module_target = "//base/test:google_test_runner"
}
# See above call. # See above call.
set_sources_assignment_filter([]) set_sources_assignment_filter([])
forward_variables_from(invoker, "*", [ "testonly" ]) forward_variables_from(invoker, "*", [ "testonly" ])
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment