FBSDKCoreKit/FBSDKCoreKitTests/Internal/FBSDKSwizzlerTests.m (421 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <XCTest/XCTest.h>
#import "FBSDKSwizzler.h"
#import "FBSDKSwizzler+Testing.h"
@interface FBSDKSwizzlerTestObject : NSObject
@property (nonatomic) int methodToSwizzleAsynchronouslyCallCount;
@property (nonatomic) int methodWithNoArgumentsCallCount;
@property (nonatomic) int methodWithOneArgumentCallCount;
@property (nonatomic) int methodWithTwoArgumentsCallCount;
@property (nonatomic) int methodWithThreeArgumentsCallCount;
@property (nonatomic) int methodWithFourArgumentsCallCount;
@property (nonatomic) int methodToUnswizzleCallCount;
@property (nonatomic) int methodToConflictCallCount;
@property (nonatomic) int methodToOverrideCallCount;
@property (nonatomic) int methodToSwizzleInSuperAndSubclassCallCount;
- (void)methodToSwizzleAsynchronously;
- (void)methodWithNoArguments;
- (void)methodWithOneArgument:(id)arg;
- (void)methodWithArgument:(id)arg
secondArgument:(id)arg2;
- (void)methodWithArgument:(id)arg
secondArgument:(id)arg2
thirdArgument:(id)arg3;
- (void)methodWithArgument:(id)arg
secondArgument:(id)arg2
thirdArgument:(id)arg3
fourthArgument:(id)arg4;
- (void)methodToUnswizzle;
- (void)methodToConflict;
- (void)methodToOverride;
- (void)methodToSwizzleInSuperAndSubclass;
@end
@implementation FBSDKSwizzlerTestObject
- (instancetype)init
{
if ((self = [super init])) {
_methodToSwizzleAsynchronouslyCallCount = 0;
_methodWithNoArgumentsCallCount = 0;
_methodWithOneArgumentCallCount = 0;
_methodWithTwoArgumentsCallCount = 0;
_methodWithThreeArgumentsCallCount = 0;
_methodWithFourArgumentsCallCount = 0;
_methodToUnswizzleCallCount = 0;
_methodToConflictCallCount = 0;
_methodToOverrideCallCount = 0;
}
return self;
}
- (void)methodToSwizzleAsynchronously
{
self.methodToSwizzleAsynchronouslyCallCount++;
}
- (void)methodWithNoArguments
{
self.methodWithNoArgumentsCallCount++;
}
- (void)methodWithOneArgument:(id)arg
{
self.methodWithOneArgumentCallCount++;
}
- (void)methodWithArgument:(id)arg
secondArgument:(id)arg2
{
self.methodWithTwoArgumentsCallCount++;
}
- (void)methodWithArgument:(id)arg
secondArgument:(id)arg2
thirdArgument:(id)arg3
{
self.methodWithThreeArgumentsCallCount++;
}
- (void)methodWithArgument:(id)arg
secondArgument:(id)arg2
thirdArgument:(id)arg3
fourthArgument:(id)arg4
{
self.methodWithFourArgumentsCallCount++;
}
- (void)methodToUnswizzle
{
self.methodToUnswizzleCallCount++;
}
- (void)methodToConflict
{
self.methodToConflictCallCount++;
}
- (void)methodToOverride
{
self.methodToOverrideCallCount++;
}
- (void)methodToSwizzleInSuperAndSubclass
{
self.methodToSwizzleInSuperAndSubclassCallCount++;
}
@end
@interface FBSDKSwizzlerTestObjectSubclass : FBSDKSwizzlerTestObject
@end
@implementation FBSDKSwizzlerTestObjectSubclass
- (void)methodToOverride
{
[super methodToOverride];
}
- (void)methodToSwizzleInSuperAndSubclass
{
self.methodToSwizzleInSuperAndSubclassCallCount++;
[super methodToSwizzleInSuperAndSubclass];
}
@end
@interface FBSDKSwizzlerTests : XCTestCase
@end
@implementation FBSDKSwizzlerTests
- (void)_testSwizzlingIsAsynchronousByDefault
{
__block int swizzleBlockInvocationCount = 0;
[FBSDKSwizzler swizzleSelector:@selector(methodToSwizzleAsynchronously)
onClass:FBSDKSwizzlerTestObject.class
withBlock:^{
swizzleBlockInvocationCount++;
}
named:self.name];
FBSDKSwizzlerTestObject *testObject = [FBSDKSwizzlerTestObject new];
[testObject methodToSwizzleAsynchronously];
XCTAssertEqual(
swizzleBlockInvocationCount,
0,
"Should not swizzle the method synchronously"
);
XCTAssertEqual(
testObject.methodToSwizzleAsynchronouslyCallCount,
1,
"Should invoke the original implementation once per method call"
);
// Predicates expectations are evaluated every second so we can just poll
// the method until it has been swizzled asynchronously
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL (id evaluatedObject, NSDictionary<NSString *, id> *bindings) {
[testObject methodToSwizzleAsynchronously];
return swizzleBlockInvocationCount > 0;
}];
XCTNSPredicateExpectation *expectation = [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:self];
[self waitForExpectations:@[expectation] timeout:3];
}
- (void)testSwizzlingMethodWithNoArguments
{
__block int swizzleBlockInvocationCount = 0;
[FBSDKSwizzler swizzleSelector:@selector(methodWithNoArguments)
onClass:FBSDKSwizzlerTestObject.class
withBlock:^{
swizzleBlockInvocationCount++;
}
named:self.name
async:NO];
FBSDKSwizzlerTestObject *testObject = [FBSDKSwizzlerTestObject new];
[testObject methodWithNoArguments];
[testObject methodWithNoArguments];
XCTAssertEqual(
swizzleBlockInvocationCount,
2,
"Should invoke the swizzle block once per method call"
);
XCTAssertEqual(
testObject.methodWithNoArgumentsCallCount,
2,
"Should invoke the original implementation once per method call"
);
}
- (void)testSwizzlingMethodWithOneArgument
{
__block int swizzleBlockInvocationCount = 0;
__block id capturedArgument = nil;
[FBSDKSwizzler swizzleSelector:@selector(methodWithOneArgument:)
onClass:FBSDKSwizzlerTestObject.class
withBlock:^(id caller, SEL command, id arg) {
swizzleBlockInvocationCount++;
capturedArgument = arg;
}
named:self.name
async:NO];
FBSDKSwizzlerTestObject *testObject = [FBSDKSwizzlerTestObject new];
[testObject methodWithOneArgument:@"Foo"];
XCTAssertEqualObjects(
capturedArgument,
@"Foo",
"Should invoke the swizzle block with the original arguments"
);
XCTAssertEqual(
testObject.methodWithOneArgumentCallCount,
1,
"Should invoke the original implementation"
);
XCTAssertEqual(
swizzleBlockInvocationCount,
1,
"Should invoke the swizzle block once per method call"
);
}
- (void)testSwizzlingMethodWithTwoArguments
{
__block id capturedArg1 = nil;
__block id capturedArg2 = nil;
__block int swizzleBlockInvocationCount = 0;
[FBSDKSwizzler swizzleSelector:@selector(methodWithArgument:secondArgument:)
onClass:FBSDKSwizzlerTestObject.class
withBlock:^(id caller, SEL command, id arg1, id arg2) {
capturedArg1 = arg1;
capturedArg2 = arg2;
swizzleBlockInvocationCount++;
}
named:self.name
async:NO];
NSString *arg1 = @"Foo";
NSString *arg2 = @"Bar";
FBSDKSwizzlerTestObject *testObject = [FBSDKSwizzlerTestObject new];
[testObject methodWithArgument:arg1
secondArgument:arg2];
XCTAssertEqualObjects(capturedArg1, arg1);
XCTAssertEqualObjects(capturedArg2, arg2);
XCTAssertEqual(
testObject.methodWithTwoArgumentsCallCount,
1,
"Should invoke the original implementation"
);
XCTAssertEqual(
swizzleBlockInvocationCount,
1,
"Should invoke the swizzle block once per method call"
);
}
- (void)testSwizzlingMethodWithMaxAllowedArguments
{
__block id capturedArg1 = nil;
__block id capturedArg2 = nil;
__block id capturedArg3 = nil;
__block int swizzleBlockInvocationCount = 0;
[FBSDKSwizzler swizzleSelector:@selector(methodWithArgument:secondArgument:thirdArgument:)
onClass:FBSDKSwizzlerTestObject.class
withBlock:^(id caller, SEL command, id arg1, id arg2, id arg3) {
capturedArg1 = arg1;
capturedArg2 = arg2;
capturedArg3 = arg3;
swizzleBlockInvocationCount++;
}
named:self.name
async:NO];
NSString *arg1 = @"Foo";
NSString *arg2 = @"Bar";
NSString *arg3 = @"Baz";
FBSDKSwizzlerTestObject *testObject = [FBSDKSwizzlerTestObject new];
[testObject methodWithArgument:arg1
secondArgument:arg2
thirdArgument:arg3];
XCTAssertEqualObjects(capturedArg1, arg1);
XCTAssertEqualObjects(capturedArg2, arg2);
XCTAssertEqualObjects(capturedArg3, arg3);
XCTAssertEqual(
testObject.methodWithThreeArgumentsCallCount,
1,
"Should invoke the original implementation"
);
XCTAssertEqual(
swizzleBlockInvocationCount,
1,
"Should invoke the swizzle block once per method call"
);
}
- (void)testSwizzlingMethodWithMoreThanMaxAllowedArguments
{
__block BOOL didInvokeSwizzleBlock = NO;
[FBSDKSwizzler swizzleSelector:@selector(methodWithArgument:secondArgument:thirdArgument:fourthArgument:) // fifthArgument:)
onClass:FBSDKSwizzlerTestObject.class
withBlock:^(id caller, SEL command, id arg1, id arg2, id arg3, id arg4) {
didInvokeSwizzleBlock = YES;
}
named:self.name
async:NO];
FBSDKSwizzlerTestObject *testObject = [FBSDKSwizzlerTestObject new];
[testObject methodWithArgument:@"Foo"
secondArgument:@"Foo"
thirdArgument:@"Foo"
fourthArgument:@"Foo"];
XCTAssertFalse(
didInvokeSwizzleBlock,
"Should not invoke the swizzle block if there are too many arguments"
);
XCTAssertEqual(
testObject.methodWithFourArgumentsCallCount,
1,
"Should invoke the original implementation"
);
}
- (void)testSwizzlingWithNoName
{
__block BOOL swizzleBlockWasCalled = NO;
[FBSDKSwizzler swizzleSelector:@selector(methodWithNoArguments)
onClass:FBSDKSwizzlerTestObject.class
withBlock:^{
swizzleBlockWasCalled = YES;
}
named:nil
async:NO];
FBSDKSwizzlerTestObject *testObject = [FBSDKSwizzlerTestObject new];
[testObject methodWithNoArguments];
XCTAssertFalse(
swizzleBlockWasCalled,
"Should not invoke the swizzle block for an unnamed swizzle"
);
XCTAssertEqual(
testObject.methodWithNoArgumentsCallCount,
1,
"Should invoke the original implementation once per method call"
);
}
- (void)testUnswizzling
{
__block BOOL swizzleBlockWasCalled = NO;
[FBSDKSwizzler swizzleSelector:@selector(methodToUnswizzle)
onClass:FBSDKSwizzlerTestObject.class
withBlock:^{
swizzleBlockWasCalled = YES;
}
named:self.name
async:NO];
FBSDKSwizzlerTestObject *testObject = [FBSDKSwizzlerTestObject new];
[testObject methodToUnswizzle];
XCTAssertTrue(swizzleBlockWasCalled);
// Reset the expectation
swizzleBlockWasCalled = NO;
[FBSDKSwizzler unswizzleSelector:@selector(methodToUnswizzle)
onClass:FBSDKSwizzlerTestObject.class
named:self.name];
[testObject methodToUnswizzle];
XCTAssertFalse(
swizzleBlockWasCalled,
"Should not invoke the swizzle block after the method has been unswizzled"
);
XCTAssertEqual(
testObject.methodToUnswizzleCallCount,
2,
"Should invoke the original implementation once per method call"
);
}
- (void)testConflictingSwizzles
{
__block int swizzleBlockInvocationCount = 0;
[FBSDKSwizzler swizzleSelector:@selector(methodToConflict)
onClass:FBSDKSwizzlerTestObject.class
withBlock:^{
XCTFail("Should not call the overridden swizzle block");
}
named:self.name
async:NO];
[FBSDKSwizzler swizzleSelector:@selector(methodToConflict)
onClass:FBSDKSwizzlerTestObject.class
withBlock:^{
swizzleBlockInvocationCount++;
}
named:self.name
async:NO];
FBSDKSwizzlerTestObject *testObject = [FBSDKSwizzlerTestObject new];
[testObject methodToConflict];
XCTAssertEqual(
swizzleBlockInvocationCount,
1,
"Should override the first swizzle block with the subsequent swizzle block"
);
XCTAssertEqual(
testObject.methodToConflictCallCount,
1,
"Should invoke the original implementation once per method call"
);
}
- (void)testSwizzlingSubclass
{
__block int swizzleBlockInvocationCount = 0;
[FBSDKSwizzler swizzleSelector:@selector(methodToOverride)
onClass:FBSDKSwizzlerTestObject.class
withBlock:^{
swizzleBlockInvocationCount++;
}
named:self.name
async:NO];
FBSDKSwizzlerTestObjectSubclass *testObject = [FBSDKSwizzlerTestObjectSubclass new];
[testObject methodToOverride];
XCTAssertEqual(
swizzleBlockInvocationCount,
1,
"Should find the swizzle on the superclass"
);
}
- (void)testSwizzlingWhenBothSubAndSuperclassAreSwizzled
{
// Swizzle the superclass
__block int firstSwizzleBlockCallCount = 0;
[FBSDKSwizzler swizzleSelector:@selector(methodToSwizzleInSuperAndSubclass)
onClass:FBSDKSwizzlerTestObject.class
withBlock:^{
firstSwizzleBlockCallCount++;
}
named:self.name
async:NO];
// Swizzle the same method on the subclass
__block int secondSwizzleBlockCallCount = 0;
[FBSDKSwizzler swizzleSelector:@selector(methodToSwizzleInSuperAndSubclass)
onClass:FBSDKSwizzlerTestObjectSubclass.class
withBlock:^{
secondSwizzleBlockCallCount++;
}
named:self.name
async:NO];
FBSDKSwizzlerTestObjectSubclass *testObject = [FBSDKSwizzlerTestObjectSubclass new];
[testObject methodToSwizzleInSuperAndSubclass];
XCTAssertEqual(
firstSwizzleBlockCallCount,
1,
"Should call the swizzle block on the superclass"
);
XCTAssertEqual(
secondSwizzleBlockCallCount,
1,
"Should call the swizzle block on the subclass"
);
XCTAssertEqual(
testObject.methodToSwizzleInSuperAndSubclassCallCount,
2,
"Should call the original method on the superclass and subclass"
);
}
@end