Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inject XCTestCases directly into XCTest #7

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 89 additions & 144 deletions Bundle/GoogleTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,6 @@
*/
static NSString * const GeneratedClassPrefix = @"";

/**
* Map of test keys to Google Test filter strings.
*
* Some names allowed by Google Test would result in illegal Objective-C
* identifiers and in such cases the generated class and method names are
* adjusted to handle this. This map is used to obtain the original Google Test
* filter string associated with a generated Objective-C test method.
*/
static NSDictionary *GoogleTestFilterMap;

/**
* A Google Test listener that reports failures to XCTest.
*/
Expand All @@ -77,77 +67,23 @@ void OnTestPartResult(const TestPartResult& test_part_result) {
XCTestCase *_testCase;
};

/**
* Registers an XCTestCase subclass for each Google Test case.
*
* Generating these classes allows Google Test cases to be represented as peers
* of standard XCTest suites and supports filtering of test runs to specific
* Google Test cases or individual tests via Xcode.
*/
@interface GoogleTestLoader : NSObject
@end

/**
* Base class for the generated classes for Google Test cases.
*/
@interface GoogleTestCase : XCTestCase
@end

@implementation GoogleTestCase

/**
* Associates generated Google Test classes with the test bundle.
*
* This affects how the generated test cases are represented in reports. By
* associating the generated classes with a test bundle the Google Test cases
* appear to be part of the same test bundle that this source file is compiled
* into. Without this association they appear to be part of a bundle
* representing the directory of an internal Xcode tool that runs the tests.
*/
+ (NSBundle *)bundleForClass {
return [NSBundle bundleForClass:[GoogleTestLoader class]];
}

/**
* Implementation of +[XCTestCase testInvocations] that returns an array of test
* invocations for each test method in the class.
*
* This differs from the standard implementation of testInvocations, which only
* adds methods with a prefix of "test".
*/
+ (NSArray *)testInvocations {
NSMutableArray *invocations = [NSMutableArray array];

unsigned int methodCount = 0;
Method *methods = class_copyMethodList([self class], &methodCount);

for (unsigned int i = 0; i < methodCount; i++) {
SEL sel = method_getName(methods[i]);
NSMethodSignature *sig = [self instanceMethodSignatureForSelector:sel];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setSelector:sel];
[invocations addObject:invocation];
}

free(methods);

return invocations;
}

@end

