/* * 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 #import #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* 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* _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* 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 { config = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath]; } 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* (*TestsToRunFunction)(id, SEL); IMP testsToRunMethod = [config methodForSelector:testsToRunSelector]; auto testsToRunFunction = reinterpret_cast(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* testsToRun) { NSMutableString* result = [[NSMutableString alloc] init]; for (NSString* spec in testsToRun) { NSArray* 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(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* 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(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(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* allTests = [NSSet setWithObject:masterTestCaseName]; NSSet* 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* 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