Skip to content

Commit

Permalink
Initial commit of AutoMockable template
Browse files Browse the repository at this point in the history
  • Loading branch information
jimmya committed Jul 29, 2023
1 parent 21ce7c7 commit 267ae32
Show file tree
Hide file tree
Showing 18 changed files with 862 additions and 0 deletions.
Binary file added .DS_Store
Binary file not shown.
Binary file added .github/.DS_Store
Binary file not shown.
17 changes: 17 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Test
on: [push]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
tsest:
runs-on: macos-13
steps:
- uses: actions/checkout@v3
- name: Install sourcery
run: brew install sourcery
- name: Run sourcery
run: sourcery
- name: Test
run: swift test
9 changes: 9 additions & 0 deletions .sourcery.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
configurations:
- sources:
- Sources/MockDeclarations
templates:
- Sources/Templates/AutoMockable.swifttemplate
output: Tests/TemplateTests
args:
imports: [Foundation, XCTest]
testableImports: [MockDeclarations]
Binary file added .swiftpm/xcode/.DS_Store
Binary file not shown.
7 changes: 7 additions & 0 deletions .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

122 changes: 122 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
{
"pins" : [
{
"identity" : "aexml",
"kind" : "remoteSourceControl",
"location" : "https://github.com/tadija/AEXML.git",
"state" : {
"revision" : "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3",
"version" : "4.6.1"
}
},
{
"identity" : "commander",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kylef/Commander.git",
"state" : {
"revision" : "4b6133c3071d521489a80c38fb92d7983f19d438",
"version" : "0.9.1"
}
},
{
"identity" : "komondor",
"kind" : "remoteSourceControl",
"location" : "https://github.com/shibapm/Komondor.git",
"state" : {
"revision" : "90b087b1e39069684b1ff4bf915c2aae594f2d60",
"version" : "1.1.3"
}
},
{
"identity" : "packageconfig",
"kind" : "remoteSourceControl",
"location" : "https://github.com/shibapm/PackageConfig.git",
"state" : {
"revision" : "58523193c26fb821ed1720dcd8a21009055c7cdb",
"version" : "1.1.3"
}
},
{
"identity" : "pathkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kylef/PathKit.git",
"state" : {
"revision" : "3bfd2737b700b9a36565a8c94f4ad2b050a5e574",
"version" : "1.0.1"
}
},
{
"identity" : "shellout",
"kind" : "remoteSourceControl",
"location" : "https://github.com/JohnSundell/ShellOut.git",
"state" : {
"revision" : "e1577acf2b6e90086d01a6d5e2b8efdaae033568",
"version" : "2.3.0"
}
},
{
"identity" : "sourcery",
"kind" : "remoteSourceControl",
"location" : "https://github.com/krzysztofzablocki/Sourcery",
"state" : {
"revision" : "d441f56ba972d04014c6de0f48ac17861ce629f3",
"version" : "2.0.2"
}
},
{
"identity" : "spectre",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kylef/Spectre.git",
"state" : {
"revision" : "26cc5e9ae0947092c7139ef7ba612e34646086c7",
"version" : "0.10.1"
}
},
{
"identity" : "stencil",
"kind" : "remoteSourceControl",
"location" : "https://github.com/stencilproject/Stencil.git",
"state" : {
"revision" : "4f222ac85d673f35df29962fc4c36ccfdaf9da5b",
"version" : "0.15.1"
}
},
{
"identity" : "stencilswiftkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftGen/StencilSwiftKit.git",
"state" : {
"revision" : "20e2de5322c83df005939d9d9300fab130b49f97",
"version" : "2.10.1"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "2c49d66d34dfd6f8130afdba889de77504b58ec0",
"version" : "508.0.1"
}
},
{
"identity" : "xcodeproj",
"kind" : "remoteSourceControl",
"location" : "https://github.com/tuist/XcodeProj.git",
"state" : {
"revision" : "446f3a0db73e141c7f57e26fcdb043096b1db52c",
"version" : "8.3.1"
}
},
{
"identity" : "yams",
"kind" : "remoteSourceControl",
"location" : "https://github.com/jpsim/Yams.git",
"state" : {
"revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3",
"version" : "5.0.6"
}
}
],
"version" : 2
}
36 changes: 36 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "SourceryTemplates",
platforms: [
.macOS(.v10_15),
],
dependencies: [
.package(url: "https://github.com/krzysztofzablocki/Sourcery", exact: "2.0.2"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "Templates",
dependencies: [
.product(name: "SourceryRuntime", package: "Sourcery"),
],
exclude: [
"AutoMockable.swifttemplate",
]
),
.target(
name: "MockDeclarations"
),
.testTarget(
name: "TemplateTests",
dependencies: [
"MockDeclarations",
]
),
]
)
Binary file added Sources/.DS_Store
Binary file not shown.
9 changes: 9 additions & 0 deletions Sources/MockDeclarations/MockProtocolDeclaration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// sourcery: AutoMockable
protocol MockProtocolDeclaration {
var immutableProperty: Int { get }
var mutableProperty: Int { get set }
var immutableOptionalProperty: Int? { get }
var mutableOptionalProperty: Int? {get set }

func method(property: Int, optionalProperty: Int?, closureProperty: @escaping (Bool, Int) -> Int) -> Int
}
Binary file added Sources/Templates/.DS_Store
Binary file not shown.
66 changes: 66 additions & 0 deletions Sources/Templates/AutoMockable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import SourceryRuntime

