From 4510c13266cff20e92a15249d45cf0cce8b2b499 Mon Sep 17 00:00:00 2001 From: Vitalii Malakhovskyi Date: Tue, 15 May 2018 13:39:31 +0300 Subject: [PATCH 1/3] added stencil template files for mocking and randoms generation --- .../Templates/AutoMockable.stencil | 54 +++++++++++++++++++ .../Templates/AutoRandom.stencil | 51 ++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 Example/RoboKittenTests/Templates/AutoMockable.stencil create mode 100644 Example/RoboKittenTests/Templates/AutoRandom.stencil diff --git a/Example/RoboKittenTests/Templates/AutoMockable.stencil b/Example/RoboKittenTests/Templates/AutoMockable.stencil new file mode 100644 index 0000000..c4c4d07 --- /dev/null +++ b/Example/RoboKittenTests/Templates/AutoMockable.stencil @@ -0,0 +1,54 @@ +import Foundation +import SwiftyMock +import Result +@testable import Betterme + +{% macro parseFunctionType method %}{% if method.parameters.count == 0 %}FunctionVoidCall{% else %}FunctionCall{% endif %}{% endmacro %} + +{% macro defineReturn method %}{% if not method.returnTypeName.isVoid %}return {% endif %}{% endmacro %} +{% macro parseArguments method %}{% if method.parameters.count == 1 %}{{ method.parameters.first.typeName }}{% else %}({% for param in method.parameters %}{{ param.name }}: {{ param.typeName|replace:"@escaping ","" }}{% if not forloop.last %}, {% endif %}{% endfor %}){% endif %}{% endmacro %} + +{% macro parseParameterName parameter %}{{ parameter.typeAttributes }}{% endmacro %} + +{% macro parseArgumentsInMethod method %}{% if method.parameters.count > 0 %}, argument: {% if method.parameters.count == 1 %}{{ method.parameters.first.name }}{% else %}({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% if not forloop.last %}, {% endif %}{% endfor %}){% endif %}{% endif %}{% endmacro %} + +{% macro parseReturn method %}{% if not method.returnTypeName.isVoid %}{{ method.returnTypeName }}{% else %}(){% endif %}{% endmacro %} + +{% macro mockOptionalVariable variable %} + var {{ variable.name }}: {{ variable.typeName }} +{% endmacro %} + +{% macro mockNonOptionalArrayOrDictionaryVariable variable %} + var {{ variable.name }}: {{ variable.typeName }} = {% if variable.isArray %}[]{% elif variable.isDictionary %}[:]{% endif %} +{% endmacro %} + +{% macro mockNonOptionalVariable variable %} + {% if variable.type.implements.Random or variable.type.implements.AutoRandom %} + var {{ variable.name }} = {{ variable.typeName }}.random() + {% else %} + var {{ variable.name }}: {{ variable.typeName }} { + get { return {% call underlyingMockedVariableName variable %} } + set(value) { {% call underlyingMockedVariableName variable %} = value } + } + var {% call underlyingMockedVariableName variable %}: {{ variable.typeName }}! + {% endif %} +{% endmacro %} + +{% macro underlyingMockedVariableName variable %}underlying{{ variable.name|upperFirstLetter }}{% endmacro %} + +{% macro parseDefault method %}{% if method.returnTypeName.isVoid %}(){% else %}{% if method.returnTypeName|contains:"Signal" %}.empty{% elif method.returnType.implements.Random or method.returnType.implements.AutoRandom %}{{ method.returnTypeName }}.random(){% else %}Fake{{ method.returnTypeName }}(){%endif%}{% endif %}{% endmacro %} + +{% for type in types.protocols where type.based.AutoMock %} +final class Fake{{ type.name }}: {{ type.name }} { + {% for variable in type.allVariables|!definedInExtension %} + {% if variable.isOptional %}{% call mockOptionalVariable variable %}{% elif variable.isArray or variable.isDictionary %}{% call mockNonOptionalArrayOrDictionaryVariable variable %}{% else %}{% call mockNonOptionalVariable variable %}{% endif %} + {% endfor %} + {% for method in type.allMethods|!definedInExtension %} + + let {{ method.shortName }}Call = {% call parseFunctionType method %}<{% if method.parameters.count > 0 %}{% call parseArguments method %}, {% endif %}{% call parseReturn method %}>() + func {{ method.name }}{% if method.throws %} throws{% endif %}{% if not method.returnTypeName.isVoid %} -> {{ method.returnTypeName }}{% endif %} { + {% call defineReturn method %}stubCall({{ method.shortName }}Call{% call parseArgumentsInMethod method %}, defaultValue: {% call parseDefault method %}) + } + {% endfor %} +} +{% endfor %} diff --git a/Example/RoboKittenTests/Templates/AutoRandom.stencil b/Example/RoboKittenTests/Templates/AutoRandom.stencil new file mode 100644 index 0000000..f59561f --- /dev/null +++ b/Example/RoboKittenTests/Templates/AutoRandom.stencil @@ -0,0 +1,51 @@ +import Foundation +@testable import Betterme + +{% macro defineDeclaration var %}{{ var.name }}:{% if var.isClosure and not var.isOptional %} @escaping{% endif %} {{ var.typeName }} = {% call defineRandom var %}{% endmacro %} +{% macro defineRandom var %}{% if var.isClosure %}{}{% else %}.random(){% endif %}{% endmacro %} + +{% for type in types.implementing.AutoRandom|!enum %} + +extension {{ type.name }}: Random { + static func random() -> {{ type.name }} { + return .restrictedRandom() + } + + static func restrictedRandom( + {% for var in type.variables %} + {% if not var.name == "hashValue" %} + {% if forloop.last %} + {% call defineDeclaration var %} + {% else %} + {% call defineDeclaration var %}, + {% endif %} + {% endif %} + {% endfor %} + ) -> {{ type.name }} { + return {{ type.name }}( + {% for var in type.variables %} + {% if not var.name == "hashValue" %} + {% if forloop.last %} + {{ var.name }}: {{ var.name }} + {% else %} + {{ var.name }}: {{ var.name }}, + {% endif %} + {% endif %} + {% endfor %} + ) + } +} +{% endfor %} + +{% for type in types.implementing.AutoRandom|enum %} +extension {{ type.name }}: Random { + static func random() -> {{ type.name }} { + switch arc4random_uniform({{ type.cases.count }}) { + {% for case in type.cases %} + case {{ forloop.counter0 }}: return .{{ case.name }}{% if case.hasAssociatedValue %}({% for associated in case.associatedValues %}{% if associated.externalName %}{{ associated.externalName }}: {% endif %}.random(){% endfor %}){% endif %} + {% endfor %} + default: return {{ .type.cases.first.name }} + } + } +} +{% endfor %} From e8b5602bbddf3fd2f09ccb108288a1e4894a2c23 Mon Sep 17 00:00:00 2001 From: Vitalii Malakhovskyi Date: Thu, 17 May 2018 11:43:23 +0300 Subject: [PATCH 2/3] added random implementation for basic swift types --- Example/RoboKittenTests/Common/Random.swift | 144 ++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 Example/RoboKittenTests/Common/Random.swift diff --git a/Example/RoboKittenTests/Common/Random.swift b/Example/RoboKittenTests/Common/Random.swift new file mode 100644 index 0000000..a09907a --- /dev/null +++ b/Example/RoboKittenTests/Common/Random.swift @@ -0,0 +1,144 @@ +import Foundation +import UIKit + +protocol Random { + static func random() -> Self +} + +extension Array: Random { + private static func randomElement() -> Element? { + guard Element.self is Random.Type else { + return nil + } + return (Element.self as? Random.Type)?.random() as? Element + } + + static func random() -> [Element] { + return (0...Int(arc4random() % 3)) + .map { _ in randomElement() } + .compactMap { $0 } + } +} + +extension Optional: Random { + private static func randomElement() -> Wrapped? { + guard Wrapped.self is Random.Type else { + return nil + } + return (Wrapped.self as? Random.Type)?.random() as? Wrapped + } + + static func random() -> Wrapped? { + return Int(arc4random() % 2) == 0 + ? nil + : randomElement() + } +} + +extension String: Random { + static func random() -> String { + let letters : NSString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + let len = UInt32(letters.length) + + var randomString = "" + + for _ in 0 ..< 20 { + let rand = arc4random_uniform(len) + var nextChar = letters.character(at: Int(rand)) + randomString += NSString(characters: &nextChar, length: 1) as String + } + + return randomString + } +} + +extension Int: Random { + static func random() -> Int { + return Int(arc4random() % 200) + } +} + +extension UInt: Random { + static func random() -> UInt { + return UInt(arc4random() % 200) + } +} + +extension Int32: Random { + static func random() -> Int32 { + return Int32(arc4random() % 300) + } +} + +extension Int64: Random { + static func random() -> Int64 { + return Int64(arc4random() % 300) + } +} + +extension Double: Random { + static func random() -> Double { + return Double(arc4random() % 1000) / 100 + } +} + +extension Float: Random { + static func random() -> Float { + return Float(arc4random() % 1000) / 100 + } +} + +extension Bool: Random { + static func random() -> Bool { + return arc4random() % 2 == 1 + } +} + +extension Data: Random { + static func random() -> Data { + let bytes = [UInt32](repeating: 0, count: 10).map { _ in arc4random() } + return Data(bytes: bytes, count: 10 ) + } +} + +extension Date: Random { + static func random() -> Date { + return Date(timeIntervalSince1970: Double.random()) + } +} + +extension UIImage { + static func random() -> UIImage { + let color = UIColor.random() + let rect = CGRect(origin: CGPoint(x: 0, y:0), size: CGSize(width: 1, height: 1)) + UIGraphicsBeginImageContext(rect.size) + let context = UIGraphicsGetCurrentContext()! + + context.setFillColor(color.cgColor) + context.fill(rect) + + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return image! + } +} + +extension UIColor { + static func random() -> UIColor { + let literal = CGFloat(arc4random() % 255) + return .init(red: literal, green: literal, blue: literal, alpha: literal) + } +} + +extension NSError { + static func random() -> NSError { + return NSError(domain: .random(), code: .random(), userInfo: nil) + } +} + +extension URL: Random { + static func random() -> URL { + return URL(string: .random())! + } +} From e1fc29261dafe0b27f5c86e22fd1e88f72ed6747 Mon Sep 17 00:00:00 2001 From: Vitalii Malakhovskyi Date: Thu, 17 May 2018 12:13:24 +0300 Subject: [PATCH 3/3] generating testable import from argument --- Example/RoboKittenTests/Templates/AutoMockable.stencil | 3 +-- Example/RoboKittenTests/Templates/AutoRandom.stencil | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Example/RoboKittenTests/Templates/AutoMockable.stencil b/Example/RoboKittenTests/Templates/AutoMockable.stencil index c4c4d07..bc45979 100644 --- a/Example/RoboKittenTests/Templates/AutoMockable.stencil +++ b/Example/RoboKittenTests/Templates/AutoMockable.stencil @@ -1,7 +1,6 @@ import Foundation import SwiftyMock -import Result -@testable import Betterme +{% if argument.testable %}@testable import {{ argument.testable }}{% endif %} {% macro parseFunctionType method %}{% if method.parameters.count == 0 %}FunctionVoidCall{% else %}FunctionCall{% endif %}{% endmacro %} diff --git a/Example/RoboKittenTests/Templates/AutoRandom.stencil b/Example/RoboKittenTests/Templates/AutoRandom.stencil index f59561f..d658e2b 100644 --- a/Example/RoboKittenTests/Templates/AutoRandom.stencil +++ b/Example/RoboKittenTests/Templates/AutoRandom.stencil @@ -1,5 +1,5 @@ import Foundation -@testable import Betterme +{% if argument.testable %}@testable import {{ argument.testable }}{% endif %} {% macro defineDeclaration var %}{{ var.name }}:{% if var.isClosure and not var.isOptional %} @escaping{% endif %} {{ var.typeName }} = {% call defineRandom var %}{% endmacro %} {% macro defineRandom var %}{% if var.isClosure %}{}{% else %}.random(){% endif %}{% endmacro %}