| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452 |
- /*
- * Copyright 2017 Google LLC
- *
- * 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.
- */
- #import <XCTest/XCTest.h>
- #import <objc/runtime.h>
- #include "gtest/gtest.h"
- /**
- * An XCTest test case that finds C++ test cases written in the GoogleTest
- * framework, runs them, and reports the results back to Xcode. This allows
- * tests written in C++ that don't rely on XCTest to coexist in this project.
- *
- * As an extra feature, you can run all C++ tests by focusing on the GoogleTests
- * class.
- *
- * Each GoogleTest TestCase is mapped to a dynamically generated XCTestCase
- * class. Each GoogleTest TEST() is mapped to a test method on that XCTestCase.
- */
- @interface GoogleTests : XCTestCase
- @end
- namespace {
- // A testing::TestCase named "Foo" corresponds to an XCTestCase named
- // "FooTests".
- NSString* const kTestCaseSuffix = @"Tests";
- // A testing::TestInfo named "Foo" corresponds to test method named "testFoo".
- NSString* const kTestMethodPrefix = @"test";
- // A map of keys created by TestInfoKey to the corresponding testing::TestInfo
- // (wrapped in an NSValue). The generated XCTestCase classes are discovered and
- // instantiated by XCTest so this is the only means of plumbing per-test-method
- // state into these methods.
- NSDictionary<NSString*, NSValue*>* testInfosByKey;
- // If the user focuses on GoogleTests itself, this means force all C++ tests to
- // run.
- bool forceAllTests = false;
- void RunGoogleTestTests();
- /**
- * Loads this XCTest runner's configuration file and figures out which tests to
- * run based on the contents of that configuration file.
- *
- * @return the set of tests to run, or nil if the user asked for all tests or if
- * there's any problem loading or parsing the configuration.
- */
- NSSet<NSString*>* _Nullable LoadXCTestConfigurationTestsToRun() {
- // Xcode invokes the test runner with an XCTestConfigurationFilePath
- // environment variable set to the path of a configuration file containing,
- // among other things, the set of tests to run. The configuration file
- // deserializes to a non-public XCTestConfiguration class.
- //
- // This loads that file and then reflectively pulls out the testsToRun set.
- // Just in case any of these private details should change in the future and
- // something should fail here, the mechanism complains but fails open. This
- // way the worst that can happen is that users end up running more tests than
- // they intend, but we never accidentally show a green run that wasn't.
- static NSString* const configEnvVar = @"XCTestConfigurationFilePath";
- NSDictionary<NSString*, NSString*>* env =
- [[NSProcessInfo processInfo] environment];
- NSString* filePath = [env objectForKey:configEnvVar];
- if (!filePath) {
- NSLog(@"Missing %@ environment variable; assuming all tests", configEnvVar);
- return nil;
- }
- id config;
- NSError* error;
- if (@available(macOS 10.13, iOS 11, tvOS 11, *)) {
- NSData* data = [NSData dataWithContentsOfFile:filePath
- options:kNilOptions
- error:&error];
- if (!data) {
- NSLog(@"Failed to fill data with contents of file. %@", error);
- return nil;
- }
- config = [NSKeyedUnarchiver unarchivedObjectOfClass:NSObject.class
- fromData:data
- error:&error];
- } else {
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated-declarations"
- config = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
- #pragma clang diagnostic pop
- }
- if (!config) {
- NSLog(@"Failed to load any configuaration from %@=%@. %@", configEnvVar,
- filePath, error);
- return nil;
- }
- SEL testsToRunSelector = NSSelectorFromString(@"testsToRun");
- if (![config respondsToSelector:testsToRunSelector]) {
- NSLog(@"Invalid configuaration from %@=%@: missing testsToRun",
- configEnvVar, filePath);
- return nil;
- }
- // Invoke the testsToRun selector safely. This indirection is required because
- // just calling -performSelector: fails to properly retain the NSSet under
- // ARC.
- typedef NSSet<NSString*>* (*TestsToRunFunction)(id, SEL);
- IMP testsToRunMethod = [config methodForSelector:testsToRunSelector];
- auto testsToRunFunction =
- reinterpret_cast<TestsToRunFunction>(testsToRunMethod);
- return testsToRunFunction(config, testsToRunSelector);
- }
- /**
- * Creates a GoogleTest filter specification, suitable for passing to the
- * --gtest_filter flag, that limits GoogleTest to running the same set of tests
- * that Xcode requested.
- *
- * Each member of the testsToRun set is mapped as follows:
- *
- * * Bare class: "ClassTests" => "Class.*"
- * * Class and method: "ClassTests/testMethod" => "Class.Method"
- *
- * These members are then joined with a ":" as googletest requires.
- *
- * @see
- * https://github.com/google/googletest/blob/master/googletest/docs/AdvancedGuide.md
- */
- NSString* CreateTestFiltersFromTestsToRun(NSSet<NSString*>* testsToRun) {
- NSMutableString* result = [[NSMutableString alloc] init];
- for (NSString* spec in testsToRun) {
- NSArray<NSString*>* parts = [spec componentsSeparatedByString:@"/"];
- NSString* gtestCaseName = nil;
- if (parts.count > 0) {
- NSString* className = parts[0];
- if ([className hasSuffix:kTestCaseSuffix]) {
- gtestCaseName = [className
- substringToIndex:className.length - kTestCaseSuffix.length];
- }
- }
- NSString* gtestMethodName = nil;
- if (parts.count > 1) {
- NSString* methodName = parts[1];
- if ([methodName hasPrefix:kTestMethodPrefix]) {
- gtestMethodName =
- [methodName substringFromIndex:kTestMethodPrefix.length];
- }
- }
- if (gtestCaseName) {
- if (result.length > 0) {
- [result appendString:@":"];
- }
- [result appendString:gtestCaseName];
- [result appendString:@"."];
- [result appendString:(gtestMethodName ? gtestMethodName : @"*")];
- }
- }
- return result;
- }
- /** Returns the name of the selector for the test method representing this
- * specific test. */
- NSString* SelectorNameForTestInfo(const testing::TestInfo* testInfo) {
- return
- [NSString stringWithFormat:@"%@%s", kTestMethodPrefix, testInfo->name()];
- }
- /** Returns the name of the class representing the given testing::TestCase. */
- NSString* ClassNameForTestCase(const testing::TestCase* testCase) {
- return [NSString stringWithFormat:@"%s%@", testCase->name(), kTestCaseSuffix];
- }
- /**
- * Returns a key name for the testInfosByKey dictionary. Each (class, selector)
- * pair corresponds to a unique GoogleTest result.
- */
- NSString* TestInfoKey(Class testClass, SEL testSelector) {
- return [NSString stringWithFormat:@"%@.%@", NSStringFromClass(testClass),
- NSStringFromSelector(testSelector)];
- }
- /**
- * A function that is the implementation for each generated test method. It
- * shouldn't be used directly--instead use it with class_addMethod to define the
- * behavior of the generated XCTestCase class.
- *
- * The first invocation of this method runs all GoogleTest tests. Delaying
- * execution this way allows XCTest to register to the test runner that it
- * actually has started.
- *
- * Looks up the testing::TestInfo for this test method and reports on the
- * outcome to XCTest, as if the test actually ran in this method.
- *
- * Note: The parameter names of self and _cmd match up with the implicit
- * parameters passed to any Objective-C method. Naming them this way here allows
- * XCTAssert and friends to work.
- */
- void XCTestMethod(XCTestCase* self, SEL _cmd) {
- RunGoogleTestTests();
- NSString* testInfoKey = TestInfoKey([self class], _cmd);
- NSValue* holder = testInfosByKey[testInfoKey];
- auto testInfo = static_cast<const testing::TestInfo*>(holder.pointerValue);
- if (!testInfo) {
- return;
- }
- if (!testInfo->should_run()) {
- // Test was filtered out by gunit; nothing to report.
- return;
- }
- const testing::TestResult* result = testInfo->result();
- if (result->Passed()) {
- // Let XCode know that the test ran and succeeded.
- XCTAssertTrue(true);
- return;
- } else if (result->Skipped()) {
- #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130400 || \
- __TV_OS_VERSION_MAX_ALLOWED >= 130400 || \
- __MAC_OS_X_VERSION_MAX_ALLOWED >= 101504
- // Let XCode know that the test was skipped.
- XCTSkip();
- #endif
- }
- // Test failed :-(. Record the failure such that XCode will navigate directly
- // to the file:line.
- int parts = result->total_part_count();
- for (int i = 0; i < parts; i++) {
- const testing::TestPartResult& part = result->GetTestPartResult(i);
- const char* path = part.file_name() ? part.file_name() : "";
- int line = part.line_number() > 0 ? part.line_number() : 0;
- #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 || \
- __MAC_OS_X_VERSION_MAX_ALLOWED >= 101500
- // Xcode 12
- auto* location = [[XCTSourceCodeLocation alloc] initWithFilePath:@(path)
- lineNumber:line];
- auto* context = [[XCTSourceCodeContext alloc] initWithLocation:location];
- auto* issue = [[XCTIssue alloc] initWithType:XCTIssueTypeAssertionFailure
- compactDescription:@(part.summary())
- detailedDescription:@(part.message())
- sourceCodeContext:context
- associatedError:nil
- attachments:@[]];
- [self recordIssue:issue];
- #else
- // Xcode 11 and prior. recordFailureWithDescription:inFile:atLine:expected:
- // is deprecated in Xcode 12.
- [self recordFailureWithDescription:@(part.message())
- inFile:@(path)
- atLine:line
- expected:true];
- #endif
- }
- }
- /**
- * Generates a new subclass of XCTestCase for the given GoogleTest TestCase.
- * Each TestInfo (which represents an individual test method execution) is
- * translated into a method on the test case.
- *
- * @param testCase The testing::TestCase of interest to translate.
- * @param infoMap A map of TestInfoKeys to testing::TestInfos, populated by this
- * method.
- *
- * @return A new Class that's a subclass of XCTestCase, that's been registered
- * with the Objective-C runtime.
- */
- Class CreateXCTestCaseClass(const testing::TestCase* testCase,
- NSMutableDictionary<NSString*, NSValue*>* infoMap) {
- NSString* testCaseName = ClassNameForTestCase(testCase);
- Class testClass =
- objc_allocateClassPair([XCTestCase class], [testCaseName UTF8String], 0);
- // Create a method for each TestInfo.
- int testInfos = testCase->total_test_count();
- for (int j = 0; j < testInfos; j++) {
- const testing::TestInfo* testInfo = testCase->GetTestInfo(j);
- NSString* selectorName = SelectorNameForTestInfo(testInfo);
- SEL selector = sel_registerName([selectorName UTF8String]);
- // Use the XCTestMethod function as the method implementation. The v@:
- // indicates it is a void objective-C method; this must continue to match
- // the signature of XCTestMethod.
- IMP method = reinterpret_cast<IMP>(XCTestMethod);
- class_addMethod(testClass, selector, method, "v@:");
- NSString* infoKey = TestInfoKey(testClass, selector);
- NSValue* holder = [NSValue valueWithPointer:testInfo];
- infoMap[infoKey] = holder;
- }
- objc_registerClassPair(testClass);
- return testClass;
- }
- /**
- * Creates a test suite containing all C++ tests, used when the user starts the
- * GoogleTests class.
- *
- * Note: normally XCTest finds all the XCTestCase classes that are registered
- * with the run time and asks them to create suites for themselves. When a user
- * focuses on the GoogleTests class, XCTest no longer does this so we have to
- * force XCTest to see more tests than it would normally look at so that the
- * indicators in the test navigator update properly.
- */
- XCTestSuite* CreateAllTestsTestSuite() {
- XCTestSuite* allTestsSuite =
- [[XCTestSuite alloc] initWithName:@"All GoogleTest Tests"];
- [allTestsSuite
- addTest:[XCTestSuite testSuiteForTestCaseClass:[GoogleTests class]]];
- const testing::UnitTest* master = testing::UnitTest::GetInstance();
- int testCases = master->total_test_case_count();
- for (int i = 0; i < testCases; i++) {
- const testing::TestCase* testCase = master->GetTestCase(i);
- NSString* testCaseName = ClassNameForTestCase(testCase);
- Class testClass = objc_getClass([testCaseName UTF8String]);
- [allTestsSuite addTest:[XCTestSuite testSuiteForTestCaseClass:testClass]];
- }
- return allTestsSuite;
- }
- /**
- * Finds and runs googletest-based tests based on the XCTestConfiguration of the
- * current test invocation.
- */
- void CreateGoogleTestTests() {
- NSString* masterTestCaseName = NSStringFromClass([GoogleTests class]);
- // Initialize GoogleTest but don't run the tests yet.
- int argc = 1;
- const char* argv[] = {[masterTestCaseName UTF8String]};
- testing::InitGoogleTest(&argc, const_cast<char**>(argv));
- // Convert XCTest's testToRun set to the equivalent --gtest_filter flag.
- //
- // Note that we only set forceAllTests to true if the user specifically
- // focused on GoogleTests. This prevents XCTest double-counting test cases
- // (and failures) when a user asks for all tests.
- NSSet<NSString*>* allTests = [NSSet setWithObject:masterTestCaseName];
- NSSet<NSString*>* testsToRun = LoadXCTestConfigurationTestsToRun();
- if (testsToRun) {
- if ([allTests isEqual:testsToRun]) {
- NSLog(@"Forcing all tests to run");
- forceAllTests = true;
- } else {
- NSString* filters = CreateTestFiltersFromTestsToRun(testsToRun);
- NSLog(@"Using --gtest_filter=%@", filters);
- if (filters) {
- testing::GTEST_FLAG(filter) = [filters UTF8String];
- }
- }
- }
- // Create XCTestCases and populate the testInfosByKey map
- const testing::UnitTest* master = testing::UnitTest::GetInstance();
- NSMutableDictionary<NSString*, NSValue*>* infoMap =
- [NSMutableDictionary dictionaryWithCapacity:master->total_test_count()];
- int testCases = master->total_test_case_count();
- for (int i = 0; i < testCases; i++) {
- const testing::TestCase* testCase = master->GetTestCase(i);
- CreateXCTestCaseClass(testCase, infoMap);
- }
- testInfosByKey = infoMap;
- }
- void RunGoogleTestTests() {
- static bool firstRun = true;
- if (firstRun) {
- firstRun = false;
- int result = RUN_ALL_TESTS();
- // RUN_ALL_TESTS by default doesn't want you to ignore its result, but it's
- // safe here. Test failures are already logged by GoogleTest itself (and
- // then again by XCTest). Test failures are reported via
- // -recordFailureWithDescription:inFile:atLine:expected: which then causes
- // XCTest itself to fail the run.
- (void)result;
- }
- }
- } // namespace
- @implementation GoogleTests
- + (XCTestSuite*)defaultTestSuite {
- // Only return all tests beyond GoogleTests if the user is focusing on
- // GoogleTests.
- if (forceAllTests) {
- return CreateAllTestsTestSuite();
- } else {
- // just run the tests that are a part of this class
- return [XCTestSuite testSuiteForTestCaseClass:[self class]];
- }
- }
- - (void)testGoogleTestsActuallyRun {
- // This whole mechanism is sufficiently tricky that we should verify that the
- // build actually plumbed this together correctly.
- const testing::UnitTest* master = testing::UnitTest::GetInstance();
- XCTAssertGreaterThan(master->total_test_case_count(), 0);
- }
- @end
- /**
- * This class is registered as the NSPrincipalClass in the Firestore_Tests
- * bundle's Info.plist. XCTest instantiates this class to perform one-time setup
- * for the test bundle, as documented here:
- *
- * https://developer.apple.com/documentation/xctest/xctestobservationcenter
- */
- @interface FSTGoogleTestsPrincipal : NSObject
- @end
- @implementation FSTGoogleTestsPrincipal
- - (instancetype)init {
- self = [super init];
- CreateGoogleTestTests();
- return self;
- }
- @end
|