extension Annotations {
var testableImports: [String] {
self["testableImports"] as? [String] ?? []
}

var imports: [String] {
self["imports"] as? [String] ?? []
}
}

enum AutoMockable {
static func generate(types: Types, annotations: Annotations) -> String {
var lines: [String] = []
lines.append(contentsOf: annotations.testableImports.map { "@testable import \($0)" })
lines.append(contentsOf: annotations.imports.map { "import \($0)" })
lines.append("")

let sortedProtocols = types.protocols.sorted(by: { $0.name < $1.name }).filter(\.isAutoMockable)
sortedProtocols.forEach { protocolType in
lines.append(contentsOf: mock(type: protocolType))
}
return lines.joined(separator: .newLine)
}

static func mock(type: Protocol) -> [String] {
let variables = type.allVariables.filter { $0.definedInType?.isExtension == false }
let allMethods = type.allMethods.filter { $0.definedInType?.isExtension == false }.sorted()
let variableLines = variables.flatMap { variable in
[
variable.generateMock(),
"",
]
}
let methodLines: [String] = allMethods.flatMap { method in
let methodName = generateMethodName(method: method, allMethods: allMethods).replacingOccurrences(of: "?", with: "")
var lines = [
mockStubParameters(methodName: methodName, method: method, type: type),
mockMethodAttributes(method: method)
]
if method.isInitializer {
lines.append(contentsOf: [
"required \(method.name.replacingOccurrences(of: "?", with: "")) {".addingIndent(),
mockMethodReceivedParameters(methodName: methodName, method: method).addingIndent(count: 2),
"}".addingIndent(),
])
} else {
lines.append(contentsOf: [
mockMethodFunctionDeclaration(method: method, type: type).addingIndent(),
mockMethodReceivedParameters(methodName: methodName, method: method),
"}".addingIndent(),
])
}
return lines
}
var lines = [
type.generateClassDeclaration(),
"",
]
lines.append(contentsOf: variableLines)
lines.append(contentsOf: methodLines)
lines.append("}")
return lines
}
}
7 changes: 7 additions & 0 deletions Sources/Templates/AutoMockable.swifttemplate
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<%# TODO: Break helpers up into extensions -%>
<%- includeFile("helpers.swift") -%>
<%- includeFile("Variable+Extension.swift") -%>
<%- includeFile("Protocol+Extension.swift") -%>
<%- includeFile("AutoMockable.swift") -%>

<%= AutoMockable.generate(types: types, annotations: argument) -%>
22 changes: 22 additions & 0 deletions Sources/Templates/Protocol+Extension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import SourceryRuntime

extension Protocol {
/// Returns `class DefaultProtocolNameMock: InheritedTypes {`
func generateClassDeclaration() -> String {
"\(mockType) Default\(name)Mock: \(mockInheritedTypes) {"
}
}

private extension Protocol {
var mockInheritedTypes: String {
let inherited = genericRequirements.map { "\($0.rightType.typeName.name), " }.joined()
return "\(inherited)\(name)"
}

var mockType: String {
if based.contains(where: { $0.key == "AnyActor"}) {
return "actor"
}
return isFinal ? "final class" : "class"
}
}
58 changes: 58 additions & 0 deletions Sources/Templates/Variable+Extension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import SourceryRuntime

extension Variable {

func generateMock() -> String {
isMutable ? mockMutable : mockComputed
}
}

private extension Variable {
var mockMutable: String {
let capitalizedName = name.capitalizingFirstLetter()
let defaultValue = generateDefaultValue(type: type, typeName: typeName, includeComplexType: false)
let nonOptionalSignature = defaultValue.isEmpty ? "!" : "! = \(defaultValue)"
let typeName = typeName.isClosure ? "(\(typeName))" : typeName.name
let listTypeName = self.typeName.isClosure ? "(\(typeName))" : self.typeName.name
return """
var invoked\(capitalizedName)Setter = false
var invoked\(capitalizedName)SetterCount = 0
var invoked\(capitalizedName): \(typeName)\(isOptional ? "" : "?")
var invoked\(capitalizedName)List: [\(listTypeName)] = []
var invoked\(capitalizedName)Getter = false
var invoked\(capitalizedName)GetterCount = 0
var stubbed\(capitalizedName): \(typeName)\(isOptional ? "" : nonOptionalSignature)
var \(name): \(typeName) {
get {
invoked\(capitalizedName)Getter = true
invoked\(capitalizedName)GetterCount += 1
return stubbed\(capitalizedName)
}
set {
invoked\(capitalizedName)Setter = true
invoked\(capitalizedName)SetterCount += 1
invoked\(capitalizedName) = newValue
invoked\(capitalizedName)List.append(newValue)
}
}
"""
}

var mockComputed: String {
let capitalizedName = name.capitalizingFirstLetter()
let defaultValue = generateDefaultValue(type: type, typeName: typeName, includeComplexType: false)
let nonOptionalSignature = defaultValue.isEmpty ? "!" : "! = \(defaultValue)"
return """
var invoked\(capitalizedName)Getter = false
var invoked\(capitalizedName)GetterCount = 0
var stubbed\(capitalizedName): \(typeName)\(isOptional ? "" : nonOptionalSignature)
var \(name): \(typeName) {
invoked\(capitalizedName)Getter = true
invoked\(capitalizedName)GetterCount += 1
return stubbed\(capitalizedName)
}
"""
}
}
Loading

0 comments on commit 267ae32

Please sign in to comment.