blob: 4f52fd8d438317f7a2b139c6c4b5b915a1c6e05b [file] [log] [blame]
//
// Copyright 2018 Google Inc.
//
// 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 "GTXPluginXCTestCase.h"
#import "GTXAssertions.h"
#import "GTXiLibCore.h"
#import "GTXTestEnvironment.h"
#import <objc/runtime.h>
/**
Reference to XCTestCase class which will be dynamically set when needed, this allows for tests as
well as apps (which dont link to XCTest) use GTX.
*/
Class gXCTestCaseClass;
@implementation GTXPluginXCTestCase
+ (void)installPlugin {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
gXCTestCaseClass = NSClassFromString(@"XCTestCase");
NSAssert(gXCTestCaseClass, @"XCTestCase class was not found, either XCTest framework is not "
@"being linked or not loaded yet.");
[self gtx_addInstanceMethod:@selector(gtx_tearDown)
fromClass:self
toClass:gXCTestCaseClass];
[self gtx_addInstanceMethod:@selector(gtx_invokeTest)
fromClass:self
toClass:gXCTestCaseClass];
[self gtx_swizzleInstanceMethod:@selector(invokeTest)
withMethod:@selector(gtx_invokeTest)
inClass:gXCTestCaseClass];
[GTXTestEnvironment setupEnvironment];
});
}
#pragma mark - XCTestCase Methods
- (void)tearDown {
NSAssert(NO, @"This method was added only for a reference to the selector but must never be "
@"invoked directly.");
}
- (void)invokeTest {
NSAssert(NO, @"This method was added only for a reference to the selector but must never be "
@"invoked directly.");
}
#pragma mark - Private
/**
Adds the given method from the given source to the given destination class.
@param methodSelector selector of the method to be added.
@param srcClass Source class from where to get the method implementation
@param destClass destination class where the method implementation is to be added.
*/
+ (void)gtx_addInstanceMethod:(SEL)methodSelector
fromClass:(Class)srcClass
toClass:(Class)destClass {
Method instanceMethod = class_getInstanceMethod(srcClass, methodSelector);
const char *typeEncoding = method_getTypeEncoding(instanceMethod);
NSAssert(typeEncoding, @"Failed to get method type encoding.");
BOOL success = class_addMethod(destClass,
methodSelector,
method_getImplementation(instanceMethod),
typeEncoding);
NSAssert(success, @"Failed to add %@ from %@ to %@",
NSStringFromSelector(methodSelector), srcClass, destClass);
(void)success; // Ensures 'success' is marked as used even if NSAssert is removed.
}
/**
Swizzles the given methods in the given class.
@param methodSelector1 Selector for the original method.
@param methodSelector2 Selector for the method to be swizzled with.
@param clazz Target class to be swizzled in.
*/
+ (void)gtx_swizzleInstanceMethod:(SEL)methodSelector1
withMethod:(SEL)methodSelector2
inClass:(Class)clazz {
Method method1 = class_getInstanceMethod(clazz, methodSelector1);
Method method2 = class_getInstanceMethod(clazz, methodSelector2);
// Only swizzle if both methods are found
if (method1 && method2) {
IMP imp1 = method_getImplementation(method1);
IMP imp2 = method_getImplementation(method2);
if (class_addMethod(clazz, methodSelector1, imp2, method_getTypeEncoding(method2))) {
class_replaceMethod(clazz, methodSelector2, imp1, method_getTypeEncoding(method1));
} else {
method_exchangeImplementations(method1, method2);
}
} else {
GTX_ASSERT(NO, @"Cannot swizzle %@", NSStringFromSelector(methodSelector1));
}
}
/**
@return Closest superclass to @c clazz that has -tearDown method.
*/
+ (Class)gtx_classWithInstanceTearDown:(Class)clazz {
while (clazz != gXCTestCaseClass &&
![clazz instanceMethodForSelector:@selector(tearDown)]) {
clazz = [clazz superclass];
}
return clazz;
}
/**
Wires up teardown in the given class so that -gtx_tearDown is invoked instead.
@param clazz The class to be modified.
*/
+ (void)gtx_wireUpTeardownInClass:(Class)clazz {
Class baseClass = gXCTestCaseClass;
// If gtx_tearDown does not exists in clazz, add it first.
if (baseClass != clazz) {
Method defaultGTXTearDown = class_getInstanceMethod(baseClass, @selector(gtx_tearDown));
struct objc_method_description *desc = method_getDescription(defaultGTXTearDown);
IMP gtxTearDownIMP = [baseClass instanceMethodForSelector:@selector(gtx_tearDown)];
class_addMethod(clazz, @selector(gtx_tearDown), gtxTearDownIMP, desc->types);
}
[self gtx_swizzleInstanceMethod:@selector(tearDown)
withMethod:@selector(gtx_tearDown)
inClass:clazz];
}
/**
Restores @c clazz to its original state (opposite of +gtx_wireUpTeardownInClass).
*/
+ (void)gtx_unWireTeardownInClass:(Class)clazz {
// Swizzle again to restore tearDown/gtx_tearDown to their original IMPs.
[self gtx_swizzleInstanceMethod:@selector(tearDown)
withMethod:@selector(gtx_tearDown)
inClass:clazz];
}
#pragma mark - Swizzled methods
/**
Swizzled method for -invokeTest method that posts notifications when test begins.
*/
- (void)gtx_invokeTest {
id actualSelf = self; // Note that self is of type XCTestCase
Class classWithTearDown =
[[GTXPluginXCTestCase class] gtx_classWithInstanceTearDown:[self class]];
[[GTXPluginXCTestCase class] gtx_wireUpTeardownInClass:classWithTearDown];
[[NSNotificationCenter defaultCenter] postNotificationName:gtxTestCaseDidBeginNotification
object:self
userInfo:@{gtxTestClassUserInfoKey:
[self class],
gtxTestInvocationUserInfoKey:
[actualSelf invocation]}];
@try {
typedef void (*InvokeTestMethodType)(id, SEL);
InvokeTestMethodType method =
(InvokeTestMethodType)[self methodForSelector:@selector(gtx_invokeTest)];
method(self, _cmd);
} @finally {
[[GTXPluginXCTestCase class] gtx_unWireTeardownInClass:classWithTearDown];
}
}
/**
Swizzled method for -tearDown method that posts notifications for teardown.
*/
- (void)gtx_tearDown {
id actualSelf = self; // Note that self is of type XCTestCase
[[NSNotificationCenter defaultCenter] postNotificationName:gtxTestCaseDidEndNotification
object:self
userInfo:@{gtxTestClassUserInfoKey:
[self class],
gtxTestInvocationUserInfoKey:
[actualSelf invocation]}];
typedef void (*TearDownMethodType)(id, SEL);
TearDownMethodType method =
(TearDownMethodType)[self methodForSelector:@selector(gtx_tearDown)];
method(self, _cmd);
}
@end