diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6dad9e07..ce451642 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,8 @@ name: CI on: [push, pull_request] +env: + DEVELOPER_DIR: /Applications/Xcode_13.2.app/Contents/Developer jobs: test-e2e: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3c6ef9ce..d5f3868b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,8 @@ on: - 'release-*' tags: - '*' +env: + DEVELOPER_DIR: /Applications/Xcode_13.2.app/Contents/Developer jobs: build-signed-artifacts: diff --git a/Mockingbird.xcodeproj/project.pbxproj b/Mockingbird.xcodeproj/project.pbxproj index 9c0b5790..e230b5c7 100644 --- a/Mockingbird.xcodeproj/project.pbxproj +++ b/Mockingbird.xcodeproj/project.pbxproj @@ -199,6 +199,8 @@ 28FB7EC62789C14000125FDA /* Data+SHA1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285C8EC4277DE42400DE525A /* Data+SHA1.swift */; }; 28FB7EC82789D0E000125FDA /* Path+Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FB7EC72789D0E000125FDA /* Path+Backup.swift */; }; 28FB7ECA2789D87800125FDA /* Git.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FB7EC92789D87800125FDA /* Git.swift */; }; + 5E45ADF4279DA63D004AB972 /* StubbingAsyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E45ADF3279DA63D004AB972 /* StubbingAsyncTests.swift */; }; + 5E45ADF7279DB9A9004AB972 /* AsyncMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E45ADF5279DB949004AB972 /* AsyncMethods.swift */; }; 8356225C26A94CBE005CD5C5 /* TargetDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8356225B26A94CBE005CD5C5 /* TargetDescriptionTests.swift */; }; D314ED7F24CE1C10000CC23D /* GenericsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D314ED7E24CE1C10000CC23D /* GenericsTests.swift */; }; D3643B6C247B78A5002DF069 /* Function.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3643B6B247B78A4002DF069 /* Function.swift */; }; @@ -719,6 +721,8 @@ 28FB7EBF2789B9B000125FDA /* MockingbirdCommon.xcscheme */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = MockingbirdCommon.xcscheme; sourceTree = ""; }; 28FB7EC72789D0E000125FDA /* Path+Backup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Path+Backup.swift"; sourceTree = ""; }; 28FB7EC92789D87800125FDA /* Git.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Git.swift; sourceTree = ""; }; + 5E45ADF3279DA63D004AB972 /* StubbingAsyncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubbingAsyncTests.swift; sourceTree = ""; }; + 5E45ADF5279DB949004AB972 /* AsyncMethods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncMethods.swift; sourceTree = ""; }; 8356225B26A94CBE005CD5C5 /* TargetDescriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetDescriptionTests.swift; sourceTree = ""; }; 942B00CDCB48ADC877A01AEE /* MockingbirdTestsHostMocks.generated.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; name = MockingbirdTestsHostMocks.generated.swift; path = Tests/MockingbirdTests/Mocks/MockingbirdTestsHostMocks.generated.swift; sourceTree = ""; }; D314ED7E24CE1C10000CC23D /* GenericsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericsTests.swift; sourceTree = ""; }; @@ -1526,6 +1530,7 @@ 281B623A251DC6B10084EBED /* Shadowed */, OBJ_141 /* MockingbirdTestsHost.h */, OBJ_143 /* ArgumentMatching.swift */, + 5E45ADF5279DB949004AB972 /* AsyncMethods.swift */, OBJ_144 /* Child.swift */, OBJ_145 /* ChildProtocol.swift */, OBJ_146 /* ClassInitializers.swift */, @@ -1745,6 +1750,7 @@ OBJ_263 /* OverloadedMethodTests.swift */, 28A1F3BF26ADA2A8002F282D /* PartialMockTests.swift */, OBJ_264 /* SequentialValueStubbingTests.swift */, + 5E45ADF3279DA63D004AB972 /* StubbingAsyncTests.swift */, OBJ_265 /* StubbingTests.swift */, 28A1F3BB26AD9E3B002F282D /* StubbingThrowingErrorsTests.swift */, 28A1F3BD26AD9EAB002F282D /* StubbingInoutTests.swift */, @@ -2507,6 +2513,7 @@ OBJ_1032 /* ExtensionsMockableTests.swift in Sources */, OBJ_1033 /* ExtensionsStubbableTests.swift in Sources */, OBJ_1034 /* ExternalModuleClassScopedTypesMockableTests.swift in Sources */, + 5E45ADF4279DA63D004AB972 /* StubbingAsyncTests.swift in Sources */, OBJ_1035 /* ExternalModuleClassScopedTypesStubbableTests.swift in Sources */, 28D08CCE2774247C00AE7C39 /* OptionalsTests.swift in Sources */, 287C4F5426A36FFF00A7E0D9 /* DynamicSwiftTests.swift in Sources */, @@ -2591,6 +2598,7 @@ OBJ_1137 /* ExternalModuleClassScopedTypes.swift in Sources */, OBJ_1138 /* ExternalModuleImplicitlyImportedTypes.swift in Sources */, OBJ_1139 /* ExternalModuleTypealiasing.swift in Sources */, + 5E45ADF7279DB9A9004AB972 /* AsyncMethods.swift in Sources */, OBJ_1140 /* ExternalModuleTypes.swift in Sources */, OBJ_1141 /* FakeableTypes.swift in Sources */, OBJ_1142 /* Generics.swift in Sources */, @@ -3654,7 +3662,7 @@ repositoryURL = "https://github.com/jpsim/SourceKitten.git"; requirement = { kind = exactVersion; - version = 0.30.0; + version = 0.31.1; }; }; 287F852625194F2A007D135D /* XCRemoteSwiftPackageReference "ZIPFoundation" */ = { diff --git a/Mockingbird.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mockingbird.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a8430829..40a892e2 100644 --- a/Mockingbird.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mockingbird.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -10,24 +10,6 @@ "version": "4.6.1" } }, - { - "package": "Commandant", - "repositoryURL": "https://github.com/Carthage/Commandant.git", - "state": { - "branch": null, - "revision": "ab68611013dec67413628ac87c1f29e8427bc8e4", - "version": "0.17.0" - } - }, - { - "package": "Nimble", - "repositoryURL": "https://github.com/Quick/Nimble.git", - "state": { - "branch": null, - "revision": "7a46a5fc86cb917f69e3daf79fcb045283d8f008", - "version": "8.1.2" - } - }, { "package": "PathKit", "repositoryURL": "https://github.com/kylef/PathKit", @@ -37,22 +19,13 @@ "version": "1.0.1" } }, - { - "package": "Quick", - "repositoryURL": "https://github.com/Quick/Quick.git", - "state": { - "branch": null, - "revision": "09b3becb37cb2163919a3842a4c5fa6ec7130792", - "version": "2.2.1" - } - }, { "package": "SourceKitten", "repositoryURL": "https://github.com/jpsim/SourceKitten.git", "state": { "branch": null, - "revision": "b7a7df0d25981998bb9f4770ff8faf7a28a6e649", - "version": "0.30.0" + "revision": "558628392eb31d37cb251cfe626c53eafd330df6", + "version": "0.31.1" } }, { diff --git a/Sources/MockingbirdFramework/Mocking/Declaration.swift b/Sources/MockingbirdFramework/Mocking/Declaration.swift index 45d28cbc..2dd67e3e 100644 --- a/Sources/MockingbirdFramework/Mocking/Declaration.swift +++ b/Sources/MockingbirdFramework/Mocking/Declaration.swift @@ -15,8 +15,12 @@ public class PropertySetterDeclaration: VariableDeclaration {} /// Mockable function declarations. public class FunctionDeclaration: Declaration {} +/// Mockable async function declarations. +public class AsyncFunctionDeclaration: FunctionDeclaration {} /// Mockable throwing function declarations. public class ThrowingFunctionDeclaration: FunctionDeclaration {} +/// Mockable throwing async function declarations. +public class ThrowingAsyncFunctionDeclaration: AsyncFunctionDeclaration {} /// Mockable subscript declarations. public class SubscriptDeclaration: Declaration {} diff --git a/Sources/MockingbirdFramework/Mocking/MockingContext.swift b/Sources/MockingbirdFramework/Mocking/MockingContext.swift index e6befbb1..c608bb52 100644 --- a/Sources/MockingbirdFramework/Mocking/MockingContext.swift +++ b/Sources/MockingbirdFramework/Mocking/MockingContext.swift @@ -20,6 +20,15 @@ import Foundation return try thunk(invocation) } + /// Invoke an async thunk that can throw. + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func didInvoke(_ invocation: I, + evaluating thunk: (I) async throws -> T) async rethrows -> T { + // Ensures that the thunk is evaluated prior to recording the invocation. + defer { didInvoke(invocation) } + return try await thunk(invocation) + } + /// Invoke a non-throwing thunk. func didInvoke(_ invocation: I, evaluating thunk: (I) -> T) -> T { // Ensures that the thunk is evaluated prior to recording the invocation. @@ -27,6 +36,14 @@ import Foundation return thunk(invocation) } + /// Invoke an async non-throwing thunk. + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func didInvoke(_ invocation: I, evaluating thunk: (I) async -> T) async -> T { + // Ensures that the thunk is evaluated prior to recording the invocation. + defer { didInvoke(invocation) } + return await thunk(invocation) + } + /// Invoke a thunk from Objective-C. @objc public func objcDidInvoke(_ invocation: ObjCInvocation, evaluating thunk: (ObjCInvocation) -> Any?) -> Any? { diff --git a/Sources/MockingbirdFramework/Stubbing/Stubbing.swift b/Sources/MockingbirdFramework/Stubbing/Stubbing.swift index 63de79bf..cbd8726b 100644 --- a/Sources/MockingbirdFramework/Stubbing/Stubbing.swift +++ b/Sources/MockingbirdFramework/Stubbing/Stubbing.swift @@ -162,6 +162,14 @@ public class StubbingManager Self { - return addImplementation({ return value }) + if isAsync { + if isThrowing { + return addImplementation({ () async throws in value }) + } else { + return addImplementation({ () async in value }) + } + } else { + return addImplementation({ value }) + } } /// Stub a mocked method or property with an implementation provider. @@ -481,7 +497,15 @@ public func ~> ( manager: StaticStubbingManager, implementation: @escaping @autoclosure () -> ReturnType ) { - manager.addImplementation(implementation) + if manager.isAsync { + if manager.isThrowing { + manager.addImplementation({ () async throws in implementation() }) + } else { + manager.addImplementation({ () async in implementation() }) + } + } else { + manager.addImplementation({ implementation() }) + } } /// Stub a mocked method or property with a closure implementation. diff --git a/Sources/MockingbirdGenerator/Generator/Templates/InitializerMethodTemplate.swift b/Sources/MockingbirdGenerator/Generator/Templates/InitializerMethodTemplate.swift index 1934a5a0..e1f77041 100644 --- a/Sources/MockingbirdGenerator/Generator/Templates/InitializerMethodTemplate.swift +++ b/Sources/MockingbirdGenerator/Generator/Templates/InitializerMethodTemplate.swift @@ -19,6 +19,7 @@ class InitializerMethodTemplate: MethodTemplate { let mock: \(scopedName)\(failable) = \(FunctionCallTemplate( name: scopedName, parameters: method.parameters, + isAsync: method.isAsync, isThrowing: method.isThrowing)) mock\(failable).mockingbirdContext.sourceLocation = SourceLocation(__file, __line) return mock @@ -41,6 +42,7 @@ class InitializerMethodTemplate: MethodTemplate { let body = !context.shouldGenerateThunks ? MockableTypeTemplate.Constants.thunkStub : FunctionCallTemplate(name: "super.init", parameters: method.parameters, + isAsync: method.isAsync, isThrowing: method.attributes.contains(.throws)).render() return String(lines: [ trivia, diff --git a/Sources/MockingbirdGenerator/Generator/Templates/MethodTemplate.swift b/Sources/MockingbirdGenerator/Generator/Templates/MethodTemplate.swift index b7b0283f..317277db 100644 --- a/Sources/MockingbirdGenerator/Generator/Templates/MethodTemplate.swift +++ b/Sources/MockingbirdGenerator/Generator/Templates/MethodTemplate.swift @@ -57,6 +57,7 @@ class MethodTemplate: Template { longSignature: longSignature, returnType: matchableReturnType, isBridged: false, + isAsync: method.isAsync, isThrowing: method.isThrowing, isStatic: method.kind.typeScope.isStatic, callMember: { scope in @@ -65,6 +66,7 @@ class MethodTemplate: Template { return FunctionCallTemplate( name: scopedName, arguments: self.invocationArguments, + isAsync: self.method.isAsync, isThrowing: self.method.isThrowing).render() } @@ -77,6 +79,7 @@ class MethodTemplate: Template { return FunctionCallTemplate( name: name.render(), unlabeledArguments: self.invocationArguments.map({ $0.parameterName }), + isAsync: self.method.isAsync, isThrowing: self.method.isThrowing).render() }, invocationArguments: invocationArguments).render() @@ -111,7 +114,7 @@ class MethodTemplate: Template { let genericTypes = [declarationTypeForMocking, invocationType, matchableReturnType] let returnType = "Mockingbird.Mockable<\(separated: genericTypes)>" - let declaration = "public \(regularModifiers)func \(fullNameForMatching) -> \(returnType)" + let declaration = "public \(regularModifiers)func \(fullNameForMatching)\(returnTypeAttributesForMockableDeclaration) -> \(returnType)" let genericConstraints = method.whereClauses.map({ context.specializeTypeName("\($0)") }) let body = !context.shouldGenerateThunks ? MockableTypeTemplate.Constants.thunkStub : """ @@ -336,20 +339,37 @@ class MethodTemplate: Template { } lazy var returnTypeAttributesForMocking: String = { - if method.attributes.contains(.rethrows) { return " rethrows" } - if method.attributes.contains(.throws) { return " throws" } - return "" + var attributes = "" + if method.attributes.contains(.async) { attributes += " async" } + if method.attributes.contains(.rethrows) { attributes += " rethrows" } + else if method.attributes.contains(.throws) { attributes += " throws" } + return attributes + }() + + lazy var returnTypeAttributesForMockableDeclaration: String = { + return method.isAsync ? " async" : "" }() lazy var returnTypeAttributesForMatching: String = { - return method.isThrowing ? "throws " : "" + var returnType = "" + if method.isAsync { returnType += "async " } + if method.isThrowing { returnType += "throws " } + return returnType }() lazy var declarationTypeForMocking: String = { if method.attributes.contains(.throws) { - return "\(Declaration.throwingFunctionDeclaration)" + if method.isAsync { + return "\(Declaration.throwingAsyncFunctionDeclaration)" + } else { + return "\(Declaration.throwingFunctionDeclaration)" + } } else { - return "\(Declaration.functionDeclaration)" + if method.isAsync { + return "\(Declaration.asyncFunctionDeclaration)" + } else { + return "\(Declaration.functionDeclaration)" + } } }() @@ -377,7 +397,8 @@ class MethodTemplate: Template { /// Original function signature for casting to a matchable signature (variadics support). lazy var originalSignature: String = { - let modifiers = method.isThrowing ? " throws" : "" + var modifiers = method.isAsync ? " async" : "" + modifiers += method.isThrowing ? " throws" : "" let parameterTypes = method.parameters.map({ $0.matchableTypeName(context: self, bridgeVariadics: false) }) @@ -386,13 +407,15 @@ class MethodTemplate: Template { /// General function signature for matching. lazy var longSignature: String = { - let modifiers = method.isThrowing ? " throws" : "" + var modifiers = method.isAsync ? " async" : "" + modifiers += method.isThrowing ? " throws" : "" return "(\(separated: matchableParameterTypes))\(modifiers) -> \(matchableReturnType)" }() /// Convenience function signature for matching without any arguments. lazy var shortSignature: String = { - let modifiers = method.isThrowing ? " throws" : "" + var modifiers = method.isAsync ? " async" : "" + modifiers += method.isThrowing ? " throws" : "" return "()\(modifiers) -> \(matchableReturnType)" }() @@ -418,6 +441,10 @@ class MethodTemplate: Template { } extension Method { + var isAsync: Bool { + return attributes.contains(.async) + } + var isThrowing: Bool { return attributes.contains(.throws) || attributes.contains(.rethrows) } diff --git a/Sources/MockingbirdGenerator/Generator/Templates/MockableTypeTemplate.swift b/Sources/MockingbirdGenerator/Generator/Templates/MockableTypeTemplate.swift index 2ee768f6..dc1a4042 100644 --- a/Sources/MockingbirdGenerator/Generator/Templates/MockableTypeTemplate.swift +++ b/Sources/MockingbirdGenerator/Generator/Templates/MockableTypeTemplate.swift @@ -5,7 +5,9 @@ import MockingbirdCommon enum Declaration: String, CustomStringConvertible { case functionDeclaration = "Mockingbird.FunctionDeclaration" + case asyncFunctionDeclaration = "Mockingbird.AsyncFunctionDeclaration" case throwingFunctionDeclaration = "Mockingbird.ThrowingFunctionDeclaration" + case throwingAsyncFunctionDeclaration = "Mockingbird.ThrowingAsyncFunctionDeclaration" case propertyGetterDeclaration = "Mockingbird.PropertyGetterDeclaration" case propertySetterDeclaration = "Mockingbird.PropertySetterDeclaration" diff --git a/Sources/MockingbirdGenerator/Generator/Templates/Primitives/ClosureTemplate.swift b/Sources/MockingbirdGenerator/Generator/Templates/Primitives/ClosureTemplate.swift index 3969bd90..9d2a3077 100644 --- a/Sources/MockingbirdGenerator/Generator/Templates/Primitives/ClosureTemplate.swift +++ b/Sources/MockingbirdGenerator/Generator/Templates/Primitives/ClosureTemplate.swift @@ -3,21 +3,25 @@ import Foundation struct ClosureTemplate: Template { let parameters: [String] let returnType: String + let isAsync: Bool let isThrowing: Bool let body: String init(parameters: [(argumentLabel: String, type: String)] = [], returnType: String = "Void", + isAsync: Bool = false, isThrowing: Bool = false, body: String) { self.parameters = parameters.map({ $0.argumentLabel + ": " + $0.type }) self.returnType = returnType + self.isAsync = isAsync self.isThrowing = isThrowing self.body = body } func render() -> String { - let modifiers = isThrowing ? " throws" : "" + var modifiers = isAsync ? " async" : "" + modifiers += isThrowing ? " throws" : "" let signature = parameters.isEmpty && returnType == "Void" ? "" : "(\(separated: parameters))\(modifiers) -> \(returnType) in " return BlockTemplate(body: "\(signature)\(body)", multiline: false).render() diff --git a/Sources/MockingbirdGenerator/Generator/Templates/Primitives/FunctionCallTemplate.swift b/Sources/MockingbirdGenerator/Generator/Templates/Primitives/FunctionCallTemplate.swift index 7150e7e4..e78438bc 100644 --- a/Sources/MockingbirdGenerator/Generator/Templates/Primitives/FunctionCallTemplate.swift +++ b/Sources/MockingbirdGenerator/Generator/Templates/Primitives/FunctionCallTemplate.swift @@ -3,35 +3,40 @@ import Foundation struct FunctionCallTemplate: Template { let name: String let arguments: [String] + let isAsync: Bool let isThrowing: Bool init(name: String, arguments: [(argumentLabel: String?, parameterName: String)], + isAsync: Bool = false, isThrowing: Bool = false) { self.name = name self.arguments = arguments.map({ guard let argumentLabel = $0.argumentLabel else { return $0.parameterName } return "\(argumentLabel): \($0.parameterName)" }) + self.isAsync = isAsync self.isThrowing = isThrowing } - init(name: String, unlabeledArguments: [String] = [], isThrowing: Bool = false) { + init(name: String, unlabeledArguments: [String] = [], isAsync: Bool = false, isThrowing: Bool = false) { self.name = name self.arguments = unlabeledArguments + self.isAsync = isAsync self.isThrowing = isThrowing } - init(name: String, parameters: [MethodParameter], isThrowing: Bool = false) { + init(name: String, parameters: [MethodParameter], isAsync: Bool = false, isThrowing: Bool = false) { self.name = name self.arguments = parameters.map({ parameter -> String in guard let label = parameter.argumentLabel else { return parameter.name.backtickWrapped } return "\(label): \(backticked: parameter.name)" }) + self.isAsync = isAsync self.isThrowing = isThrowing } func render() -> String { - return "\(isThrowing ? "try " : "")\(name)(\(separated: arguments))" + return "\(isThrowing ? "try " : "")\(isAsync ? "await " : "")\(name)(\(separated: arguments))" } } diff --git a/Sources/MockingbirdGenerator/Generator/Templates/SubscriptMethodTemplate.swift b/Sources/MockingbirdGenerator/Generator/Templates/SubscriptMethodTemplate.swift index fd36f317..77057fc1 100644 --- a/Sources/MockingbirdGenerator/Generator/Templates/SubscriptMethodTemplate.swift +++ b/Sources/MockingbirdGenerator/Generator/Templates/SubscriptMethodTemplate.swift @@ -43,6 +43,7 @@ class SubscriptMethodTemplate: MethodTemplate { longSignature: longSignature, returnType: matchableReturnType, isBridged: true, + isAsync: method.isAsync, isThrowing: method.isThrowing, isStatic: method.kind.typeScope.isStatic, callMember: { scope in @@ -67,6 +68,7 @@ class SubscriptMethodTemplate: MethodTemplate { longSignature: setterLongSignature, returnType: "Void", isBridged: true, + isAsync: method.isAsync, isThrowing: method.isThrowing, isStatic: method.kind.typeScope.isStatic, callMember: { scope in diff --git a/Sources/MockingbirdGenerator/Generator/Templates/ThunkTemplate.swift b/Sources/MockingbirdGenerator/Generator/Templates/ThunkTemplate.swift index f683172d..208f21b4 100644 --- a/Sources/MockingbirdGenerator/Generator/Templates/ThunkTemplate.swift +++ b/Sources/MockingbirdGenerator/Generator/Templates/ThunkTemplate.swift @@ -7,6 +7,7 @@ class ThunkTemplate: Template { let longSignature: String let returnType: String let isBridged: Bool + let isAsync: Bool let isThrowing: Bool let isStatic: Bool let callMember: (_ scope: Scope) -> String @@ -29,6 +30,7 @@ class ThunkTemplate: Template { longSignature: String, returnType: String, isBridged: Bool, + isAsync: Bool, isThrowing: Bool, isStatic: Bool, callMember: @escaping (_ scope: Scope) -> String, @@ -39,6 +41,7 @@ class ThunkTemplate: Template { self.longSignature = longSignature self.returnType = returnType self.isBridged = isBridged + self.isAsync = isAsync self.isThrowing = isThrowing self.isStatic = isStatic self.callMember = callMember @@ -52,14 +55,15 @@ class ThunkTemplate: Template { body: """ return \(FunctionCallTemplate(name: "mkbImpl", unlabeledArguments: unlabledArguments, + isAsync: isAsync, isThrowing: isThrowing)) - """) + """).render() let callConvenience: String = { guard let shortSignature = shortSignature else { return "" } return IfStatementTemplate( condition: "let mkbImpl = mkbImpl as? \(shortSignature)", body: """ - return \(FunctionCallTemplate(name: "mkbImpl", isThrowing: isThrowing)) + return \(FunctionCallTemplate(name: "mkbImpl", isAsync: isAsync, isThrowing: isThrowing)) """).render() }() @@ -77,6 +81,7 @@ class ThunkTemplate: Template { FunctionCallTemplate( name: "mkbImpl", unlabeledArguments: unlabledArguments.map({ $0 + " as Any?" }), + isAsync: isAsync, isThrowing: isThrowing).render() ])) as \(returnType) """).render() @@ -89,7 +94,7 @@ class ThunkTemplate: Template { return \(FunctionCallTemplate( name: "Mockingbird.dynamicCast", unlabeledArguments: [ - FunctionCallTemplate(name: "mkbImpl", isThrowing: isThrowing).render() + FunctionCallTemplate(name: "mkbImpl", isAsync: isAsync, isThrowing: isThrowing).render() ])) as \(returnType) """).render() }() @@ -98,6 +103,7 @@ class ThunkTemplate: Template { let supertype = isStatic ? "MockingbirdSupertype.Type" : "MockingbirdSupertype" let didInvoke = FunctionCallTemplate(name: "\(context).mocking.didInvoke", unlabeledArguments: [invocation], + isAsync: isAsync, isThrowing: isThrowing) let isSubclass = mockableType.kind != .class @@ -112,7 +118,7 @@ class ThunkTemplate: Template { let mkbImpl = \(FunctionCallTemplate(name: "\(context).stubbing.implementation", arguments: [("for", "$0")])) \(String(lines: [ - callDefault.render(), + callDefault, callConvenience, callBridgedDefault, callBridgedConvenience, diff --git a/Sources/MockingbirdGenerator/Generator/Templates/VariableTemplate.swift b/Sources/MockingbirdGenerator/Generator/Templates/VariableTemplate.swift index 268fb003..9021f6c8 100644 --- a/Sources/MockingbirdGenerator/Generator/Templates/VariableTemplate.swift +++ b/Sources/MockingbirdGenerator/Generator/Templates/VariableTemplate.swift @@ -71,6 +71,7 @@ class VariableTemplate: Template { longSignature: "() -> \(matchableType)", returnType: matchableType, isBridged: true, + isAsync: false, isThrowing: false, isStatic: variable.kind.typeScope.isStatic, callMember: { scope in @@ -86,6 +87,7 @@ class VariableTemplate: Template { longSignature: "\(parenthetical: matchableType) -> Void", returnType: "Void", isBridged: true, + isAsync: false, isThrowing: false, isStatic: variable.kind.typeScope.isStatic, callMember: { scope in diff --git a/Sources/MockingbirdGenerator/Parser/Models/Function.swift b/Sources/MockingbirdGenerator/Parser/Models/Function.swift index 99421fbf..1ce45e94 100644 --- a/Sources/MockingbirdGenerator/Parser/Models/Function.swift +++ b/Sources/MockingbirdGenerator/Parser/Models/Function.swift @@ -129,24 +129,28 @@ struct Function: CustomStringConvertible, CustomDebugStringConvertible, Serializ let parameters: [Parameter] let returnType: DeclaredType + let isAsync: Bool let isThrowing: Bool var description: String { + let `async` = isAsync ? "async " : "" let throwing = isThrowing ? "throws " : "" - return "(\(parameters.map({ "\($0)" }).joined(separator: ", "))) \(throwing)-> \(returnType)" + return "(\(parameters.map({ "\($0)" }).joined(separator: ", "))) \(`async`)\(throwing)-> \(returnType)" } var debugDescription: String { var description: String { + let `async` = isAsync ? "async " : "" let throwing = isThrowing ? "throws " : "" - return "(\(parameters.map({ String(reflecting: $0) }).joined(separator: ", "))) \(throwing)-> \(String(reflecting: returnType))" + return "(\(parameters.map({ String(reflecting: $0) }).joined(separator: ", "))) \(`async`)\(throwing)-> \(String(reflecting: returnType))" } return "Function(\(description))" } func serialize(with request: SerializationRequest) -> String { + let `async` = isAsync ? "async " : "" let throwing = isThrowing ? "throws " : "" - return "(\(parameters.map({ $0.serialize(with: request) }).joined(separator: ", "))) \(throwing)-> \(returnType.serialize(with: request))" + return "(\(parameters.map({ $0.serialize(with: request) }).joined(separator: ", "))) \(`async`)\(throwing)-> \(returnType.serialize(with: request))" } init?(from serialized: Substring) { @@ -166,6 +170,8 @@ struct Function: CustomStringConvertible, CustomDebugStringConvertible, Serializ let returnAttributes = serialized[parametersEndIndex.. Bool + + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func asyncMethod(parameter: String) async -> Int + + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func asyncThrowingMethod() async throws -> Int + + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func asyncClosureMethod(block: () async -> Bool) async + + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func asyncClosureThrowingMethod(block: () async throws -> Bool) async throws -> Bool +} diff --git a/Tests/MockingbirdTests/Framework/StubbingAsyncTests.swift b/Tests/MockingbirdTests/Framework/StubbingAsyncTests.swift new file mode 100644 index 00000000..58e2e1c4 --- /dev/null +++ b/Tests/MockingbirdTests/Framework/StubbingAsyncTests.swift @@ -0,0 +1,111 @@ +import Foundation +import Mockingbird +@testable import MockingbirdTestsHost +import XCTest + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension XCTest { + func XCTAssertThrowsAsyncError( + _ expression: @autoclosure () async throws -> T, + _ message: @autoclosure () -> String = "XCTAssertThrowsAsyncError failed: did not throw an error", + file: StaticString = #filePath, + line: UInt = #line, + _ errorHandler: (_ error: Error) -> Void = { _ in } + ) async { + do { + _ = try await expression() + XCTFail(message(), file: file, line: line) + } catch { + errorHandler(error) + } + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +class StubbingAsyncTests: BaseTestCase { + + struct FakeError: Error {} + + var asyncProtocol: AsyncProtocolMock! + var asyncProtocolInstance: AsyncProtocol { asyncProtocol } + + override func setUp() { + asyncProtocol = mock(AsyncProtocol.self) + } + + func testStubAsyncMethodVoid() async { + given(await asyncProtocol.asyncMethodVoid()).willReturn() + await asyncProtocolInstance.asyncMethodVoid() + verify(await asyncProtocol.asyncMethodVoid()).wasCalled() + } + + func testStubAsyncMethod_returnsValue() async { + given(await asyncProtocol.asyncMethod()) ~> true + + let result: Bool = await asyncProtocolInstance.asyncMethod() + + XCTAssertEqual(result, true) + verify(await asyncProtocol.asyncMethod()).wasCalled() + } + + func testStubAsyncMethodWithParameter_returnsValue() async { + given(await asyncProtocol.asyncMethod(parameter: any())) ~> 2 + + let result: Int = await asyncProtocolInstance.asyncMethod(parameter: "parameter") + + XCTAssertEqual(result, 2) + verify(await asyncProtocol.asyncMethod(parameter: "parameter")).wasCalled() + } + + func testStubAsyncThrowingMethod_returnsValue() async throws { + given(await asyncProtocol.asyncThrowingMethod()) ~> 1 + + let result: Int = try await asyncProtocolInstance.asyncThrowingMethod() + + XCTAssertEqual(result, 1) + verify(await asyncProtocol.asyncThrowingMethod()).wasCalled() + } + + func testStubAsyncThrowingMethod_throwsError() async throws { + given(await asyncProtocol.asyncThrowingMethod()) ~> { () throws -> Int in throw FakeError() } + await XCTAssertThrowsAsyncError(try await asyncProtocolInstance.asyncThrowingMethod()) + verify(await asyncProtocol.asyncThrowingMethod()).wasCalled() + } + + func testStubAsyncClosureMethod() async throws { + given(await asyncProtocol.asyncClosureMethod(block: any())).willReturn() + await asyncProtocolInstance.asyncClosureMethod(block: { true }) + verify(await asyncProtocol.asyncClosureMethod(block: any())).wasCalled() + } + + func testStubAsyncClosureThrowingMethod_returnsValue() async throws { + given(await asyncProtocol.asyncClosureThrowingMethod(block: any())) ~> true + + let result: Bool = try await asyncProtocolInstance.asyncClosureThrowingMethod(block: { false }) + + XCTAssertTrue(result) + verify(await asyncProtocol.asyncClosureThrowingMethod(block: any())).wasCalled() + } + + func testStubAsyncClosureThrowingMethod_throwsError() async throws { + given(await asyncProtocol.asyncClosureThrowingMethod(block: any())) ~> { _ in throw FakeError() } + await XCTAssertThrowsAsyncError(try await asyncProtocolInstance.asyncClosureThrowingMethod(block: { true })) + verify(await asyncProtocol.asyncClosureThrowingMethod(block: any())).wasCalled() + } + + func testStubAsyncClosureThrowingMethod_returnsValueFromBlock() async throws { + given(await asyncProtocol.asyncClosureThrowingMethod(block: any())) ~> { try await $0() } + + let result: Bool = try await asyncProtocolInstance.asyncClosureThrowingMethod(block: { true }) + + XCTAssertTrue(result) + verify(await asyncProtocol.asyncClosureThrowingMethod(block: any())).wasCalled() + } + + func testStubAsyncClosureThrowingMethod_throwsErrorFromBlock() async throws { + given(await asyncProtocol.asyncClosureThrowingMethod(block: any())) ~> { try await $0() } + await XCTAssertThrowsAsyncError(try await asyncProtocolInstance.asyncClosureThrowingMethod(block: { throw FakeError() })) + verify(await asyncProtocol.asyncClosureThrowingMethod(block: any())).wasCalled() + } + +}