Compare commits

...

9 Commits

Author SHA1 Message Date
Kelvin Harron 088c021cbc Update README.md for SPM instructions
specified the target to select as part of the SPM installation process
2023-06-03 14:39:07 +02:00
Matyáš Kříž 475322e609 Bump version. 2023-04-17 17:30:01 +02:00
nanashiki 9676d7e374 Up DEPLOYMENT_TARGETs 2023-04-17 17:27:11 +02:00
nanashiki 75a209c3ba Fix String index is out of bounds crash 2023-04-17 17:24:25 +02:00
Matyáš Kříž e29f10eac6 Bump version. 2023-04-11 23:19:48 +02:00
Kabir Oberai 4148a6a7ff Support effectful properties 2023-04-11 23:17:51 +02:00
Kabir Oberai a71ba74df1 Fix generator crashing for property wrappers with empty type. 2023-04-01 10:19:13 +02:00
Matyáš Kříž fa66b6a0a0 Bump version. 2023-03-26 14:22:04 +02:00
Seth Deckard 6f41152d83
Fix Generator to handle reserved keywords
Allows for reserved keywords to be used as argument labels / parameters
in mocked types.

Fixes: https://github.com/Brightify/Cuckoo/issues/452

- Moves escaping util function to Utils and makes it internal since it
  now needs to be used outside of `Generator`.
- Uses escaping function in areas affected by the bug.
- Renames set of reserved names to reflect the larger scope (no longer
  used for just function names).
- Some minor clean-up to style and formatting on areas touched.

Example of the problem:

Generate a mock for a protocol with the function:

```
func escape(for: String) -> String
```

This would result in `for` as a function parameter used within the body
of the function for the mock and verification and stubbing proxies,
which would not compile. This change escapes keywords like this so the
generated code will compile.

See `TestedProtocol.withReservedKeywords` for a test example. An example
was added to `TestedClass` as well, but the real motivation for this is
protocols since conforming types can and often do use a different
parameter name so the argument label like `for` would never be used
within the body of the function.
2023-03-23 08:31:12 -04:00
18 changed files with 579 additions and 247 deletions

View File

@ -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

View File

@ -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(

View File

@ -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 {

View File

@ -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]

View File

@ -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,

View File

@ -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,

View File

@ -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 }

View File

@ -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 }

View File

@ -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

View File

@ -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)

View File

@ -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
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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")
} }

View File

@ -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

View File

@ -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 {

View File

@ -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")
} }
} }