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

Added override for specifying the global actor of mocked async functions #260

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Added feature for specifying the global actor of mocked async functions
  • Loading branch information
stuaustin committed May 21, 2024
commit d92b6963f4ec69221e075be92927bd879118f0a3
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,32 @@ This will generate:
public class FooMock: FooProtocol { ... }
```

To override the actor used for a mock's async functions:
```swift
/// @mockable(override: asyncFunctionGlobalActor = MainActor)
public protocol FooProtocol {
func asyncFunction() async -> Bool
}
```

This will generate:
```swift
public class FooProtocolMock: FooProtocol {
public init() { }


public private(set) var asyncFunctionCallCount = 0
public var asyncFunctionHandler: (() async -> (Bool))?
@MainActor public func asyncFunction() async -> Bool {
asyncFunctionCallCount += 1
if let asyncFunctionHandler = asyncFunctionHandler {
return await asyncFunctionHandler()
}
return false
}
}
```

## Used libraries

[SwiftSyntax](https://github.com/apple/swift-syntax) |
Expand Down
5 changes: 4 additions & 1 deletion Sources/MockoloFramework/Models/MethodModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ final class MethodModel: Model {
var offset: Int64
let length: Int64
let accessLevel: String
var attributes: [String]? = nil
let attribute: String
let genericTypeParams: [ParamModel]
var genericWhereClause: String? = nil
let params: [ParamModel]
Expand Down Expand Up @@ -175,6 +175,7 @@ final class MethodModel: Model {
funcsWithArgsHistory: [String],
customModifiers: [String: Modifier],
modelDescription: String?,
globalActorAttribute: String?,
processed: Bool) {
self.name = name.trimmingCharacters(in: .whitespaces)
self.type = Type(typeName.trimmingCharacters(in: .whitespaces))
Expand All @@ -192,6 +193,7 @@ final class MethodModel: Model {
self.customModifiers = customModifiers
self.modelDescription = modelDescription
self.accessLevel = acl
self.attribute = globalActorAttribute.map { "@\($0)" } ?? ""
}

var fullName: String {
Expand Down Expand Up @@ -237,6 +239,7 @@ final class MethodModel: Model {
params: params,
returnType: type,
accessLevel: accessLevel,
attribute: attribute,
suffix: suffix,
argsHistory: argsHistory,
handler: handler(encloser: encloser))
Expand Down
1 change: 1 addition & 0 deletions Sources/MockoloFramework/Models/ParsedEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ public enum CombineType {
/// e.g. @mockable(module: prefix = Foo; typealias: T = Any; U = String; rx: barStream = PublishSubject; history: bazFunc = true; modifiers: someVar = weak; combine: fooPublisher = CurrentValueSubject; otherPublisher = @Published otherProperty, override: name = FooMock)
struct AnnotationMetadata {
var nameOverride: String?
var asyncFunctionGlobalActorOverride: String?
var module: String?
var typeAliases: [String: String]?
var varTypes: [String: String]?
Expand Down
20 changes: 13 additions & 7 deletions Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,19 +163,19 @@ extension MemberBlockItemSyntax {
} else if let funcMember = self.decl.as(FunctionDeclSyntax.self) {
if validateMember(funcMember.modifiers, declType, processed: processed) {
let acl = memberAcl(funcMember.modifiers, encloserAcl, declType)
let item = funcMember.model(with: acl, declType: declType, funcsWithArgsHistory: metadata?.funcsWithArgsHistory, customModifiers: metadata?.modifiers, processed: processed)
let item = funcMember.model(with: acl, declType: declType, metadata: metadata, funcsWithArgsHistory: metadata?.funcsWithArgsHistory, customModifiers: metadata?.modifiers, processed: processed)
return (item, funcMember.attributes.trimmedDescription, false)
}
} else if let subscriptMember = self.decl.as(SubscriptDeclSyntax.self) {
if validateMember(subscriptMember.modifiers, declType, processed: processed) {
let acl = memberAcl(subscriptMember.modifiers, encloserAcl, declType)
let item = subscriptMember.model(with: acl, declType: declType, processed: processed)
let item = subscriptMember.model(with: acl, declType: declType, metadata: metadata, processed: processed)
return (item, subscriptMember.attributes.trimmedDescription, false)
}
} else if let initMember = self.decl.as(InitializerDeclSyntax.self) {
if validateInit(initMember, declType, processed: processed) {
let acl = memberAcl(initMember.modifiers, encloserAcl, declType)
let item = initMember.model(with: acl, declType: declType, processed: processed)
let item = initMember.model(with: acl, declType: declType, metadata: metadata, processed: processed)
return (item, initMember.attributes.trimmedDescription, true)
}
} else if let patMember = self.decl.as(AssociatedTypeDeclSyntax.self) {
Expand Down Expand Up @@ -387,7 +387,7 @@ extension VariableDeclSyntax {
}

extension SubscriptDeclSyntax {
func model(with acl: String, declType: DeclType, processed: Bool) -> Model {
func model(with acl: String, declType: DeclType, metadata: AnnotationMetadata?, processed: Bool) -> Model {
let isStatic = self.modifiers.isStatic

let params = self.parameterClause.parameters.compactMap { $0.model(inInit: false, declType: declType) }
Expand All @@ -410,20 +410,23 @@ extension SubscriptDeclSyntax {
funcsWithArgsHistory: [],
customModifiers: [:],
modelDescription: self.description,
globalActorAttribute: metadata?.asyncFunctionGlobalActorOverride,
processed: processed)
return subscriptModel
}
}

extension FunctionDeclSyntax {

func model(with acl: String, declType: DeclType, funcsWithArgsHistory: [String]?, customModifiers: [String : Modifier]?, processed: Bool) -> Model {
func model(with acl: String, declType: DeclType, metadata: AnnotationMetadata?, funcsWithArgsHistory: [String]?, customModifiers: [String : Modifier]?, processed: Bool) -> Model {
let isStatic = self.modifiers.isStatic

let params = self.signature.parameterClause.parameters.compactMap { $0.model(inInit: false, declType: declType) }
let genericTypeParams = self.genericParameterClause?.parameters.compactMap { $0.model(inInit: false) } ?? []
let genericWhereClause = self.genericWhereClause?.description

let asyncOrReasync = self.signature.effectSpecifiers?.asyncSpecifier?.text

let funcmodel = MethodModel(name: self.name.description,
typeName: self.signature.returnClause?.type.description ?? "",
kind: .funcKind,
Expand All @@ -433,13 +436,14 @@ extension FunctionDeclSyntax {
genericWhereClause: genericWhereClause,
params: params,
throwsOrRethrows: self.signature.effectSpecifiers?.throwsSpecifier?.text,
asyncOrReasync: self.signature.effectSpecifiers?.asyncSpecifier?.text,
asyncOrReasync: asyncOrReasync,
isStatic: isStatic,
offset: self.offset,
length: self.length,
funcsWithArgsHistory: funcsWithArgsHistory ?? [],
customModifiers: customModifiers ?? [:],
modelDescription: self.description,
globalActorAttribute: asyncOrReasync != nil ? metadata?.asyncFunctionGlobalActorOverride : nil,
processed: processed)
return funcmodel
}
Expand All @@ -458,7 +462,7 @@ extension InitializerDeclSyntax {
return false
}

func model(with acl: String, declType: DeclType, processed: Bool) -> Model {
func model(with acl: String, declType: DeclType, metadata: AnnotationMetadata?, processed: Bool) -> Model {
let requiredInit = isRequired(with: declType)

let params = self.signature.parameterClause.parameters.compactMap { $0.model(inInit: true, declType: declType) }
Expand All @@ -481,6 +485,7 @@ extension InitializerDeclSyntax {
funcsWithArgsHistory: [],
customModifiers: [:],
modelDescription: self.description,
globalActorAttribute: metadata?.asyncFunctionGlobalActorOverride,
processed: processed)
}

Expand Down Expand Up @@ -707,6 +712,7 @@ extension Trivia {
if let arguments = parseArguments(argsStr, identifier: .overrideColon) {

ret.nameOverride = arguments[.name]
ret.asyncFunctionGlobalActorOverride = arguments[.asyncFunctionGlobalActor]
}
if let arguments = parseArguments(argsStr, identifier: .rxColon) {

Expand Down
4 changes: 3 additions & 1 deletion Sources/MockoloFramework/Templates/MethodTemplate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ extension MethodModel {
params: [ParamModel],
returnType: Type,
accessLevel: String,
attribute: String,
suffix: String,
argsHistory: ArgumentsHistoryModel?,
handler: ClosureModel?) -> String {
Expand All @@ -39,6 +40,7 @@ extension MethodModel {
let returnTypeName = returnType.isUnknown ? "" : returnType.typeName

let acl = accessLevel.isEmpty ? "" : accessLevel+" "
let attribute = attribute.isEmpty ? "" : attribute+" "
let genericTypeDeclsStr = genericTypeParams.compactMap {$0.render(with: "", encloser: "")}.joined(separator: ", ")
let genericTypesStr = genericTypeDeclsStr.isEmpty ? "" : "<\(genericTypeDeclsStr)>"
var genericWhereStr = ""
Expand Down Expand Up @@ -148,7 +150,7 @@ extension MethodModel {
template = """
\(template)
\(1.tab)\(acl)\(staticStr)var \(handlerVarName): \(handlerVarType)
\(1.tab)\(acl)\(staticStr)\(overrideStr)\(modifierTypeStr)\(keyword)\(name)\(genericTypesStr)(\(paramDeclsStr)) \(suffixStr)\(returnStr)\(genericWhereStr) {
\(1.tab)\(attribute)\(acl)\(staticStr)\(overrideStr)\(modifierTypeStr)\(keyword)\(name)\(genericTypesStr)(\(paramDeclsStr)) \(suffixStr)\(returnStr)\(genericWhereStr) {
\(wrapped)
\(1.tab)}
"""
Expand Down
1 change: 1 addition & 0 deletions Sources/MockoloFramework/Utils/StringExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ extension String {
static let `escaping` = "@escaping"
static let autoclosure = "@autoclosure"
static let name = "name"
static let asyncFunctionGlobalActor = "asyncFunctionGlobalActor"
static let sendable = "Sendable"
static let uncheckedSendable = "@unchecked Sendable"
static public let mockAnnotation = "@mockable"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

class AsyncFunctionGlobalActorOverrideTests: MockoloTestCase {
func testAsyncFunctionGlobalActorOverride() {
verify(srcContent: asyncFunctionGlobalActorOverride, dstContent: asyncFunctionGlobalActorOverrideMock)
}
}
38 changes: 38 additions & 0 deletions Tests/TestOverrides/FixtureMockAsyncFunctionGlobalActors.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Foundation

let asyncFunctionGlobalActorOverride =
"""
/// \(String.mockAnnotation)(override: asyncFunctionGlobalActor = MainActor)
protocol FooProtocol {
func asyncFunction() async -> Bool
func syncFunction() -> Bool
}
"""

let asyncFunctionGlobalActorOverrideMock =
"""
class FooProtocolMock: FooProtocol {
init() { }


private(set) var asyncFunctionCallCount = 0
var asyncFunctionHandler: (() async -> (Bool))?
@MainActor func asyncFunction() async -> Bool {
asyncFunctionCallCount += 1
if let asyncFunctionHandler = asyncFunctionHandler {
return await asyncFunctionHandler()
}
return false
}

private(set) var syncFunctionCallCount = 0
var syncFunctionHandler: (() -> (Bool))?
func syncFunction() -> Bool {
syncFunctionCallCount += 1
if let syncFunctionHandler = syncFunctionHandler {
return syncFunctionHandler()
}
return false
}
}
"""