Compare commits

...

7 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
15 changed files with 531 additions and 223 deletions

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "Cuckoo"
s.version = "1.10.1"
s.version = "1.10.3"
s.summary = "Cuckoo - first boilerplate-free Swift mocking framework."
s.description = <<-DESC
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

@ -60,15 +60,15 @@ extension {{ container.parentFullyQualifiedName }} {
{{ attribute.text }}
{% endfor %}
{{ property.accessibility }}{% if container.isImplementation %} override{% endif %} var {{ property.name }}: {{ property.type }} {
get {
return cuckoo_manager.getter("{{ property.name }}",
get{% if property.isAsync %} async{% endif %}{% if property.isThrowing %} throws{% endif %} {
return {% if property.isThrowing %}try {% endif %}{% if property.isAsync %}await {% endif %}cuckoo_manager.getter{% if property.isThrowing %}Throws{% endif %}("{{ property.name }}",
superclassCall:
{% if container.isImplementation %}
super.{{ property.name }}
{% if property.isThrowing %}try {% endif %}{% if property.isAsync %}await {% endif %}super.{{ property.name }}
{% else %}
Cuckoo.MockManager.crashOnProtocolSuperclassCall()
{% endif %},
defaultCall: __defaultImplStub!.{{property.name}})
defaultCall: {% if property.isThrowing %}try {% endif %}{% if property.isAsync %}await {% endif %} __defaultImplStub!.{{property.name}})
}
{% ifnot property.isReadOnly %}
set {

View File

@ -185,11 +185,19 @@ public struct Tokenizer {
guessedType = type
}
let effects: InstanceVariable.Effects
if let bodyRange = bodyRange {
effects = parseEffects(source: source.utf8[bodyRange])
} else {
effects = .init()
}
return InstanceVariable(
name: name,
type: guessedType ?? .type("__UnknownType"),
accessibility: accessibility,
setterAccessibility: setterAccessibility,
effects: effects,
range: range!,
nameRange: nameRange!,
overriding: false,
@ -518,6 +526,34 @@ public struct Tokenizer {
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.
private func fixSourceKittenLastGenericParameterBug(_ genericParameters: [GenericParameter]) -> [GenericParameter] {
let fixedGenericParameters: [GenericParameter]

View File

@ -1,8 +1,14 @@
public struct InstanceVariable: Token, HasAccessibility, HasAttributes {
public struct Effects {
public var isThrowing = false
public var isAsync = false
}
public var name: String
public var type: WrappableType
public var accessibility: Accessibility
public var setterAccessibility: Accessibility?
public var effects: Effects
public var range: CountableRange<Int>
public var nameRange: CountableRange<Int>
public var overriding: Bool
@ -22,8 +28,10 @@ public struct InstanceVariable: Token, HasAccessibility, HasAttributes {
}
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 throwingString = effects.isThrowing ? "Throwing" : ""
return [
"name": name,
@ -31,8 +39,10 @@ public struct InstanceVariable: Token, HasAccessibility, HasAttributes {
"nonOptionalType": type.unoptionaled.sugarized,
"accessibility": accessibility.sourceName,
"isReadOnly": readOnly,
"stubType": (overriding ? "Class" : "Protocol") + "ToBeStubbed\(readOnlyString)\(optionalString)Property",
"verifyType": "Verify\(readOnlyString)\(optionalString)Property",
"isAsync": effects.isAsync,
"isThrowing": effects.isThrowing,
"stubType": (overriding ? "Class" : "Protocol") + "ToBeStubbed\(readOnlyStubString)\(optionalString)\(throwingString)Property",
"verifyType": "Verify\(readOnlyVerifyString)\(optionalString)Property",
"attributes": attributes.filter { $0.isSupported },
"hasUnavailablePlatforms": hasUnavailablePlatforms,
"unavailablePlatformsCheck": unavailablePlatformsCheck,

View File

@ -3,6 +3,8 @@ import Foundation
struct TypeGuesser {
static func guessType(from value: String) -> String? {
let value = value.trimmed
guard !value.isEmpty else { return nil }
let casting = checkCasting(from: value)
guard casting == nil else { return casting }

View File

@ -39,9 +39,9 @@ Due to the limitations mentioned above, unoverridable code structures are not su
## Requirements
Cuckoo works on the following platforms:
- **iOS 8+**
- **Mac OSX 10.9+**
- **tvOS 9+**
- **iOS 11+**
- **Mac OSX 10.13+**
- **tvOS 11+**
**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
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.
```Bash

View File

@ -310,11 +310,27 @@ extension MockManager {
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) {
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 {
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)

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))
}
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() {
var called = false
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"
}
var readOnlyPropertyWithJapaneseComment: String {
//
return "a"
}
@available(iOS 42.0, *)
var unavailableProperty: UnavailableProtocol? {
return nil
@ -53,6 +58,18 @@ class TestedClass {
) -> String
) -> () = { i in }
var asyncProperty: Int {
get async { 0 }
}
var asyncThrowsProperty: Int {
get async throws { 0 }
}
var throwsProperty: Int {
get throws { 0 }
}
func noReturn() {
}

View File

@ -34,6 +34,14 @@ protocol TestedProtocol {
) -> String
) -> () { 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 count(characters: String) -> Int

View File

@ -124,6 +124,55 @@ class StubbingTest: XCTestCase {
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, *)
func testAsyncMethods() async {

View File

@ -19,11 +19,11 @@ public enum PlatformType: String {
public var libraryDeploymentTarget: DeploymentTarget {
switch self {
case .iOS:
return .iOS(targetVersion: "8.0", devices: [.iphone, .ipad])
return .iOS(targetVersion: "11.0", devices: [.iphone, .ipad])
case .macOS:
return .macOS(targetVersion: "10.9")
return .macOS(targetVersion: "10.13")
case .tvOS:
return .tvOS(targetVersion: "9.0")
return .tvOS(targetVersion: "11.0")
}
}