/**
* Runs a single test.
*/
static void RunTest(id self, SEL _cmd) {
static void RunTest(id self, NSString *testFilter) {
XCTestListener *listener = new XCTestListener(self);
UnitTest *googleTest = UnitTest::GetInstance();
googleTest->listeners().Append(listener);

NSString *testKey = [NSString stringWithFormat:@"%@.%@", [self class], NSStringFromSelector(_cmd)];
NSString *testFilter = GoogleTestFilterMap[testKey];
XCTAssertNotNil(testFilter, @"No test filter found for test %@", testKey);

testing::GTEST_FLAG(filter) = [testFilter UTF8String];

(void)RUN_ALL_TESTS();
Expand All @@ -158,107 +94,116 @@ static void RunTest(id self, SEL _cmd) {
XCTAssertEqual(totalTestsRun, 1, @"Expected to run a single test for filter \"%@\"", testFilter);
}

@implementation GoogleTestLoader

/**
* Performs registration of classes for Google Test cases after our bundle has
* finished loading.
*
* This registration needs to occur before XCTest queries the runtime for test
* subclasses, but after C++ static initializers have run so that all Google
* Test cases have been registered. This is accomplished by synchronously
* observing the NSBundleDidLoadNotification for our own bundle.
* Test suite for the entire set of gtests. Finds all registered tests and adds them to itself.
*/
+ (void)load {
NSBundle *bundle = [NSBundle bundleForClass:self];
[[NSNotificationCenter defaultCenter] addObserverForName:NSBundleDidLoadNotification object:bundle queue:nil usingBlock:^(NSNotification *notification) {
[self registerTestClasses];
}];
}
@interface GoogleTestSuite : XCTestSuite
@end

+ (void)registerTestClasses {
// Pass the command-line arguments to Google Test to support the --gtest options
NSArray *arguments = [[NSProcessInfo processInfo] arguments];
@implementation GoogleTestSuite

int i = 0;
int argc = (int)[arguments count];
const char **argv = (const char **)calloc((unsigned int)argc + 1, sizeof(const char *));
for (NSString *arg in arguments) {
argv[i++] = [arg UTF8String];
}
- (instancetype)init
{
if (self = [self initWithName:@"GoogleTestSuite"]) {
// Pass the command-line arguments to Google Test to support the --gtest options
NSArray *arguments = [[NSProcessInfo processInfo] arguments];

testing::InitGoogleTest(&argc, (char **)argv);
UnitTest *googleTest = UnitTest::GetInstance();
testing::TestEventListeners& listeners = googleTest->listeners();
delete listeners.Release(listeners.default_result_printer());
free(argv);
int i = 0;
int argc = (int)[arguments count];
const char **argv = (const char **)calloc((unsigned int)argc + 1, sizeof(const char *));
for (NSString *arg in arguments) {
argv[i++] = [arg UTF8String];
}

testing::InitGoogleTest(&argc, (char **)argv);
UnitTest *googleTest = UnitTest::GetInstance();
testing::TestEventListeners& listeners = googleTest->listeners();
delete listeners.Release(listeners.default_result_printer());
free(argv);

BOOL runDisabledTests = testing::GTEST_FLAG(also_run_disabled_tests);
NSMutableDictionary *testFilterMap = [NSMutableDictionary dictionary];
NSCharacterSet *decimalDigitCharacterSet = [NSCharacterSet decimalDigitCharacterSet];
BOOL runDisabledTests = testing::GTEST_FLAG(also_run_disabled_tests);
NSCharacterSet *decimalDigitCharacterSet = [NSCharacterSet decimalDigitCharacterSet];

for (int testCaseIndex = 0; testCaseIndex < googleTest->total_test_case_count(); testCaseIndex++) {
const TestCase *testCase = googleTest->GetTestCase(testCaseIndex);
NSString *testCaseName = @(testCase->name());
for (int testCaseIndex = 0; testCaseIndex < googleTest->total_test_case_count(); testCaseIndex++) {
const TestCase *testCase = googleTest->GetTestCase(testCaseIndex);
NSString *testCaseName = @(testCase->name());

// For typed tests '/' is used to separate the parts of the test case name.
NSArray *testCaseNameComponents = [testCaseName componentsSeparatedByString:@"/"];
// For typed tests '/' is used to separate the parts of the test case name.
NSArray *testCaseNameComponents = [testCaseName componentsSeparatedByString:@"/"];

if (runDisabledTests == NO) {
BOOL testCaseDisabled = NO;
if (runDisabledTests == NO) {
BOOL testCaseDisabled = NO;

for (NSString *component in testCaseNameComponents) {
if ([component hasPrefix:GoogleTestDisabledPrefix]) {
testCaseDisabled = YES;
break;
for (NSString *component in testCaseNameComponents) {
if ([component hasPrefix:GoogleTestDisabledPrefix]) {
testCaseDisabled = YES;
break;
}
}
}

if (testCaseDisabled) {
continue;
if (testCaseDisabled) {
continue;
}
}
}

// Join the test case name components with '_' rather than '/' to create
// a valid class name.
NSString *className = [GeneratedClassPrefix stringByAppendingString:[testCaseNameComponents componentsJoinedByString:@"_"]];
// Join the test case name components with '_' rather than '/' to create
// a valid class name.
NSString *className = [GeneratedClassPrefix stringByAppendingString:[testCaseNameComponents componentsJoinedByString:@"_"]];

Class testClass = objc_allocateClassPair([GoogleTestCase class], [className UTF8String], 0);
NSAssert1(testClass, @"Failed to register Google Test class \"%@\", this class may already exist. The value of GeneratedClassPrefix can be changed to avoid this.", className);
BOOL hasMethods = NO;
Class testClass = objc_allocateClassPair([GoogleTestCase class], [className UTF8String], 0);
NSAssert1(testClass, @"Failed to register Google Test class \"%@\", this class may already exist. The value of GeneratedClassPrefix can be changed to avoid this.", className);
std::vector<SEL> selectors;

for (int testIndex = 0; testIndex < testCase->total_test_count(); testIndex++) {
const TestInfo *testInfo = testCase->GetTestInfo(testIndex);
NSString *testName = @(testInfo->name());
if (runDisabledTests == NO && [testName hasPrefix:GoogleTestDisabledPrefix]) {
continue;
}
for (int testIndex = 0; testIndex < testCase->total_test_count(); testIndex++) {
const TestInfo *testInfo = testCase->GetTestInfo(testIndex);
NSString *testName = @(testInfo->name());
if (runDisabledTests == NO && [testName hasPrefix:GoogleTestDisabledPrefix]) {
continue;
}

// Google Test allows test names starting with a digit, prefix these with an
// underscore to create a valid method name.
NSString *methodName = testName;
if ([methodName length] > 0 && [decimalDigitCharacterSet characterIsMember:[methodName characterAtIndex:0]]) {
methodName = [@"_" stringByAppendingString:methodName];
}
// Google Test allows test names starting with a digit, prefix these with an
// underscore to create a valid method name.
NSString *methodName = testName;
if ([methodName length] > 0 && [decimalDigitCharacterSet characterIsMember:[methodName characterAtIndex:0]]) {
methodName = [@"_" stringByAppendingString:methodName];
}

NSString *testKey = [NSString stringWithFormat:@"%@.%@", className, methodName];
NSString *testFilter = [NSString stringWithFormat:@"%@.%@", testCaseName, testName];
testFilterMap[testKey] = testFilter;
// Google Test set test method name in parameterized tests to <name>/<index>.
// Replace / with a _ to create a valid method name.
methodName = [methodName stringByReplacingOccurrencesOfString:@"/" withString:@"_"];

SEL selector = sel_registerName([methodName UTF8String]);
BOOL added = class_addMethod(testClass, selector, (IMP)RunTest, "v@:");
NSAssert1(added, @"Failed to add Goole Test method \"%@\", this method may already exist in the class.", methodName);
hasMethods = YES;
}
NSString *testFilter = [NSString stringWithFormat:@"%@.%@", testCaseName, testName];
IMP imp = imp_implementationWithBlock(^void (id self_) { RunTest(self_, testFilter); });
SEL selector = sel_registerName([methodName UTF8String]);
BOOL added = class_addMethod(testClass, selector, imp, "v@:");
NSAssert1(added, @"Failed to add Goole Test method \"%@\", this method may already exist in the class.", methodName);
selectors.push_back(selector);
}

if (hasMethods) {
objc_registerClassPair(testClass);
} else {
objc_disposeClassPair(testClass);
if (!selectors.empty()) {
objc_registerClassPair(testClass);
for (SEL s : selectors) {
[self addTest:[testClass testCaseWithSelector:s]];
}
} else {
objc_disposeClassPair(testClass);
}
}
}

GoogleTestFilterMap = testFilterMap;
return self;
}

@end

/**
* Test case that bootstraps the test suite containing all the gtests.
*/
@interface GoogleTests : XCTestCase
@end

@implementation GoogleTests
+ (XCTestSuite *)defaultTestSuite
{
return [GoogleTestSuite new];
}
@end