From a10b059e95eea2c2d114c0c9a726cffeb1798a44 Mon Sep 17 00:00:00 2001
From: treastrain / Tanaka Ryoga
Date: Sat, 4 Mar 2023 03:40:32 +0900
Subject: [PATCH 1/8] Add `String.actor` and `String.actorProtocol`
---
Sources/MockoloFramework/Utils/StringExtensions.swift | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Sources/MockoloFramework/Utils/StringExtensions.swift b/Sources/MockoloFramework/Utils/StringExtensions.swift
index 9fee0222..27edc792 100644
--- a/Sources/MockoloFramework/Utils/StringExtensions.swift
+++ b/Sources/MockoloFramework/Utils/StringExtensions.swift
@@ -38,6 +38,7 @@ extension String {
static let `static` = "static"
static let importSpace = "import "
static public let `class` = "class"
+ static public let `actor` = "actor"
static public let `final` = "final"
static let override = "override"
static let privateSet = "private(set)"
@@ -47,6 +48,7 @@ extension String {
static let any = "Any"
static let some = "some"
static let anyObject = "AnyObject"
+ static let actorProtocol = "Actor"
static let fatalError = "fatalError"
static let available = "available"
static let `public` = "public"
From f6f56d5fe5e7e88812580d46d581810c8e75a939 Mon Sep 17 00:00:00 2001
From: treastrain / Tanaka Ryoga
Date: Sat, 4 Mar 2023 03:43:24 +0900
Subject: [PATCH 2/8] Add `ModelType.actor`
---
Sources/MockoloFramework/Models/Model.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Sources/MockoloFramework/Models/Model.swift b/Sources/MockoloFramework/Models/Model.swift
index 2743c8e0..ef22faff 100644
--- a/Sources/MockoloFramework/Models/Model.swift
+++ b/Sources/MockoloFramework/Models/Model.swift
@@ -17,7 +17,7 @@
import Foundation
public enum ModelType {
- case variable, method, typeAlias, parameter, macro, `class`
+ case variable, method, typeAlias, parameter, macro, `class`, `actor`
}
/// Represents a model for an entity such as var, func, class, etc.
From 9f5b92291abda48d9ef05c9aea0bf7d0c9110e9a Mon Sep 17 00:00:00 2001
From: treastrain / Tanaka Ryoga
Date: Sat, 4 Mar 2023 03:43:44 +0900
Subject: [PATCH 3/8] Create ActorTemplate.swift
---
.../Templates/ActorTemplate.swift | 286 ++++++++++++++++++
1 file changed, 286 insertions(+)
create mode 100644 Sources/MockoloFramework/Templates/ActorTemplate.swift
diff --git a/Sources/MockoloFramework/Templates/ActorTemplate.swift b/Sources/MockoloFramework/Templates/ActorTemplate.swift
new file mode 100644
index 00000000..9788666f
--- /dev/null
+++ b/Sources/MockoloFramework/Templates/ActorTemplate.swift
@@ -0,0 +1,286 @@
+//
+// ActorTemplate.swift
+// MockoloFramework
+//
+// Created by treastrain on 2023/03/04.
+//
+
+import Foundation
+
+extension ActorModel {
+ func applyActorTemplate(name: String,
+ identifier: String,
+ accessLevel: String,
+ attribute: String,
+ declType: DeclType,
+ metadata: AnnotationMetadata?,
+ useTemplateFunc: Bool,
+ useMockObservable: Bool,
+ allowSetCallCount: Bool,
+ mockFinal: Bool,
+ enableFuncArgsHistory: Bool,
+ disableCombineDefaultValues: Bool,
+ initParamCandidates: [Model],
+ declaredInits: [MethodModel],
+ entities: [(String, Model)]) -> String {
+
+ processCombineAliases(entities: entities)
+
+ let acl = accessLevel.isEmpty ? "" : accessLevel + " "
+ let typealiases = typealiasWhitelist(in: entities)
+ let renderedEntities = entities
+ .compactMap { (uniqueId: String, model: Model) -> (String, Int64)? in
+ if model.modelType == .typeAlias, let _ = typealiases?[model.name] {
+ // this case will be handlded by typealiasWhitelist look up later
+ return nil
+ }
+ if model.modelType == .variable, model.name == String.hasBlankInit {
+ return nil
+ }
+ if model.modelType == .method, model.isInitializer, !model.processed {
+ return nil
+ }
+ if let ret = model.render(with: uniqueId, encloser: name, useTemplateFunc: useTemplateFunc, useMockObservable: useMockObservable, allowSetCallCount: allowSetCallCount, mockFinal: mockFinal, enableFuncArgsHistory: enableFuncArgsHistory, disableCombineDefaultValues: disableCombineDefaultValues) {
+ return (ret, model.offset)
+ }
+ return nil
+ }
+ .sorted { (left: (String, Int64), right: (String, Int64)) -> Bool in
+ if left.1 == right.1 {
+ return left.0 < right.0
+ }
+ return left.1 < right.1
+ }
+ .map {$0.0}
+ .joined(separator: "\n")
+
+ var typealiasTemplate = ""
+ let addAcl = declType == .protocolType ? acl : ""
+ if let typealiasWhitelist = typealiases {
+ typealiasTemplate = typealiasWhitelist.map { (arg: (key: String, value: [String])) -> String in
+ let joinedType = arg.value.sorted().joined(separator: " & ")
+ return "\(1.tab)\(addAcl)\(String.typealias) \(arg.key) = \(joinedType)"
+ }.joined(separator: "\n")
+ }
+
+ var moduleDot = ""
+ if let moduleName = metadata?.module, !moduleName.isEmpty {
+ moduleDot = moduleName + "."
+ }
+
+ let extraInits = extraInitsIfNeeded(initParamCandidates: initParamCandidates, declaredInits: declaredInits, acl: acl, declType: declType, overrides: metadata?.varTypes)
+
+ var body = ""
+ if !typealiasTemplate.isEmpty {
+ body += "\(typealiasTemplate)\n"
+ }
+ if !extraInits.isEmpty {
+ body += "\(extraInits)\n"
+ }
+ if !renderedEntities.isEmpty {
+ body += "\(renderedEntities)"
+ }
+
+ let finalStr = mockFinal ? "\(String.final) " : ""
+ let template = """
+ \(attribute)
+ \(acl)\(finalStr)actor \(name): \(moduleDot)\(identifier) {
+ \(body)
+ }
+ """
+
+ return template
+ }
+
+ private func extraInitsIfNeeded(initParamCandidates: [Model],
+ declaredInits: [MethodModel],
+ acl: String,
+ declType: DeclType,
+ overrides: [String: String]?) -> String {
+
+ let declaredInitParamsPerInit = declaredInits.map { $0.params }
+
+ var needParamedInit = false
+ var needBlankInit = false
+
+ if declaredInits.isEmpty, initParamCandidates.isEmpty {
+ needBlankInit = true
+ needParamedInit = false
+ } else {
+ if declType == .protocolType {
+ needParamedInit = !initParamCandidates.isEmpty
+ needBlankInit = true
+
+ let buffer = initParamCandidates.sorted(path: \.fullName, fallback: \.name)
+ for paramList in declaredInitParamsPerInit {
+ if paramList.isEmpty {
+ needBlankInit = false
+ } else {
+ let list = paramList.sorted(path: \.fullName, fallback: \.name)
+ if list.count > 0, list.count == buffer.count {
+ let dups = zip(list, buffer).filter {$0.0.fullName == $0.1.fullName}
+ if !dups.isEmpty {
+ needParamedInit = false
+ }
+ }
+ }
+ }
+ }
+ }
+
+ var initTemplate = ""
+ if needParamedInit {
+ var paramsAssign = ""
+ let params = initParamCandidates
+ .map { (element: Model) -> String in
+ if let val = element.type.defaultVal(with: overrides, overrideKey: element.name, isInitParam: true) {
+ return "\(element.name): \(element.type.typeName) = \(val)"
+ }
+ var prefix = ""
+ if element.type.hasClosure {
+ if !element.type.isOptional {
+ prefix = String.escaping + " "
+ }
+ }
+ return "\(element.name): \(prefix)\(element.type.typeName)"
+ }
+ .joined(separator: ", ")
+
+
+ paramsAssign = initParamCandidates.map { p in
+ return "\(2.tab)self.\(p.underlyingName) = \(p.name.safeName)"
+
+ }.joined(separator: "\n")
+
+ initTemplate = """
+ \(1.tab)\(acl)init(\(params)) {
+ \(paramsAssign)
+ \(1.tab)}
+ """
+ }
+
+ let extraInitParamNames = initParamCandidates.map{$0.name}
+ let extraVarsToDecl = declaredInitParamsPerInit.flatMap{$0}.compactMap { (p: ParamModel) -> String? in
+ if !extraInitParamNames.contains(p.name) {
+ return p.asVarDecl
+ }
+ return nil
+ }
+ .joined(separator: "\n")
+
+ let declaredInitStr = declaredInits.compactMap { (m: MethodModel) -> String? in
+ if case let .initKind(required, override) = m.kind, !m.processed {
+ let modifier = required ? "\(String.required) " : (override ? "\(String.override) " : "")
+ let mAcl = m.accessLevel.isEmpty ? "" : "\(m.accessLevel) "
+ let genericTypeDeclsStr = m.genericTypeParams.compactMap {$0.render(with: "", encloser: "")}.joined(separator: ", ")
+ let genericTypesStr = genericTypeDeclsStr.isEmpty ? "" : "<\(genericTypeDeclsStr)>"
+ let paramDeclsStr = m.params.compactMap{$0.render(with: "", encloser: "")}.joined(separator: ", ")
+
+ if override {
+ let paramsList = m.params.map { param in
+ return "\(param.name): \(param.name.safeName)"
+ }.joined(separator: ", ")
+
+ return """
+ \(1.tab)\(modifier)\(mAcl)init\(genericTypesStr)(\(paramDeclsStr)) {
+ \(2.tab)super.init(\(paramsList))
+ \(1.tab)}
+ """
+ } else {
+ let paramsAssign = m.params.map { param in
+ let underVars = initParamCandidates.compactMap { return $0.name.safeName == param.name.safeName ? $0.underlyingName : nil}
+ if let underVar = underVars.first {
+ return "\(2.tab)self.\(underVar) = \(param.name.safeName)"
+ } else {
+ return "\(2.tab)self.\(param.underlyingName) = \(param.name.safeName)"
+ }
+ }.joined(separator: "\n")
+
+ return """
+ \(1.tab)\(modifier)\(mAcl)init\(genericTypesStr)(\(paramDeclsStr)) {
+ \(paramsAssign)
+ \(1.tab)}
+ """
+ }
+ }
+ return nil
+ }.sorted().joined(separator: "\n")
+
+ var template = ""
+
+ if !extraVarsToDecl.isEmpty {
+ template += "\(1.tab)\(extraVarsToDecl)\n"
+ }
+
+ if needBlankInit {
+ // In case of protocol mocking, we want to provide a blank init (if not present already) for convenience,
+ // where instance vars do not have to be set in init since they all have get/set (see VariableTemplate).
+ let blankInit = "\(acl)init() { }"
+ template += "\(1.tab)\(blankInit)\n"
+ }
+
+ if !initTemplate.isEmpty {
+ template += "\(initTemplate)\n"
+ }
+
+ if !declaredInitStr.isEmpty {
+ template += "\(declaredInitStr)\n"
+ }
+
+ return template
+ }
+
+
+ /// Returns a map of typealiases with conflicting types to be whitelisted
+ /// @param models Potentially contains typealias models
+ /// @returns A map of typealiases with multiple possible types
+ func typealiasWhitelist(`in` models: [(String, Model)]) -> [String: [String]]? {
+ let typealiasModels = models.filter{$0.1.modelType == .typeAlias}
+ var aliasMap = [String: [String]]()
+ typealiasModels.forEach { (arg: (key: String, value: Model)) in
+
+ let alias = arg.value
+ if aliasMap[alias.name] == nil {
+ aliasMap[alias.name] = [alias.type.typeName]
+ } else {
+ if let val = aliasMap[alias.name], !val.contains(alias.type.typeName) {
+ aliasMap[alias.name]?.append(alias.type.typeName)
+ }
+ }
+ }
+ let aliasDupes = aliasMap.filter {$0.value.count > 1}
+ return aliasDupes.isEmpty ? nil : aliasDupes
+ }
+
+ // Finds all combine properties that are attempting to use a property wrapper alias
+ // and locates the matching property within the actor, if one exists.
+ //
+ private func processCombineAliases(entities: [(String, Model)]) {
+ var variableModels = [VariableModel]()
+ var nameToVariableModels = [String: VariableModel]()
+
+ for entity in entities {
+ guard let variableModel = entity.1 as? VariableModel else {
+ continue
+ }
+ variableModels.append(variableModel)
+ nameToVariableModels[variableModel.name] = variableModel
+ }
+
+ for variableModel in variableModels {
+ guard case .property(let wrapper, let name) = variableModel.combineType else {
+ continue
+ }
+
+ // If a variable member in this entity already exists, link the two together.
+ // Otherwise, the user's setup is incorrect and we will fallback to using a PassthroughSubject.
+ //
+ if let matchingAliasModel = nameToVariableModels[name] {
+ variableModel.wrapperAliasModel = matchingAliasModel
+ matchingAliasModel.propertyWrapper = wrapper
+ } else {
+ variableModel.combineType = .passthroughSubject
+ }
+ }
+ }
+}
From 3ade7291efa36ff6279e722e381c61216f8217d5 Mon Sep 17 00:00:00 2001
From: treastrain / Tanaka Ryoga
Date: Sat, 4 Mar 2023 03:43:52 +0900
Subject: [PATCH 4/8] Create ActorModel.swift
---
.../MockoloFramework/Models/ActorModel.swift | 52 +++++++++++++++++++
1 file changed, 52 insertions(+)
create mode 100644 Sources/MockoloFramework/Models/ActorModel.swift
diff --git a/Sources/MockoloFramework/Models/ActorModel.swift b/Sources/MockoloFramework/Models/ActorModel.swift
new file mode 100644
index 00000000..b4c30445
--- /dev/null
+++ b/Sources/MockoloFramework/Models/ActorModel.swift
@@ -0,0 +1,52 @@
+//
+// ActorModel.swift
+// MockoloFramework
+//
+// Created by treastrain on 2023/03/04.
+//
+
+import Foundation
+
+final class ActorModel: Model {
+ var name: String
+ var offset: Int64
+ var type: Type
+ let attribute: String
+ let accessLevel: String
+ let identifier: String
+ let declType: DeclType
+ let entities: [(String, Model)]
+ let initParamCandidates: [Model]
+ let declaredInits: [MethodModel]
+ let metadata: AnnotationMetadata?
+
+ var modelType: ModelType {
+ return .actor
+ }
+
+ init(identifier: String,
+ acl: String,
+ declType: DeclType,
+ attributes: [String],
+ offset: Int64,
+ metadata: AnnotationMetadata?,
+ initParamCandidates: [Model],
+ declaredInits: [MethodModel],
+ entities: [(String, Model)]) {
+ self.identifier = identifier
+ self.name = identifier + "Mock"
+ self.type = Type(.actor)
+ self.declType = declType
+ self.entities = entities
+ self.declaredInits = declaredInits
+ self.initParamCandidates = initParamCandidates
+ self.metadata = metadata
+ self.offset = offset
+ self.attribute = Set(attributes.filter {$0.contains(String.available)}).joined(separator: " ")
+ self.accessLevel = acl
+ }
+
+ func render(with identifier: String, encloser: String, useTemplateFunc: Bool, useMockObservable: Bool, allowSetCallCount: Bool, mockFinal: Bool, enableFuncArgsHistory: Bool, disableCombineDefaultValues: Bool) -> String? {
+ return applyActorTemplate(name: name, identifier: self.identifier, accessLevel: accessLevel, attribute: attribute, declType: declType, metadata: metadata, useTemplateFunc: useTemplateFunc, useMockObservable: useMockObservable, allowSetCallCount: allowSetCallCount, mockFinal: mockFinal, enableFuncArgsHistory: enableFuncArgsHistory, disableCombineDefaultValues: disableCombineDefaultValues, initParamCandidates: initParamCandidates, declaredInits: declaredInits, entities: entities)
+ }
+}
From b64b365b7405c2ae7d4e76ae4d68f0c8754cd2c3 Mon Sep 17 00:00:00 2001
From: treastrain / Tanaka Ryoga
Date: Sat, 4 Mar 2023 06:23:49 +0900
Subject: [PATCH 5/8] `ResolvedEntity.model()` returns `ActorModel` when the
protocol directly conforms to the `Actor` protocol
---
.../Models/ParsedEntity.swift | 30 +++++++++++++------
1 file changed, 21 insertions(+), 9 deletions(-)
diff --git a/Sources/MockoloFramework/Models/ParsedEntity.swift b/Sources/MockoloFramework/Models/ParsedEntity.swift
index 6f5e525a..5afe1319 100644
--- a/Sources/MockoloFramework/Models/ParsedEntity.swift
+++ b/Sources/MockoloFramework/Models/ParsedEntity.swift
@@ -59,15 +59,27 @@ struct ResolvedEntity {
func model() -> Model {
- return ClassModel(identifier: key,
- acl: entity.entityNode.accessLevel,
- declType: entity.entityNode.declType,
- attributes: attributes,
- offset: entity.entityNode.offset,
- metadata: entity.metadata,
- initParamCandidates: initParamCandidates,
- declaredInits: declaredInits,
- entities: uniqueModels)
+ if entity.entityNode.inheritedTypes.contains(.actorProtocol) {
+ return ActorModel(identifier: key,
+ acl: entity.entityNode.accessLevel,
+ declType: entity.entityNode.declType,
+ attributes: attributes,
+ offset: entity.entityNode.offset,
+ metadata: entity.metadata,
+ initParamCandidates: initParamCandidates,
+ declaredInits: declaredInits,
+ entities: uniqueModels)
+ } else {
+ return ClassModel(identifier: key,
+ acl: entity.entityNode.accessLevel,
+ declType: entity.entityNode.declType,
+ attributes: attributes,
+ offset: entity.entityNode.offset,
+ metadata: entity.metadata,
+ initParamCandidates: initParamCandidates,
+ declaredInits: declaredInits,
+ entities: uniqueModels)
+ }
}
}
From 77c0e72d3d3ef4e2839be752d65ff067a82a39ee Mon Sep 17 00:00:00 2001
From: treastrain / Tanaka Ryoga
Date: Tue, 7 Mar 2023 03:16:21 +0900
Subject: [PATCH 6/8] Create FixtureMockableActor.swift
---
.../FixtureMockableActor.swift | 34 +++++++++++++++++++
1 file changed, 34 insertions(+)
create mode 100644 Tests/TestActorMocking/FixtureMockableActor.swift
diff --git a/Tests/TestActorMocking/FixtureMockableActor.swift b/Tests/TestActorMocking/FixtureMockableActor.swift
new file mode 100644
index 00000000..894ce457
--- /dev/null
+++ b/Tests/TestActorMocking/FixtureMockableActor.swift
@@ -0,0 +1,34 @@
+import MockoloFramework
+
+let actorProtocol =
+"""
+/// \(String.mockAnnotation)
+protocol Foo: Actor {
+ func foo(arg: String) async -> Result
+ var bar: Int { get }
+}
+"""
+
+let actorProtocolMock =
+"""
+actor FooMock: Foo {
+ init() { }
+ init(bar: Int = 0) {
+ self.bar = bar
+ }
+
+
+ private(set) var fooCallCount = 0
+ var fooHandler: ((String) async -> (Result))?
+ func foo(arg: String) async -> Result {
+ fooCallCount += 1
+ if let fooHandler = fooHandler {
+ return await fooHandler(arg)
+ }
+ fatalError("fooHandler returns can't have a default value thus its handler must be set")
+ }
+
+ private(set) var barSetCallCount = 0
+ var bar: Int = 0 { didSet { barSetCallCount += 1 } }
+}
+"""
From 583074f091fb6da43ce100f617b5210b9edabf73 Mon Sep 17 00:00:00 2001
From: treastrain / Tanaka Ryoga
Date: Tue, 7 Mar 2023 03:16:24 +0900
Subject: [PATCH 7/8] Create MockActorTests.swift
---
Tests/TestActorMocking/MockActorTests.swift | 9 +++++++++
1 file changed, 9 insertions(+)
create mode 100644 Tests/TestActorMocking/MockActorTests.swift
diff --git a/Tests/TestActorMocking/MockActorTests.swift b/Tests/TestActorMocking/MockActorTests.swift
new file mode 100644
index 00000000..ec57ca1f
--- /dev/null
+++ b/Tests/TestActorMocking/MockActorTests.swift
@@ -0,0 +1,9 @@
+import Foundation
+
+final class MockActorTests: MockoloTestCase {
+
+ func testActorProtocol() {
+ verify(srcContent: actorProtocol,
+ dstContent: actorProtocolMock)
+ }
+}
From 4f313735422fbe64c12d1463368000f7bc2df4b6 Mon Sep 17 00:00:00 2001
From: treastrain / Tanaka Ryoga
Date: Mon, 13 Mar 2023 16:34:50 +0900
Subject: [PATCH 8/8] Update Tests/TestActorMocking/MockActorTests.swift
---
Tests/TestActorMocking/MockActorTests.swift | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Tests/TestActorMocking/MockActorTests.swift b/Tests/TestActorMocking/MockActorTests.swift
index ec57ca1f..cf5d1143 100644
--- a/Tests/TestActorMocking/MockActorTests.swift
+++ b/Tests/TestActorMocking/MockActorTests.swift
@@ -1,5 +1,6 @@
import Foundation
+#if compiler(>=5.5.2) && canImport(_Concurrency)
final class MockActorTests: MockoloTestCase {
func testActorProtocol() {
@@ -7,3 +8,4 @@ final class MockActorTests: MockoloTestCase {
dstContent: actorProtocolMock)
}
}
+#endif