Compare commits
9 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
088c021cbc | |
![]() |
475322e609 | |
![]() |
9676d7e374 | |
![]() |
75a209c3ba | |
![]() |
e29f10eac6 | |
![]() |
4148a6a7ff | |
![]() |
a71ba74df1 | |
![]() |
fa66b6a0a0 | |
![]() |
6f41152d83 |
|
@ -1,6 +1,6 @@
|
||||||
Pod::Spec.new do |s|
|
Pod::Spec.new do |s|
|
||||||
s.name = "Cuckoo"
|
s.name = "Cuckoo"
|
||||||
s.version = "1.10.0"
|
s.version = "1.10.3"
|
||||||
s.summary = "Cuckoo - first boilerplate-free Swift mocking framework."
|
s.summary = "Cuckoo - first boilerplate-free Swift mocking framework."
|
||||||
s.description = <<-DESC
|
s.description = <<-DESC
|
||||||
Cuckoo is a mocking framework with an easy to use API (inspired by Mockito).
|
Cuckoo is a mocking framework with an easy to use API (inspired by Mockito).
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,18 +2,6 @@ import Foundation
|
||||||
import Stencil
|
import Stencil
|
||||||
|
|
||||||
public struct Generator {
|
public struct Generator {
|
||||||
|
|
||||||
private static let reservedKeywordsNotAllowedAsMethodName: Set = [
|
|
||||||
// Keywords used in declarations:
|
|
||||||
"associatedtype", "class", "deinit", "enum", "extension", "fileprivate", "func", "import", "init", "inout", "internal", "let", "operator", "private", "precedencegroup", "protocol", "public", "rethrows", "static", "struct", "subscript", "typealias", "var",
|
|
||||||
// Keywords used in statements:
|
|
||||||
"break", "case", "catch", "continue", "default", "defer", "do", "else", "fallthrough", "for", "guard", "if", "in", "repeat", "return", "throw", "switch", "where", "while",
|
|
||||||
// Keywords used in expressions and types:
|
|
||||||
"Any", "as", "catch", "false", "is", "nil", "rethrows", "self", "super", "throw", "throws", "true", "try",
|
|
||||||
// Keywords used in patterns:
|
|
||||||
"_",
|
|
||||||
]
|
|
||||||
|
|
||||||
private let declarations: [Token]
|
private let declarations: [Token]
|
||||||
|
|
||||||
public init(file: FileRepresentation) {
|
public init(file: FileRepresentation) {
|
||||||
|
@ -56,10 +44,10 @@ public struct Generator {
|
||||||
guard let parameters = value as? [MethodParameter] else { return value }
|
guard let parameters = value as? [MethodParameter] else { return value }
|
||||||
return self.closeNestedClosure(for: parameters)
|
return self.closeNestedClosure(for: parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.registerFilter("escapeReservedKeywords") { (value: Any?) in
|
ext.registerFilter("escapeReservedKeywords") { (value: Any?) in
|
||||||
guard let name = value as? String else { return value }
|
guard let name = value as? String else { return value }
|
||||||
return self.escapeReservedKeywords(for: name)
|
return escapeReservedKeywords(for: name)
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.registerFilter("removeClosureArgumentNames") { (value: Any?) in
|
ext.registerFilter("removeClosureArgumentNames") { (value: Any?) in
|
||||||
|
@ -110,7 +98,13 @@ public struct Generator {
|
||||||
guard parameters.isEmpty == false else { return "let matchers: [Cuckoo.ParameterMatcher<Void>] = []" }
|
guard parameters.isEmpty == false else { return "let matchers: [Cuckoo.ParameterMatcher<Void>] = []" }
|
||||||
|
|
||||||
let tupleType = parameters.map { $0.typeWithoutAttributes }.joined(separator: ", ")
|
let tupleType = parameters.map { $0.typeWithoutAttributes }.joined(separator: ", ")
|
||||||
let matchers = parameters.enumerated().map { "wrap(matchable: \($1.name)) { $0\(parameters.count > 1 ? ".\($0)" : "") }" }.joined(separator: ", ")
|
|
||||||
|
let matchers = parameters.enumerated().map { index, parameter in
|
||||||
|
let name = escapeReservedKeywords(for: parameter.name)
|
||||||
|
return "wrap(matchable: \(name)) { $0\(parameters.count > 1 ? ".\(index)" : "") }"
|
||||||
|
}
|
||||||
|
.joined(separator: ", ")
|
||||||
|
|
||||||
return "let matchers: [Cuckoo.ParameterMatcher<(\(genericSafeType(from: tupleType)))>] = [\(matchers)]"
|
return "let matchers: [Cuckoo.ParameterMatcher<(\(genericSafeType(from: tupleType)))>] = [\(matchers)]"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,10 +145,6 @@ public struct Generator {
|
||||||
}
|
}
|
||||||
return fullString
|
return fullString
|
||||||
}
|
}
|
||||||
|
|
||||||
private func escapeReservedKeywords(for name: String) -> String {
|
|
||||||
Self.reservedKeywordsNotAllowedAsMethodName.contains(name) ? "`\(name)`" : name
|
|
||||||
}
|
|
||||||
|
|
||||||
private func removeClosureArgumentNames(for type: String) -> String {
|
private func removeClosureArgumentNames(for type: String) -> String {
|
||||||
type.replacingOccurrences(
|
type.replacingOccurrences(
|
||||||
|
|
|
@ -60,15 +60,15 @@ extension {{ container.parentFullyQualifiedName }} {
|
||||||
{{ attribute.text }}
|
{{ attribute.text }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{{ property.accessibility }}{% if container.isImplementation %} override{% endif %} var {{ property.name }}: {{ property.type }} {
|
{{ property.accessibility }}{% if container.isImplementation %} override{% endif %} var {{ property.name }}: {{ property.type }} {
|
||||||
get {
|
get{% if property.isAsync %} async{% endif %}{% if property.isThrowing %} throws{% endif %} {
|
||||||
return cuckoo_manager.getter("{{ property.name }}",
|
return {% if property.isThrowing %}try {% endif %}{% if property.isAsync %}await {% endif %}cuckoo_manager.getter{% if property.isThrowing %}Throws{% endif %}("{{ property.name }}",
|
||||||
superclassCall:
|
superclassCall:
|
||||||
{% if container.isImplementation %}
|
{% if container.isImplementation %}
|
||||||
super.{{ property.name }}
|
{% if property.isThrowing %}try {% endif %}{% if property.isAsync %}await {% endif %}super.{{ property.name }}
|
||||||
{% else %}
|
{% else %}
|
||||||
Cuckoo.MockManager.crashOnProtocolSuperclassCall()
|
Cuckoo.MockManager.crashOnProtocolSuperclassCall()
|
||||||
{% endif %},
|
{% endif %},
|
||||||
defaultCall: __defaultImplStub!.{{property.name}})
|
defaultCall: {% if property.isThrowing %}try {% endif %}{% if property.isAsync %}await {% endif %} __defaultImplStub!.{{property.name}})
|
||||||
}
|
}
|
||||||
{% ifnot property.isReadOnly %}
|
{% ifnot property.isReadOnly %}
|
||||||
set {
|
set {
|
||||||
|
|
|
@ -185,11 +185,19 @@ public struct Tokenizer {
|
||||||
guessedType = type
|
guessedType = type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let effects: InstanceVariable.Effects
|
||||||
|
if let bodyRange = bodyRange {
|
||||||
|
effects = parseEffects(source: source.utf8[bodyRange])
|
||||||
|
} else {
|
||||||
|
effects = .init()
|
||||||
|
}
|
||||||
|
|
||||||
return InstanceVariable(
|
return InstanceVariable(
|
||||||
name: name,
|
name: name,
|
||||||
type: guessedType ?? .type("__UnknownType"),
|
type: guessedType ?? .type("__UnknownType"),
|
||||||
accessibility: accessibility,
|
accessibility: accessibility,
|
||||||
setterAccessibility: setterAccessibility,
|
setterAccessibility: setterAccessibility,
|
||||||
|
effects: effects,
|
||||||
range: range!,
|
range: range!,
|
||||||
nameRange: nameRange!,
|
nameRange: nameRange!,
|
||||||
overriding: false,
|
overriding: false,
|
||||||
|
@ -518,6 +526,34 @@ public struct Tokenizer {
|
||||||
return ReturnSignature(isAsync: isAsync, throwString: throwString, returnType: returnType ?? WrappableType.type("Void"), whereConstraints: whereConstraints)
|
return ReturnSignature(isAsync: isAsync, throwString: throwString, returnType: returnType ?? WrappableType.type("Void"), whereConstraints: whereConstraints)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func parseEffects(source: String) -> InstanceVariable.Effects {
|
||||||
|
var effects = InstanceVariable.Effects()
|
||||||
|
|
||||||
|
let trimmed = source.drop(while: { $0.isWhitespace })
|
||||||
|
guard trimmed.hasPrefix("get") else { return effects }
|
||||||
|
|
||||||
|
let afterGet = trimmed.dropFirst("get".count).drop(while: { $0.isWhitespace })
|
||||||
|
var index = afterGet.startIndex
|
||||||
|
parseLoop: while index != afterGet.endIndex {
|
||||||
|
let character = afterGet[index]
|
||||||
|
switch character {
|
||||||
|
case "a":
|
||||||
|
effects.isAsync = true
|
||||||
|
index = source.index(index, offsetBy: "async".count)
|
||||||
|
case "t":
|
||||||
|
effects.isThrowing = true
|
||||||
|
index = source.index(index, offsetBy: "throws".count)
|
||||||
|
case let c where c.isWhitespace:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break parseLoop
|
||||||
|
}
|
||||||
|
index = source.index(after: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
return effects
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: Remove when SourceKitten fixes the off-by-one error that includes the ending `>` in the last inherited type.
|
// FIXME: Remove when SourceKitten fixes the off-by-one error that includes the ending `>` in the last inherited type.
|
||||||
private func fixSourceKittenLastGenericParameterBug(_ genericParameters: [GenericParameter]) -> [GenericParameter] {
|
private func fixSourceKittenLastGenericParameterBug(_ genericParameters: [GenericParameter]) -> [GenericParameter] {
|
||||||
let fixedGenericParameters: [GenericParameter]
|
let fixedGenericParameters: [GenericParameter]
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
public struct InstanceVariable: Token, HasAccessibility, HasAttributes {
|
public struct InstanceVariable: Token, HasAccessibility, HasAttributes {
|
||||||
|
public struct Effects {
|
||||||
|
public var isThrowing = false
|
||||||
|
public var isAsync = false
|
||||||
|
}
|
||||||
|
|
||||||
public var name: String
|
public var name: String
|
||||||
public var type: WrappableType
|
public var type: WrappableType
|
||||||
public var accessibility: Accessibility
|
public var accessibility: Accessibility
|
||||||
public var setterAccessibility: Accessibility?
|
public var setterAccessibility: Accessibility?
|
||||||
|
public var effects: Effects
|
||||||
public var range: CountableRange<Int>
|
public var range: CountableRange<Int>
|
||||||
public var nameRange: CountableRange<Int>
|
public var nameRange: CountableRange<Int>
|
||||||
public var overriding: Bool
|
public var overriding: Bool
|
||||||
|
@ -22,8 +28,10 @@ public struct InstanceVariable: Token, HasAccessibility, HasAttributes {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func serialize() -> [String : Any] {
|
public func serialize() -> [String : Any] {
|
||||||
let readOnlyString = readOnly ? "ReadOnly" : ""
|
let readOnlyVerifyString = readOnly ? "ReadOnly" : ""
|
||||||
|
let readOnlyStubString = effects.isThrowing ? "" : readOnlyVerifyString
|
||||||
let optionalString = type.isOptional && !readOnly ? "Optional" : ""
|
let optionalString = type.isOptional && !readOnly ? "Optional" : ""
|
||||||
|
let throwingString = effects.isThrowing ? "Throwing" : ""
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"name": name,
|
"name": name,
|
||||||
|
@ -31,8 +39,10 @@ public struct InstanceVariable: Token, HasAccessibility, HasAttributes {
|
||||||
"nonOptionalType": type.unoptionaled.sugarized,
|
"nonOptionalType": type.unoptionaled.sugarized,
|
||||||
"accessibility": accessibility.sourceName,
|
"accessibility": accessibility.sourceName,
|
||||||
"isReadOnly": readOnly,
|
"isReadOnly": readOnly,
|
||||||
"stubType": (overriding ? "Class" : "Protocol") + "ToBeStubbed\(readOnlyString)\(optionalString)Property",
|
"isAsync": effects.isAsync,
|
||||||
"verifyType": "Verify\(readOnlyString)\(optionalString)Property",
|
"isThrowing": effects.isThrowing,
|
||||||
|
"stubType": (overriding ? "Class" : "Protocol") + "ToBeStubbed\(readOnlyStubString)\(optionalString)\(throwingString)Property",
|
||||||
|
"verifyType": "Verify\(readOnlyVerifyString)\(optionalString)Property",
|
||||||
"attributes": attributes.filter { $0.isSupported },
|
"attributes": attributes.filter { $0.isSupported },
|
||||||
"hasUnavailablePlatforms": hasUnavailablePlatforms,
|
"hasUnavailablePlatforms": hasUnavailablePlatforms,
|
||||||
"unavailablePlatformsCheck": unavailablePlatformsCheck,
|
"unavailablePlatformsCheck": unavailablePlatformsCheck,
|
||||||
|
|
|
@ -38,11 +38,11 @@ public extension Method {
|
||||||
.map { $0 + ": " + $1 }
|
.map { $0 + ": " + $1 }
|
||||||
.joined(separator: ", ") + lastNamePart + returnSignatureString
|
.joined(separator: ", ") + lastNamePart + returnSignatureString
|
||||||
}
|
}
|
||||||
|
|
||||||
var isAsync: Bool {
|
var isAsync: Bool {
|
||||||
return returnSignature.isAsync
|
return returnSignature.isAsync
|
||||||
}
|
}
|
||||||
|
|
||||||
var isThrowing: Bool {
|
var isThrowing: Bool {
|
||||||
guard let throwType = returnSignature.throwType else { return false }
|
guard let throwType = returnSignature.throwType else { return false }
|
||||||
return throwType.isThrowing || throwType.isRethrowing
|
return throwType.isThrowing || throwType.isRethrowing
|
||||||
|
@ -67,7 +67,9 @@ public extension Method {
|
||||||
|
|
||||||
func serialize() -> [String : Any] {
|
func serialize() -> [String : Any] {
|
||||||
let call = parameters.map {
|
let call = parameters.map {
|
||||||
let referencedName = "\($0.isInout ? "&" : "")\($0.name)\($0.isAutoClosure ? "()" : "")"
|
let name = escapeReservedKeywords(for: $0.name)
|
||||||
|
let referencedName = "\($0.isInout ? "&" : "")\(name)\($0.isAutoClosure ? "()" : "")"
|
||||||
|
|
||||||
if let label = $0.label {
|
if let label = $0.label {
|
||||||
return "\(label): \(referencedName)"
|
return "\(label): \(referencedName)"
|
||||||
} else {
|
} else {
|
||||||
|
@ -95,7 +97,7 @@ public extension Method {
|
||||||
}
|
}
|
||||||
return "{ \(parameterSignature)\(returnSignature) in fatalError(\"This is a stub! It's not supposed to be called!\") }"
|
return "{ \(parameterSignature)\(returnSignature) in fatalError(\"This is a stub! It's not supposed to be called!\") }"
|
||||||
} else {
|
} else {
|
||||||
return parameter.name
|
return escapeReservedKeywords(for: parameter.name)
|
||||||
}
|
}
|
||||||
}.joined(separator: ", ")
|
}.joined(separator: ", ")
|
||||||
|
|
||||||
|
@ -108,7 +110,7 @@ public extension Method {
|
||||||
"accessibility": accessibility.sourceName,
|
"accessibility": accessibility.sourceName,
|
||||||
"returnSignature": returnSignature.description,
|
"returnSignature": returnSignature.description,
|
||||||
"parameters": parameters,
|
"parameters": parameters,
|
||||||
"parameterNames": parameters.map { $0.name }.joined(separator: ", "),
|
"parameterNames": parameters.map { escapeReservedKeywords(for: $0.name) }.joined(separator: ", "),
|
||||||
"escapingParameterNames": escapingParameterNames,
|
"escapingParameterNames": escapingParameterNames,
|
||||||
"isInit": isInit,
|
"isInit": isInit,
|
||||||
"returnType": returnType.explicitOptionalOnly.sugarized,
|
"returnType": returnType.explicitOptionalOnly.sugarized,
|
||||||
|
|
|
@ -3,6 +3,8 @@ import Foundation
|
||||||
struct TypeGuesser {
|
struct TypeGuesser {
|
||||||
static func guessType(from value: String) -> String? {
|
static func guessType(from value: String) -> String? {
|
||||||
let value = value.trimmed
|
let value = value.trimmed
|
||||||
|
guard !value.isEmpty else { return nil }
|
||||||
|
|
||||||
let casting = checkCasting(from: value)
|
let casting = checkCasting(from: value)
|
||||||
guard casting == nil else { return casting }
|
guard casting == nil else { return casting }
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,26 @@ extension Sequence {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reserved keywords that are not allowed as function names, function parameters, or local variables, etc.
|
||||||
|
fileprivate let reservedKeywordsNotAllowed: Set = [
|
||||||
|
// Keywords used in declarations:
|
||||||
|
"associatedtype", "class", "deinit", "enum", "extension", "fileprivate", "func", "import", "init", "inout",
|
||||||
|
"internal", "let", "operator", "private", "precedencegroup", "protocol", "public", "rethrows", "static",
|
||||||
|
"struct", "subscript", "typealias", "var",
|
||||||
|
// Keywords used in statements:
|
||||||
|
"break", "case", "catch", "continue", "default", "defer", "do", "else", "fallthrough", "for", "guard", "if", "in",
|
||||||
|
"repeat", "return", "throw", "switch", "where", "while",
|
||||||
|
// Keywords used in expressions and types:
|
||||||
|
"Any", "as", "catch", "false", "is", "nil", "rethrows", "self", "super", "throw", "throws", "true", "try",
|
||||||
|
// Keywords used in patterns:
|
||||||
|
"_",
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Utility function for escaping reserved keywords for a symbol name.
|
||||||
|
internal func escapeReservedKeywords(for name: String) -> String {
|
||||||
|
reservedKeywordsNotAllowed.contains(name) ? "`\(name)`" : name
|
||||||
|
}
|
||||||
|
|
||||||
internal func extractRange(from dictionary: [String: SourceKitRepresentable], offset: Key, length: Key) -> CountableRange<Int>? {
|
internal func extractRange(from dictionary: [String: SourceKitRepresentable], offset: Key, length: Key) -> CountableRange<Int>? {
|
||||||
guard let offset = (dictionary[offset.rawValue] as? Int64).map(Int.init),
|
guard let offset = (dictionary[offset.rawValue] as? Int64).map(Int.init),
|
||||||
let length = (dictionary[length.rawValue] as? Int64).map(Int.init) else { return nil }
|
let length = (dictionary[length.rawValue] as? Int64).map(Int.init) else { return nil }
|
||||||
|
|
10
README.md
10
README.md
|
@ -39,9 +39,9 @@ Due to the limitations mentioned above, unoverridable code structures are not su
|
||||||
## Requirements
|
## Requirements
|
||||||
Cuckoo works on the following platforms:
|
Cuckoo works on the following platforms:
|
||||||
|
|
||||||
- **iOS 8+**
|
- **iOS 11+**
|
||||||
- **Mac OSX 10.9+**
|
- **Mac OSX 10.13+**
|
||||||
- **tvOS 9+**
|
- **tvOS 11+**
|
||||||
|
|
||||||
**watchOS** support is not yet possible due to missing XCTest library.
|
**watchOS** support is not yet possible due to missing XCTest library.
|
||||||
|
|
||||||
|
@ -102,7 +102,9 @@ Note: All paths in the Run script must be absolute. Variable `PROJECT_DIR` autom
|
||||||
|
|
||||||
1. In Xcode, navigate in menu: File > Swift Packages > Add Package Dependency
|
1. In Xcode, navigate in menu: File > Swift Packages > Add Package Dependency
|
||||||
2. Add `https://github.com/Brightify/Cuckoo.git`
|
2. Add `https://github.com/Brightify/Cuckoo.git`
|
||||||
3. Select "Up to Next Major" with `1.9.1`
|
3. For the Dependency Rule, Select "Up to Next Major" with `1.9.1`. Click Add Package.
|
||||||
|
4. On the 'Choose Package Products for Cuckoo' dialog, under 'Add to Target', please ensure you select your Test target as it will not compile on the app target.
|
||||||
|
5. Click Add Package.
|
||||||
|
|
||||||
Cuckoo relies on a script that is currently not downloadable using SPM. However, for convenience, you can copy this line into the terminal to download the latest `run` script. If the `run` script changes in the future, you'll need to execute this command again.
|
Cuckoo relies on a script that is currently not downloadable using SPM. However, for convenience, you can copy this line into the terminal to download the latest `run` script. If the `run` script changes in the future, you'll need to execute this command again.
|
||||||
```Bash
|
```Bash
|
||||||
|
|
|
@ -310,11 +310,27 @@ extension MockManager {
|
||||||
return call(getterName(property), parameters: Void(), escapingParameters: Void(), superclassCall: superclassCall(), defaultCall: defaultCall())
|
return call(getterName(property), parameters: Void(), escapingParameters: Void(), superclassCall: superclassCall(), defaultCall: defaultCall())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||||
|
public func getter<T>(_ property: String, superclassCall: @autoclosure () async -> T, defaultCall: @autoclosure () async -> T) async -> T {
|
||||||
|
return await call(getterName(property), parameters: Void(), escapingParameters: Void(), superclassCall: await superclassCall(), defaultCall: await defaultCall())
|
||||||
|
}
|
||||||
|
|
||||||
public func setter<T>(_ property: String, value: T, superclassCall: @autoclosure () -> Void, defaultCall: @autoclosure () -> Void) {
|
public func setter<T>(_ property: String, value: T, superclassCall: @autoclosure () -> Void, defaultCall: @autoclosure () -> Void) {
|
||||||
return call(setterName(property), parameters: value, escapingParameters: value, superclassCall: superclassCall(), defaultCall: defaultCall())
|
return call(setterName(property), parameters: value, escapingParameters: value, superclassCall: superclassCall(), defaultCall: defaultCall())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension MockManager {
|
||||||
|
public func getterThrows<T>(_ property: String, superclassCall: @autoclosure () throws -> T, defaultCall: @autoclosure () throws -> T) throws -> T {
|
||||||
|
return try callThrows(getterName(property), parameters: Void(), escapingParameters: Void(), superclassCall: try superclassCall(), defaultCall: try defaultCall())
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||||
|
public func getterThrows<T>(_ property: String, superclassCall: @autoclosure () async throws -> T, defaultCall: @autoclosure () async throws -> T) async throws -> T {
|
||||||
|
return try await callThrows(getterName(property), parameters: Void(), escapingParameters: Void(), superclassCall: try await superclassCall(), defaultCall: try await defaultCall())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension MockManager {
|
extension MockManager {
|
||||||
public func call<IN, OUT>(_ method: String, parameters: IN, escapingParameters: IN, superclassCall: @autoclosure () -> OUT, defaultCall: @autoclosure () -> OUT) -> OUT {
|
public func call<IN, OUT>(_ method: String, parameters: IN, escapingParameters: IN, superclassCall: @autoclosure () -> OUT, defaultCall: @autoclosure () -> OUT) -> OUT {
|
||||||
return callInternal(method, parameters: parameters, escapingParameters: escapingParameters, superclassCall: superclassCall, defaultCall: defaultCall)
|
return callInternal(method, parameters: parameters, escapingParameters: escapingParameters, superclassCall: superclassCall, defaultCall: defaultCall)
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
//
|
||||||
|
// ToBeStubbedThrowingProperty.swift
|
||||||
|
// Cuckoo
|
||||||
|
//
|
||||||
|
// Created by Kabir Oberai on 2023-03-27.
|
||||||
|
//
|
||||||
|
|
||||||
|
public protocol ToBeStubbedThrowingProperty {
|
||||||
|
associatedtype GetterType: StubThrowingFunction
|
||||||
|
|
||||||
|
var get: GetterType { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ProtocolToBeStubbedThrowingProperty<MOCK: ProtocolMock, T>: ToBeStubbedThrowingProperty {
|
||||||
|
private let manager: MockManager
|
||||||
|
private let name: String
|
||||||
|
|
||||||
|
public var get: ProtocolStubThrowingFunction<Void, T> {
|
||||||
|
return ProtocolStubThrowingFunction(stub:
|
||||||
|
manager.createStub(for: MOCK.self, method: getterName(name), parameterMatchers: []))
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(manager: MockManager, name: String) {
|
||||||
|
self.manager = manager
|
||||||
|
self.name = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ClassToBeStubbedThrowingProperty<MOCK: ClassMock, T>: ToBeStubbedThrowingProperty {
|
||||||
|
private let manager: MockManager
|
||||||
|
private let name: String
|
||||||
|
|
||||||
|
public var get: ClassStubThrowingFunction<Void, T> {
|
||||||
|
return ClassStubThrowingFunction(stub:
|
||||||
|
manager.createStub(for: MOCK.self, method: getterName(name), parameterMatchers: []))
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(manager: MockManager, name: String) {
|
||||||
|
self.manager = manager
|
||||||
|
self.name = name
|
||||||
|
}
|
||||||
|
}
|
|
@ -58,6 +58,61 @@ class ProtocolTest: XCTestCase {
|
||||||
verify(mock).optionalProperty.set(equal(to: 0))
|
verify(mock).optionalProperty.set(equal(to: 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testThrowingProperty() {
|
||||||
|
stub(mock) { mock in
|
||||||
|
when(mock.throwsProperty.get).thenReturn(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(try mock.throwsProperty, 5)
|
||||||
|
verify(mock).throwsProperty.get()
|
||||||
|
|
||||||
|
clearInvocations(mock)
|
||||||
|
|
||||||
|
stub(mock) { mock in
|
||||||
|
when(mock.throwsProperty.get).thenThrow(TestError.unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertThrowsError(try mock.throwsProperty)
|
||||||
|
|
||||||
|
verify(mock).throwsProperty.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAsyncProperty() async {
|
||||||
|
stub(mock) { mock in
|
||||||
|
when(mock.asyncProperty.get).thenReturn(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await mock.asyncProperty
|
||||||
|
XCTAssertEqual(result, 5)
|
||||||
|
verify(mock).asyncProperty.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAsyncThrowingProperty() async {
|
||||||
|
stub(mock) { mock in
|
||||||
|
when(mock.asyncThrowsProperty.get).thenReturn(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = try! await mock.asyncThrowsProperty
|
||||||
|
XCTAssertEqual(result, 5)
|
||||||
|
verify(mock).asyncThrowsProperty.get()
|
||||||
|
|
||||||
|
clearInvocations(mock)
|
||||||
|
|
||||||
|
stub(mock) { mock in
|
||||||
|
when(mock.asyncThrowsProperty.get).thenThrow(TestError.unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
var threw = false
|
||||||
|
do {
|
||||||
|
_ = try await mock.asyncThrowsProperty
|
||||||
|
} catch {
|
||||||
|
threw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(threw)
|
||||||
|
verify(mock).asyncThrowsProperty.get()
|
||||||
|
}
|
||||||
|
|
||||||
func testNoReturn() {
|
func testNoReturn() {
|
||||||
var called = false
|
var called = false
|
||||||
stub(mock) { mock in
|
stub(mock) { mock in
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
//
|
||||||
|
// PropertyWrappers.swift
|
||||||
|
// Cuckoo
|
||||||
|
//
|
||||||
|
// Created by Kabir Oberai on 2023-03-28.
|
||||||
|
//
|
||||||
|
|
||||||
|
// wrappers without annotations aren't supported but their
|
||||||
|
// existence shouldn't cause the generator to crash
|
||||||
|
|
||||||
|
@propertyWrapper
|
||||||
|
struct DoublingWrapper {
|
||||||
|
private var backing: Int
|
||||||
|
var wrappedValue: Int {
|
||||||
|
get { backing * 2 }
|
||||||
|
set { backing = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
init(wrappedValue: Int) {
|
||||||
|
self.backing = wrappedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.backing = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Wrappers {
|
||||||
|
@DoublingWrapper var base
|
||||||
|
@DoublingWrapper var withValue = 2
|
||||||
|
@DoublingWrapper var withAnnotation: Int
|
||||||
|
@DoublingWrapper var withAnnotationAndValue: Int = 2
|
||||||
|
}
|
|
@ -27,6 +27,11 @@ class TestedClass {
|
||||||
return "a"
|
return "a"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var readOnlyPropertyWithJapaneseComment: String {
|
||||||
|
// 日本語コメント
|
||||||
|
return "a"
|
||||||
|
}
|
||||||
|
|
||||||
@available(iOS 42.0, *)
|
@available(iOS 42.0, *)
|
||||||
var unavailableProperty: UnavailableProtocol? {
|
var unavailableProperty: UnavailableProtocol? {
|
||||||
return nil
|
return nil
|
||||||
|
@ -53,6 +58,18 @@ class TestedClass {
|
||||||
) -> String
|
) -> String
|
||||||
) -> () = { i in }
|
) -> () = { i in }
|
||||||
|
|
||||||
|
var asyncProperty: Int {
|
||||||
|
get async { 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
var asyncThrowsProperty: Int {
|
||||||
|
get async throws { 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
var throwsProperty: Int {
|
||||||
|
get throws { 0 }
|
||||||
|
}
|
||||||
|
|
||||||
func noReturn() {
|
func noReturn() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,6 +165,10 @@ class TestedClass {
|
||||||
func withLabelAndUnderscore(labelA a: String, _ b: String) {
|
func withLabelAndUnderscore(labelA a: String, _ b: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func withReservedKeywords(for: String, in: String) -> String {
|
||||||
|
"hello"
|
||||||
|
}
|
||||||
|
|
||||||
func callingCountCharactersMethodWithHello() -> Int {
|
func callingCountCharactersMethodWithHello() -> Int {
|
||||||
return count(characters: "Hello")
|
return count(characters: "Hello")
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,14 @@ protocol TestedProtocol {
|
||||||
) -> String
|
) -> String
|
||||||
) -> () { get set }
|
) -> () { get set }
|
||||||
|
|
||||||
|
var throwsProperty: Int { get throws }
|
||||||
|
|
||||||
|
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||||
|
var asyncProperty: Int { get async }
|
||||||
|
|
||||||
|
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||||
|
var asyncThrowsProperty: Int { get async throws }
|
||||||
|
|
||||||
func noReturn()
|
func noReturn()
|
||||||
|
|
||||||
func count(characters: String) -> Int
|
func count(characters: String) -> Int
|
||||||
|
@ -56,6 +64,14 @@ protocol TestedProtocol {
|
||||||
|
|
||||||
func withLabelAndUnderscore(labelA a: String, _ b: String)
|
func withLabelAndUnderscore(labelA a: String, _ b: String)
|
||||||
|
|
||||||
|
/// In this example `for` and `in` are not actually used in a way that conflicts with reserved keywords because
|
||||||
|
/// conforming types will typically use `for` and `in` as an argument label for parameter with a different name,
|
||||||
|
/// thus avoiding the usage of a reserved keyword in the body of the function.
|
||||||
|
///
|
||||||
|
/// The problem was with the generated mock code, which was in turn using these in the body without escaping them,
|
||||||
|
/// causing the generated mock code to fail to compile.
|
||||||
|
func withReservedKeywords(for: String, in: String) -> String
|
||||||
|
|
||||||
func withNamedTuple(tuple: (a: String, b: String)) -> Int
|
func withNamedTuple(tuple: (a: String, b: String)) -> Int
|
||||||
|
|
||||||
func withImplicitlyUnwrappedOptional(i: Int!) -> String
|
func withImplicitlyUnwrappedOptional(i: Int!) -> String
|
||||||
|
|
|
@ -124,6 +124,55 @@ class StubbingTest: XCTestCase {
|
||||||
|
|
||||||
XCTAssertEqual(mock.protocolMethod(), "a1")
|
XCTAssertEqual(mock.protocolMethod(), "a1")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||||
|
func testEffectfulProps() async {
|
||||||
|
let mock = MockTestedSubSubClass()
|
||||||
|
|
||||||
|
XCTAssertNotNil(mock)
|
||||||
|
|
||||||
|
stub(mock) { stub in
|
||||||
|
when(stub.asyncProperty.get).thenReturn(5)
|
||||||
|
when(stub.throwsProperty.get).thenReturn(6)
|
||||||
|
when(stub.asyncThrowsProperty.get).thenReturn(7)
|
||||||
|
}
|
||||||
|
|
||||||
|
let resultAsync = await mock.asyncProperty
|
||||||
|
XCTAssertEqual(resultAsync, 5)
|
||||||
|
|
||||||
|
let resultThrows = try! mock.throwsProperty
|
||||||
|
XCTAssertEqual(resultThrows, 6)
|
||||||
|
|
||||||
|
let resultAsyncThrows = try! await mock.asyncThrowsProperty
|
||||||
|
XCTAssertEqual(resultAsyncThrows, 7)
|
||||||
|
|
||||||
|
verify(mock, times(1)).asyncProperty.get()
|
||||||
|
verify(mock, times(1)).throwsProperty.get()
|
||||||
|
verify(mock, times(1)).asyncThrowsProperty.get()
|
||||||
|
|
||||||
|
enum TestError: Error { case fromThrows, fromAsyncThrows }
|
||||||
|
|
||||||
|
stub(mock) { stub in
|
||||||
|
when(stub.throwsProperty.get).thenThrow(TestError.fromThrows)
|
||||||
|
when(stub.asyncThrowsProperty.get).thenThrow(TestError.fromAsyncThrows)
|
||||||
|
}
|
||||||
|
|
||||||
|
var caughtFromThrows = false
|
||||||
|
do {
|
||||||
|
_ = try mock.throwsProperty
|
||||||
|
} catch TestError.fromThrows {
|
||||||
|
caughtFromThrows = true
|
||||||
|
} catch {}
|
||||||
|
XCTAssert(caughtFromThrows)
|
||||||
|
|
||||||
|
var caughtFromAsyncThrows = false
|
||||||
|
do {
|
||||||
|
_ = try await mock.asyncThrowsProperty
|
||||||
|
} catch TestError.fromAsyncThrows {
|
||||||
|
caughtFromAsyncThrows = true
|
||||||
|
} catch {}
|
||||||
|
XCTAssert(caughtFromAsyncThrows)
|
||||||
|
}
|
||||||
|
|
||||||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
||||||
func testAsyncMethods() async {
|
func testAsyncMethods() async {
|
||||||
|
|
|
@ -19,11 +19,11 @@ public enum PlatformType: String {
|
||||||
public var libraryDeploymentTarget: DeploymentTarget {
|
public var libraryDeploymentTarget: DeploymentTarget {
|
||||||
switch self {
|
switch self {
|
||||||
case .iOS:
|
case .iOS:
|
||||||
return .iOS(targetVersion: "8.0", devices: [.iphone, .ipad])
|
return .iOS(targetVersion: "11.0", devices: [.iphone, .ipad])
|
||||||
case .macOS:
|
case .macOS:
|
||||||
return .macOS(targetVersion: "10.9")
|
return .macOS(targetVersion: "10.13")
|
||||||
case .tvOS:
|
case .tvOS:
|
||||||
return .tvOS(targetVersion: "9.0")
|
return .tvOS(targetVersion: "11.0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue