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

Add support for concurrency mocking #277

Merged
merged 6 commits into from
Jan 26, 2022
Merged
Show file tree
Hide file tree
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
name: CI

on: [push, pull_request]
env:
DEVELOPER_DIR: /Applications/Xcode_13.2.app/Contents/Developer

jobs:
test-e2e:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ on:
- 'release-*'
tags:
- '*'
env:
DEVELOPER_DIR: /Applications/Xcode_13.2.app/Contents/Developer

jobs:
build-signed-artifacts:
Expand Down
10 changes: 9 additions & 1 deletion Mockingbird.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -719,6 +721,8 @@
28FB7EBF2789B9B000125FDA /* MockingbirdCommon.xcscheme */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = MockingbirdCommon.xcscheme; sourceTree = "<group>"; };
28FB7EC72789D0E000125FDA /* Path+Backup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Path+Backup.swift"; sourceTree = "<group>"; };
28FB7EC92789D87800125FDA /* Git.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Git.swift; sourceTree = "<group>"; };
5E45ADF3279DA63D004AB972 /* StubbingAsyncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubbingAsyncTests.swift; sourceTree = "<group>"; };
5E45ADF5279DB949004AB972 /* AsyncMethods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncMethods.swift; sourceTree = "<group>"; };
8356225B26A94CBE005CD5C5 /* TargetDescriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetDescriptionTests.swift; sourceTree = "<group>"; };
942B00CDCB48ADC877A01AEE /* MockingbirdTestsHostMocks.generated.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; name = MockingbirdTestsHostMocks.generated.swift; path = Tests/MockingbirdTests/Mocks/MockingbirdTestsHostMocks.generated.swift; sourceTree = "<group>"; };
D314ED7E24CE1C10000CC23D /* GenericsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericsTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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" */ = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
},
{
Expand Down
4 changes: 4 additions & 0 deletions Sources/MockingbirdFramework/Mocking/Declaration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down
17 changes: 17 additions & 0 deletions Sources/MockingbirdFramework/Mocking/MockingContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,30 @@ 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<T, I: Invocation>(_ 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<T, I: Invocation>(_ invocation: I, evaluating thunk: (I) -> T) -> T {
// Ensures that the thunk is evaluated prior to recording the invocation.
defer { didInvoke(invocation) }
return thunk(invocation)
}

/// Invoke an async non-throwing thunk.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func didInvoke<T, I: Invocation>(_ 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? {
Expand Down
28 changes: 26 additions & 2 deletions Sources/MockingbirdFramework/Stubbing/Stubbing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,14 @@ public class StubbingManager<DeclarationType: Declaration, InvocationType, Retur
var implementationsProvidedCount = 0
var stubs = [(stub: StubbingContext.Stub, context: Context)]()

var isAsync: Bool {
return DeclarationType.self == AsyncFunctionDeclaration.self || DeclarationType.self == ThrowingAsyncFunctionDeclaration.self
}

var isThrowing: Bool {
return DeclarationType.self == ThrowingFunctionDeclaration.self || DeclarationType.self == ThrowingAsyncFunctionDeclaration.self
}

/// When to use the next chained implementation provider.
public enum TransitionStrategy {
/// Go to the next provider after providing a certain number of implementations.
Expand Down Expand Up @@ -288,7 +296,15 @@ public class StubbingManager<DeclarationType: Declaration, InvocationType, Retur
/// - Returns: The current stubbing manager which can be used to chain additional stubs.
@discardableResult
public func willReturn(_ value: ReturnType) -> 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.
Expand Down Expand Up @@ -481,7 +497,15 @@ public func ~> <DeclarationType: Declaration, InvocationType, ReturnType>(
manager: StaticStubbingManager<DeclarationType, InvocationType, ReturnType>,
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -65,6 +66,7 @@ class MethodTemplate: Template {
return FunctionCallTemplate(
name: scopedName,
arguments: self.invocationArguments,
isAsync: self.method.isAsync,
isThrowing: self.method.isThrowing).render()
}

Expand All @@ -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()
Expand Down Expand Up @@ -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 : """
Expand Down Expand Up @@ -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)"
}
}
}()

Expand Down Expand Up @@ -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)
})
Expand All @@ -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)"
}()

Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading