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

Concurrency safe handler #276

Merged
merged 13 commits into from
Dec 3, 2024
32 changes: 22 additions & 10 deletions Sources/MockoloFramework/Models/ArgumentsHistoryModel.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import Foundation
//
// Copyright (c) 2018. Uber Technologies
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

final class ArgumentsHistoryModel: Model {
let name: String
let type: SwiftType
let capturedValueType: SwiftType
let offset: Int64 = .max
let capturableParamNames: [String]
let capturableParamTypes: [SwiftType]
let capturableParams: [(String, SwiftType)]
let isHistoryAnnotated: Bool

var modelType: ModelType {
Expand All @@ -22,11 +35,10 @@ final class ArgumentsHistoryModel: Model {
self.name = name + .argsHistorySuffix
self.isHistoryAnnotated = isHistoryAnnotated

self.capturableParamNames = capturables.map(\.name.safeName)
self.capturableParamTypes = capturables.map(\.type)
self.capturableParams = capturables.map { ($0.name.safeName, $0.type) }

let genericTypeNameList = genericTypeParams.map(\.name)
self.type = SwiftType.toArgumentsHistoryType(with: capturableParamTypes, typeParams: genericTypeNameList)
self.capturedValueType = SwiftType.toArgumentsCaptureType(with: capturableParams.map(\.1), typeParams: genericTypeNameList)
}

func enable(force: Bool) -> Bool {
Expand All @@ -44,11 +56,11 @@ final class ArgumentsHistoryModel: Model {
return nil
}

switch capturableParamNames.count {
switch capturableParams.count {
case 1:
return "\(overloadingResolvedName)\(String.argsHistorySuffix).append(\(capturableParamNames[0]))"
return "\(overloadingResolvedName)\(String.argsHistorySuffix).append(\(capturableParams[0].0))"
case 2...:
let paramNamesStr = capturableParamNames.joined(separator: ", ")
let paramNamesStr = capturableParams.map(\.0).joined(separator: ", ")
return "\(overloadingResolvedName)\(String.argsHistorySuffix).append((\(paramNamesStr)))"
default:
fatalError("paramNames must not be empty.")
Expand Down
22 changes: 10 additions & 12 deletions Sources/MockoloFramework/Models/ClosureModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,49 +22,47 @@ final class ClosureModel: Model {

let funcReturnType: SwiftType
let genericTypeNames: [String]
let paramNames: [String]
let paramTypes: [SwiftType]
let params: [(String, SwiftType)]
let isAsync: Bool
let throwing: ThrowingKind

var modelType: ModelType {
return .closure
}

init(genericTypeParams: [ParamModel], paramNames: [String], paramTypes: [SwiftType], isAsync: Bool, throwing: ThrowingKind, returnType: SwiftType) {
init(genericTypeParams: [ParamModel], params: [(String, SwiftType)], isAsync: Bool, throwing: ThrowingKind, returnType: SwiftType) {
// In the mock's call handler, rethrows is unavailable.
let throwing = throwing.coerceRethrowsToThrows
self.isAsync = isAsync
self.throwing = throwing
self.genericTypeNames = genericTypeParams.map(\.name)
self.paramNames = paramNames
self.paramTypes = paramTypes
self.params = params
self.funcReturnType = returnType
}

func type(enclosingType: SwiftType) -> SwiftType {
func type(enclosingType: SwiftType, requiresSendable: Bool) -> SwiftType {
return SwiftType.toClosureType(
params: paramTypes,
params: params.map(\.1),
typeParams: genericTypeNames,
isAsync: isAsync,
throwing: throwing,
returnType: funcReturnType,
encloser: enclosingType
encloser: enclosingType,
requiresSendable: requiresSendable
)
}

func render(
context: RenderContext,
arguments: GenerationArguments = .default
arguments: GenerationArguments
) -> String? {
guard let overloadingResolvedName = context.overloadingResolvedName,
let enclosingType = context.enclosingType else {
return nil
}
return applyClosureTemplate(type: type(enclosingType: enclosingType),
return applyClosureTemplate(type: type(enclosingType: enclosingType, requiresSendable: context.requiresSendable),
name: overloadingResolvedName + .handlerSuffix,
paramVals: paramNames,
paramTypes: paramTypes,
params: params,
returnDefaultType: funcReturnType)
}
}
Expand Down
3 changes: 1 addition & 2 deletions Sources/MockoloFramework/Models/MethodModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,7 @@ final class MethodModel: Model {
}

return ClosureModel(genericTypeParams: genericTypeParams,
paramNames: params.map(\.name),
paramTypes: params.map(\.type),
params: params.map { ($0.name, $0.type) },
isAsync: isAsync,
throwing: throwing,
returnType: returnType)
Expand Down
1 change: 1 addition & 0 deletions Sources/MockoloFramework/Models/Model.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ struct RenderContext {
var overloadingResolvedName: String?
var enclosingType: SwiftType?
var annotatedTypeKind: NominalTypeDeclKind?
var requiresSendable: Bool = false
}

/// Represents a model for an entity such as var, func, class, etc.
Expand Down
11 changes: 5 additions & 6 deletions Sources/MockoloFramework/Models/NominalModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ final class NominalModel: Model {
let accessLevel: String
let identifier: String
let declKindOfMockAnnotatedBaseType: NominalTypeDeclKind
let inheritedTypes: [String]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question]

I am sorry but could you elaborate why you delete inheritedTypes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it is never used.
It might be back in the future if needed.

let entities: [(String, Model)]
let initParamCandidates: [VariableModel]
let declaredInits: [MethodModel]
let metadata: AnnotationMetadata?
let declKind: NominalTypeDeclKind
let requiresSendable: Bool

var modelType: ModelType {
return .nominal
Expand All @@ -39,27 +39,27 @@ final class NominalModel: Model {
acl: String,
declKindOfMockAnnotatedBaseType: NominalTypeDeclKind,
declKind: NominalTypeDeclKind,
inheritedTypes: [String],
attributes: [String],
offset: Int64,
metadata: AnnotationMetadata?,
initParamCandidates: [VariableModel],
declaredInits: [MethodModel],
entities: [(String, Model)]) {
self.identifier = identifier
entities: [(String, Model)],
requiresSendable: Bool) {
self.identifier = identifier
self.name = metadata?.nameOverride ?? (identifier + "Mock")
self.type = SwiftType(self.name)
self.namespaces = namespaces
self.declKindOfMockAnnotatedBaseType = declKindOfMockAnnotatedBaseType
self.declKind = declKind
self.inheritedTypes = inheritedTypes
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
self.requiresSendable = requiresSendable
}

func render(
Expand All @@ -71,7 +71,6 @@ final class NominalModel: Model {
identifier: self.identifier,
accessLevel: accessLevel,
attribute: attribute,
inheritedTypes: inheritedTypes,
metadata: metadata,
arguments: arguments,
initParamCandidates: initParamCandidates,
Expand Down
20 changes: 16 additions & 4 deletions Sources/MockoloFramework/Models/ParamModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
// limitations under the License.
//

import Foundation

final class ParamModel: Model {
internal init(label: String, name: String, type: SwiftType, isGeneric: Bool, inInit: Bool, needsVarDecl: Bool, offset: Int64, length: Int64) {
self.label = label
Expand Down Expand Up @@ -84,9 +82,23 @@ final class ParamModel: Model {
}

func render(
context: RenderContext = .init(),
arguments: GenerationArguments = .default
context: RenderContext,
arguments: GenerationArguments
) -> String? {
return applyParamTemplate(name: name, label: label, type: type, inInit: inInit)
}
}

extension [ParamModel] {
func render(
context: RenderContext,
arguments: GenerationArguments
) -> String {
return self.compactMap {
$0.render(
context: context,
arguments: arguments
)
}.joined(separator: ", ")
}
}
13 changes: 10 additions & 3 deletions Sources/MockoloFramework/Models/ParsedEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ struct ResolvedEntity {
var uniqueModels: [(String, Model)]
var attributes: [String]
var inheritedTypes: [String]
var inheritsActorProtocol: Bool

var declaredInits: [MethodModel] {
return uniqueModels.compactMap { (_, model) in
Expand All @@ -39,6 +38,10 @@ struct ResolvedEntity {
)
}

var inheritsActorProtocol: Bool {
return inheritedTypes.contains(.actorProtocol)
}

/// Returns models that can be used as parameters to an initializer
/// @param models The models of the current entity including unprocessed (ones to generate) and
/// processed (already mocked by a previous run if any) models.
Expand All @@ -56,19 +59,23 @@ struct ResolvedEntity {
return result
}

var requiresSendable: Bool {
return inheritedTypes.contains(.sendable) || inheritedTypes.contains(.error)
}
Comment on lines +62 to +64
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question]

Actor protocol is not written here. Is it okay?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mock of an Actor protocol automatically becomes an actor, so it is not necessary to make the mock implementation Sendable.


func model() -> Model {
return NominalModel(identifier: key,
namespaces: entity.entityNode.namespaces,
acl: entity.entityNode.accessLevel,
declKindOfMockAnnotatedBaseType: entity.entityNode.declKind,
declKind: inheritsActorProtocol ? .actor : .class,
inheritedTypes: inheritedTypes,
attributes: attributes,
offset: entity.entityNode.offset,
metadata: entity.metadata,
initParamCandidates: initParamCandidates,
declaredInits: declaredInits,
entities: uniqueModels)
entities: uniqueModels,
requiresSendable: requiresSendable)
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/MockoloFramework/Models/TypeAliasModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ final class TypeAliasModel: Model {

func render(
context: RenderContext,
arguments: GenerationArguments = .default
arguments: GenerationArguments
) -> String? {
let addAcl = context.annotatedTypeKind == .protocol && !processed
if processed || useDescription, let modelDescription = modelDescription?.trimmingCharacters(in: .whitespacesAndNewlines) {
Expand Down
3 changes: 2 additions & 1 deletion Sources/MockoloFramework/Models/VariableModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ final class VariableModel: Model {
allowSetCallCount: arguments.allowSetCallCount,
shouldOverride: shouldOverride,
accessLevel: accessLevel,
context: context)
context: context,
arguments: arguments)
}
}
18 changes: 13 additions & 5 deletions Sources/MockoloFramework/Operations/Generator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,17 +157,25 @@ public func generate(sourceDirs: [String],
signpost_begin(name: "Write results")
log("Write the mock results and import lines to", outputFilePath, level: .info)

let needsConcurrencyHelpers = resolvedEntities.contains { $0.requiresSendable }

let imports = handleImports(pathToImportsMap: pathToImportsMap,
customImports: customImports,
customImports: customImports + (needsConcurrencyHelpers ? ["Foundation"] : []),
excludeImports: excludeImports,
testableImports: testableImports,
relevantPaths: relevantPaths)

var helpers = [String]()
if needsConcurrencyHelpers {
helpers.append(applyConcurrencyHelpersTemplate())
}

let result = try write(candidates: candidates,
header: header,
macro: macro,
imports: imports,
to: outputFilePath)
header: header,
macro: macro,
imports: imports,
helpers: helpers,
to: outputFilePath)
signpost_end(name: "Write results")
let t5 = CFAbsoluteTimeGetCurrent()
log("Took", t5-t4, level: .verbose)
Expand Down
14 changes: 11 additions & 3 deletions Sources/MockoloFramework/Operations/OutputWriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func write(candidates: [(String, Int64)],
header: String?,
macro: String?,
imports: String,
helpers: [String],
to outputFilePath: String) throws -> String {

let entities = candidates
Expand All @@ -35,11 +36,18 @@ func write(candidates: [(String, Int64)],
let headerStr = (header ?? "") + .headerDoc
var macroStart = ""
var macroEnd = ""
if let mcr = macro, !mcr.isEmpty {
macroStart = .poundIf + mcr
if let macro, !macro.isEmpty {
macroStart = .poundIf + macro
macroEnd = .poundEndIf
}
let ret = [headerStr, macroStart, imports, entities.joined(separator: "\n"), macroEnd].joined(separator: "\n\n")
let ret = [
headerStr,
macroStart,
imports,
entities.joined(separator: "\n"),
helpers.joined(separator: "\n\n"),
macroEnd,
].joined(separator: "\n\n")
let currentFileContents = try? String(contentsOfFile: outputFilePath, encoding: .utf8)
guard currentFileContents != ret else {
log("Not writing the file as content is unchanged", level: .info)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,12 @@ private func generateUniqueModels(key: String,
let uniqueModels = [mockedUniqueEntities, unmockedUniqueEntities].flatMap {$0}
.sorted(path: \.value.offset, fallback: \.key)

var mockInheritedTypes = [String]()
if inheritedTypes.contains(.sendable) {
mockInheritedTypes.append(.uncheckedSendable)
}
sidepelican marked this conversation as resolved.
Show resolved Hide resolved

let resolvedEntity = ResolvedEntity(
key: key,
entity: entity,
uniqueModels: uniqueModels,
attributes: attributes,
inheritedTypes: mockInheritedTypes,
inheritsActorProtocol: inheritedTypes.contains(.actorProtocol)
inheritedTypes: inheritedTypes.sorted()
)

return ResolvedEntityContainer(entity: resolvedEntity, paths: paths)
Expand Down
Loading