From 1bd6c0d956ba03065a94527daf6126a05b3ae8cc Mon Sep 17 00:00:00 2001 From: Andrew Chang Date: Thu, 23 Dec 2021 13:32:37 -1000 Subject: [PATCH] Fix wildcard arg matching for Obj-C param types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dynamic mocking (introduced in 0.18) changed how Objective-C parameter types are faked for wildcard argument matching. To support Objective-C mocking, argument matchers for `NSObjectProtocol` parameters no longer create type facades that go through the resolution context thread, but instead create dynamic `NSProxy` doubles. Dynamic type facades weren’t set up to be used from Swift; this change enables Swift to properly resolve the type facades. --- Mockingbird.xcodeproj/project.pbxproj | 8 ++ .../Matching/TypeFacade.swift | 17 ++- .../Bridge/include/MKBTypeFacade.h | 9 +- .../Bridge/sources/MKBTypeFacade.m | 28 ++++- .../MKBObjectInvocationHandler.m | 4 +- .../MockingbirdTestsHost/ObjCParameters.swift | 14 +++ .../Framework/ObjectiveCParameterTests.swift | 114 ++++++++++++++++++ 7 files changed, 186 insertions(+), 8 deletions(-) create mode 100644 Sources/MockingbirdTestsHost/ObjCParameters.swift create mode 100644 Tests/MockingbirdTests/Framework/ObjectiveCParameterTests.swift diff --git a/Mockingbird.xcodeproj/project.pbxproj b/Mockingbird.xcodeproj/project.pbxproj index 3708dd5f..53a98076 100644 --- a/Mockingbird.xcodeproj/project.pbxproj +++ b/Mockingbird.xcodeproj/project.pbxproj @@ -155,6 +155,8 @@ 28C8E5DC26A64D6C00C68A1D /* MKBInvocationHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 28C8E5DA26A64D6C00C68A1D /* MKBInvocationHandler.m */; }; 28D08CD62775338100AE7C39 /* OptionGroupArgumentEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D08CD52775338100AE7C39 /* OptionGroupArgumentEncoding.swift */; }; 28D08CCE2774247C00AE7C39 /* OptionalsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D08CCD2774247C00AE7C39 /* OptionalsTests.swift */; }; + 28D08CD0277477F700AE7C39 /* ObjCParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D08CCF277477F700AE7C39 /* ObjCParameters.swift */; }; + 28D08CD327747A4B00AE7C39 /* ObjectiveCParameterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D08CD1277479B600AE7C39 /* ObjectiveCParameterTests.swift */; }; 28DAD96E251BDD66001A0B3F /* Project.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28DAD96D251BDD66001A0B3F /* Project.swift */; }; 28DDDFC126B8571D002556C7 /* DynamicCast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28DDDFC026B8571D002556C7 /* DynamicCast.swift */; }; 8356225C26A94CBE005CD5C5 /* TargetDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8356225B26A94CBE005CD5C5 /* TargetDescriptionTests.swift */; }; @@ -625,6 +627,8 @@ 28C8E5DA26A64D6C00C68A1D /* MKBInvocationHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MKBInvocationHandler.m; sourceTree = ""; }; 28D08CD52775338100AE7C39 /* OptionGroupArgumentEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionGroupArgumentEncoding.swift; sourceTree = ""; }; 28D08CCD2774247C00AE7C39 /* OptionalsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalsTests.swift; sourceTree = ""; }; + 28D08CCF277477F700AE7C39 /* ObjCParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjCParameters.swift; sourceTree = ""; }; + 28D08CD1277479B600AE7C39 /* ObjectiveCParameterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectiveCParameterTests.swift; sourceTree = ""; }; 28DAD96D251BDD66001A0B3F /* Project.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Project.swift; sourceTree = ""; }; 28DDDFC026B8571D002556C7 /* DynamicCast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicCast.swift; sourceTree = ""; }; 8356225B26A94CBE005CD5C5 /* TargetDescriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetDescriptionTests.swift; sourceTree = ""; }; @@ -1335,6 +1339,7 @@ OBJ_169 /* KeywordArgumentNames.swift */, 28A1F3C326ADD57C002F282D /* MinimalTestTypes.swift */, OBJ_196 /* ModuleImportCases.swift */, + 28D08CCF277477F700AE7C39 /* ObjCParameters.swift */, OBJ_198 /* OpaquelyInheritedTypes.swift */, OBJ_199 /* Optionals.swift */, OBJ_200 /* OverloadedMethods.swift */, @@ -1526,6 +1531,7 @@ OBJ_260 /* InitializerTests.swift */, OBJ_261 /* LastSetValueStubTests.swift */, 28719AF426B23AB200C38C2C /* ObjectiveCTests.swift */, + 28D08CD1277479B600AE7C39 /* ObjectiveCParameterTests.swift */, 28D08CCD2774247C00AE7C39 /* OptionalsTests.swift */, OBJ_262 /* OrderedVerificationTests.swift */, OBJ_263 /* OverloadedMethodTests.swift */, @@ -2194,6 +2200,7 @@ OBJ_1042 /* InitializersMockableTests.swift in Sources */, OBJ_1043 /* InoutParametersMockableTests.swift in Sources */, OBJ_1044 /* InoutParametersStubbableTests.swift in Sources */, + 28D08CD327747A4B00AE7C39 /* ObjectiveCParameterTests.swift in Sources */, OBJ_1045 /* KeywordArgumentNamesMockableTests.swift in Sources */, OBJ_1046 /* KeywordArgumentNamesStubbableTests.swift in Sources */, OBJ_1047 /* SubscriptMockableTests.swift in Sources */, @@ -2250,6 +2257,7 @@ OBJ_1124 /* ChildProtocol.swift in Sources */, OBJ_1125 /* ClassInitializers.swift in Sources */, OBJ_1126 /* ClassOnlyProtocols.swift in Sources */, + 28D08CD0277477F700AE7C39 /* ObjCParameters.swift in Sources */, OBJ_1127 /* ClassScopedTypes.swift in Sources */, OBJ_1128 /* ClosureParameters.swift in Sources */, OBJ_1129 /* Collections.swift in Sources */, diff --git a/Sources/MockingbirdFramework/Matching/TypeFacade.swift b/Sources/MockingbirdFramework/Matching/TypeFacade.swift index 605c17e6..94293779 100644 --- a/Sources/MockingbirdFramework/Matching/TypeFacade.swift +++ b/Sources/MockingbirdFramework/Matching/TypeFacade.swift @@ -13,7 +13,6 @@ import Foundation /// guarantees and autocompletion compared to conforming all parameter types to a common protocol. /// /// It goes without saying that this should probably never be done in production. - private class ResolutionContext { enum Constants { static let contextKey = DispatchSpecificKey() @@ -105,6 +104,7 @@ func createTypeFacade(_ value: Any?) -> T { func resolve(_ parameter: () -> T) -> ArgumentMatcher { let resolvedValue = ResolutionContext().resolveTypeFacade(parameter) if let matcher = resolvedValue as? ArgumentMatcher { return matcher } + if let boxedValue = resolveObjCTypeFacade(resolvedValue) { return boxedValue } if let typedValue = resolvedValue as? T { return ArgumentMatcher(typedValue) } return ArgumentMatcher(resolvedValue) } @@ -113,6 +113,7 @@ func resolve(_ parameter: () -> T) -> ArgumentMatcher { func resolve(_ parameter: () -> T) -> ArgumentMatcher { let resolvedValue = ResolutionContext().resolveTypeFacade(parameter) if let matcher = resolvedValue as? ArgumentMatcher { return matcher } + if let boxedValue = resolveObjCTypeFacade(resolvedValue) { return boxedValue } if let typedValue = resolvedValue as? T { return ArgumentMatcher(typedValue) } return ArgumentMatcher(resolvedValue) } @@ -121,3 +122,17 @@ func resolve(_ parameter: () -> T) -> ArgumentMatcher { func resolve(_ parameter: @escaping () -> ArgumentMatcher) -> ArgumentMatcher { return parameter() } + +/// Check whether this is an Objective-C type facade and try to unbox the value. +private func resolveObjCTypeFacade(_ value: Any?) -> ArgumentMatcher? { + let objectValue = value as AnyObject + if objectValue.responds(to: Selector(("mkb_isTypeFacade"))) { + let boxedObject = objectValue.perform(Selector(("mkb_boxedObject")))?.takeRetainedValue() + if let matcher = boxedObject as? ArgumentMatcher { + return ArgumentMatcher(matcher) + } else { + return ArgumentMatcher(boxedObject) + } + } + return nil +} diff --git a/Sources/MockingbirdFramework/Objective-C/Bridge/include/MKBTypeFacade.h b/Sources/MockingbirdFramework/Objective-C/Bridge/include/MKBTypeFacade.h index a0bff956..626e7f6e 100644 --- a/Sources/MockingbirdFramework/Objective-C/Bridge/include/MKBTypeFacade.h +++ b/Sources/MockingbirdFramework/Objective-C/Bridge/include/MKBTypeFacade.h @@ -13,8 +13,13 @@ NS_ASSUME_NONNULL_BEGIN @interface MKBTypeFacade : NSProxy -@property (nonatomic, strong, readonly) id boxedObject; -@property (nonatomic, strong, readonly) MKBConcreteMock *mock; +@property (nonatomic, strong, readonly) id mkb_boxedObject; +@property (nonatomic, strong, readonly) MKBConcreteMock *mkb_mock; + +/// Used to check whether an instance is a type facade. +/// Callers can just check whether the instance responds to the `mkb_isTypeFacade` selector and +/// ignore the value of this property. +@property (nonatomic, assign, readonly) bool mkb_isTypeFacade; - (instancetype)initWithMock:(id)mock object:(id)object NS_DESIGNATED_INITIALIZER; - (T)fixupType; diff --git a/Sources/MockingbirdFramework/Objective-C/Bridge/sources/MKBTypeFacade.m b/Sources/MockingbirdFramework/Objective-C/Bridge/sources/MKBTypeFacade.m index f530247b..b040c1bc 100644 --- a/Sources/MockingbirdFramework/Objective-C/Bridge/sources/MKBTypeFacade.m +++ b/Sources/MockingbirdFramework/Objective-C/Bridge/sources/MKBTypeFacade.m @@ -14,8 +14,9 @@ @implementation MKBTypeFacade - (instancetype)initWithMock:(id)mock object:(id)object { if (self) { - _mock = mock; - _boxedObject = object; + _mkb_mock = mock; + _mkb_boxedObject = object; + _mkb_isTypeFacade = YES; } return self; } @@ -31,11 +32,32 @@ + (id)createFromObject:(id)object return object; } +- (bool)isTypeFacadeSelector:(SEL)aSelector { + return aSelector == @selector(mkb_boxedObject) + || aSelector == @selector(mkb_mock) + || aSelector == @selector(mkb_isTypeFacade); +} + #pragma mark - NSProxy - (void)forwardInvocation:(NSInvocation *)invocation { - [invocation setTarget:self.mock]; + if ([self isTypeFacadeSelector:invocation.selector]) { + [invocation setTarget:self]; + } else { + [invocation setTarget:self.mkb_mock]; + } +} + +- (BOOL)respondsToSelector:(SEL)aSelector +{ + return [(NSObject *)self.mkb_mock respondsToSelector:aSelector] + || [self isTypeFacadeSelector:aSelector]; +} + +- (NSMethodSignature *_Nullable)methodSignatureForSelector:(SEL)aSelector +{ + return [(NSObject *)self.mkb_mock methodSignatureForSelector:aSelector]; } @end diff --git a/Sources/MockingbirdFramework/Objective-C/InvocationHandlers/MKBObjectInvocationHandler.m b/Sources/MockingbirdFramework/Objective-C/InvocationHandlers/MKBObjectInvocationHandler.m index b299b414..4b10bd60 100644 --- a/Sources/MockingbirdFramework/Objective-C/InvocationHandlers/MKBObjectInvocationHandler.m +++ b/Sources/MockingbirdFramework/Objective-C/InvocationHandlers/MKBObjectInvocationHandler.m @@ -29,8 +29,8 @@ - (MKBArgumentMatcher *)serializeArgumentAtIndex:(NSUInteger)index [invocation getArgument:&value atIndex:index]; // Unwrapped boxed types within type facades. - if ([[value class] isSubclassOfClass:[MKBTypeFacade class]]) { - value = ((MKBTypeFacade *)value).boxedObject; + if ([value respondsToSelector:@selector(mkb_isTypeFacade)]) { + value = ((MKBTypeFacade *)value).mkb_boxedObject; } // Use argument matchers directly. diff --git a/Sources/MockingbirdTestsHost/ObjCParameters.swift b/Sources/MockingbirdTestsHost/ObjCParameters.swift new file mode 100644 index 00000000..abf3a1f4 --- /dev/null +++ b/Sources/MockingbirdTestsHost/ObjCParameters.swift @@ -0,0 +1,14 @@ +// +// ObjCParameters.swift +// MockingbirdTestsHost +// +// Created by typealias on 12/22/21. +// + +import AppKit +import Foundation + +protocol ObjCParameters { + func method(value: NSViewController) -> Bool + func method(optionalValue: NSViewController?) -> Bool +} diff --git a/Tests/MockingbirdTests/Framework/ObjectiveCParameterTests.swift b/Tests/MockingbirdTests/Framework/ObjectiveCParameterTests.swift new file mode 100644 index 00000000..2e8257f9 --- /dev/null +++ b/Tests/MockingbirdTests/Framework/ObjectiveCParameterTests.swift @@ -0,0 +1,114 @@ +// +// ObjectiveCParameterTests.swift +// MockingbirdTestsHost +// +// Created by typealias on 12/22/21. +// + +import Mockingbird +@testable import MockingbirdTestsHost +import XCTest + +class ObjectiveCParameterTests: BaseTestCase { + + var parametersMock: ObjCParametersMock! + var parametersInstance: ObjCParameters { parametersMock } + + override func setUpWithError() throws { + self.parametersMock = mock(ObjCParameters.self) + } + + func testExactParameterMatching() { + let instance = NSViewController() + given(parametersMock.method(value: instance)).willReturn(true) + XCTAssertTrue(parametersInstance.method(value: instance)) + verify(parametersMock.method(value: instance)).wasCalled() + } + func testExactParameterMatching_stubbingOperator() { + let instance = NSViewController() + given(parametersMock.method(value: instance)) ~> true + XCTAssertTrue(parametersInstance.method(value: instance)) + verify(parametersMock.method(value: instance)).wasCalled() + } + + func testExactNilParameterMatching() { + given(parametersMock.method(optionalValue: nil)).willReturn(true) + XCTAssertTrue(parametersInstance.method(optionalValue: nil)) + verify(parametersMock.method(optionalValue: nil)).wasCalled() + } + func testExactNilParameterMatching_stubbingOperator() { + given(parametersMock.method(optionalValue: nil)) ~> true + XCTAssertTrue(parametersInstance.method(optionalValue: nil)) + verify(parametersMock.method(optionalValue: nil)).wasCalled() + } + + func testWildcardParameterMatchingAny() { + given(parametersMock.method(value: any())).willReturn(true) + XCTAssertTrue(parametersInstance.method(value: NSViewController())) + verify(parametersMock.method(value: any())).wasCalled() + } + func testWildcardParameterMatchingAny_stubbingOperator() { + given(parametersMock.method(value: any())) ~> true + XCTAssertTrue(parametersInstance.method(value: NSViewController())) + verify(parametersMock.method(value: any())).wasCalled() + } + + func testWildcardOptionalParameterMatchingAny() { + given(parametersMock.method(optionalValue: any())).willReturn(true) + XCTAssertTrue(parametersInstance.method(optionalValue: nil)) + verify(parametersMock.method(optionalValue: any())).wasCalled() + } + func testWildcardOptionalParameterMatchingAny_stubbingOperator() { + given(parametersMock.method(optionalValue: any())) ~> true + XCTAssertTrue(parametersInstance.method(optionalValue: nil)) + verify(parametersMock.method(optionalValue: any())).wasCalled() + } + + func testWildcardParameterMatchingAnyWhere() { + let instance = NSViewController() + given(parametersMock.method(value: any(where: { $0 === instance }))).willReturn(true) + XCTAssertTrue(parametersInstance.method(value: instance)) + verify(parametersMock.method(value: any(where: { $0 === instance }))).wasCalled() + } + func testWildcardParameterMatchingAnyWhere_stubbingOperator() { + let instance = NSViewController() + given(parametersMock.method(value: any(where: { $0 === instance }))) ~> true + XCTAssertTrue(parametersInstance.method(value: instance)) + verify(parametersMock.method(value: any(where: { $0 === instance }))).wasCalled() + } + + func testWildcardParameterMatchingNotNil() { + given(parametersMock.method(value: notNil())).willReturn(true) + XCTAssertTrue(parametersInstance.method(value: NSViewController())) + verify(parametersMock.method(value: notNil())).wasCalled() + } + func testWildcardParameterMatchingNotNil_stubbingOperator() { + given(parametersMock.method(value: notNil())) ~> true + XCTAssertTrue(parametersInstance.method(value: NSViewController())) + verify(parametersMock.method(value: notNil())).wasCalled() + } + + func testWildcardOptionalParameterMatchingNotNil() { + given(parametersMock.method(optionalValue: notNil())).willReturn(true) + XCTAssertTrue(parametersInstance.method(optionalValue: NSViewController())) + verify(parametersMock.method(optionalValue: notNil())).wasCalled() + } + func testWildcardOptionalParameterMatchingNotNil_stubbingOperator() { + given(parametersMock.method(optionalValue: notNil())) ~> true + XCTAssertTrue(parametersInstance.method(optionalValue: NSViewController())) + verify(parametersMock.method(optionalValue: notNil())).wasCalled() + } + + func testWildcardOptionalParameterDoesNotMatchNil() { + shouldFail { + given(self.parametersMock.method(optionalValue: notNil())).willReturn(true) + XCTAssertTrue(self.parametersInstance.method(optionalValue: nil)) + } + } + func testWildcardOptionalParameterDoesNotMatchNil_stubbingOperator() { + shouldFail { + given(self.parametersMock.method(optionalValue: notNil())) ~> true + XCTAssertTrue(self.parametersInstance.method(optionalValue: nil)) + } + } +}