Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ OPTIONS:
Maximum number of threads to execute concurrently (default = 1).
--custom-imports <custom-imports>
If set, custom module imports (separated by a space) will be added to the final import statement list.
--dependency-sourcedirs <dependency-sourcedirs>
Paths to the directories containing source files of modules this target depends on (separated by a space). Their declarations are used to resolve inherited types but are not mocked.
--enable-args-history Whether to enable args history for all functions (default = false). To enable history per function, use the 'history' keyword in the annotation argument.
--disable-combine-default-values
Whether to disable generating Combine streams in mocks (default = false). Set this to true to control how your streams are created in your mocks.
Expand Down Expand Up @@ -175,6 +177,16 @@ OPTIONS:
-h, --help Show help information.
```

### Protocols inheriting from other modules

Mockolo resolves inherited requirements only from the declarations it parses. When a mocked protocol inherits from a protocol in a module that is mocked separately, pass that module's sources as dependencies:

```
./mockolo -s ModuleB -d ./ModuleBMocks.swift --dependency-sourcedirs ModuleA
```

Declarations in `ModuleA` are used to resolve inherited members, associated types and `where` clauses of the protocols in `ModuleB`, but no mocks are generated for them, even if they carry the mock annotation. A declaration in the source files is never replaced by a dependency declaration with the same name, and a parent declared in the dependencies is used instead of a parent mock passed with `--mockfiles`.


## Distribution

Expand Down
10 changes: 10 additions & 0 deletions Sources/Mockolo/Executor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ struct Executor: ParsableCommand {
help: "If set, custom module imports (separated by a space) will be added to the final import statement list.")
private var customImports: [String] = []

@Option(name: .customLong("dependency-sourcedirs"),
parsing: .upToNextOption,
help: "Paths to the directories containing source files of modules this target depends on (separated by a space). Their declarations are used to resolve inherited types but are not mocked.",
completion: .file())
private var dependencySourceDirs: [String] = []

@Flag(name: .long,
help: "Whether to enable args history for all functions (default = false). To enable history per function, use the 'history' keyword in the annotation argument.")
private var enableArgsHistory: Bool = false
Expand Down Expand Up @@ -159,11 +165,14 @@ struct Executor: ParsableCommand {
if srcDirs.isEmpty && srcs.isEmpty {
throw ValidationError("Missing source files or directories")
}

dependencySrcDirs = self.dependencySourceDirs.map(fullPath)
}

// Source paths to be used in `run`
private var srcDirs: [String] = []
private var srcs: [String] = []
private var dependencySrcDirs: [String] = []

mutating func run() throws {
let shouldOutputRunStatusMessages: Bool = loggingLevel < 3
Expand Down Expand Up @@ -192,6 +201,7 @@ struct Executor: ParsableCommand {
do {
try generate(sourceDirs: srcDirs,
sourceFiles: srcs,
dependencySourceDirs: dependencySrcDirs,
exclusionSuffixes: exclusionSuffixes,
mockFilePaths: mockFilePaths,
annotation: annotation,
Expand Down
25 changes: 25 additions & 0 deletions Sources/MockoloFramework/Operations/Generator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ enum InputError: Error {
@discardableResult
public func generate(sourceDirs: [String],
sourceFiles: [String],
dependencySourceDirs: [String] = [],
dependencySourceFiles: [String] = [],
exclusionSuffixes: [String],
mockFilePaths: [String]?,
annotation: String,
Expand Down Expand Up @@ -107,6 +109,29 @@ public func generate(sourceDirs: [String],
}
}
}

// Dependencies only resolve inherited types: never mocked, never replacing a source declaration.
let dependencyPaths = !dependencySourceDirs.isEmpty ? dependencySourceDirs : dependencySourceFiles
if !dependencyPaths.isEmpty {
log("Process dependency source files and add them to the protocol map...", level: .info)
parser.parseDecls(dependencyPaths,
isDirs: !dependencySourceDirs.isEmpty,
exclusionSuffixes: exclusionSuffixes,
annotation: annotation,
fileMacro: macro,
declType: declType) { (elements, imports) in
elements.forEach { element in
if protocolMap[element.entityNode.nameText] == nil {
protocolMap[element.entityNode.nameText] = element
}
}
if let imports = imports {
for (path, importMap) in imports {
pathToImportsMap[path] = importMap
}
}
}
}
signpost_end(name: "Generate protocol map")
let t2 = CFAbsoluteTimeGetCurrent()
log("Took", t2-t1, level: .verbose)
Expand Down
30 changes: 27 additions & 3 deletions Tests/MockoloTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import MockoloFramework
class MockoloTestCase: XCTestCase {
var srcFilePathsCount = 1
var mockFilePathsCount = 1
var dependencyFilePathsCount = 1

let bundle = Bundle(for: MockoloTestCase.self)

Expand Down Expand Up @@ -37,6 +38,19 @@ class MockoloTestCase: XCTestCase {
return paths
}()

lazy var dependencyFilePaths: [String] = {
var idx = 0
var paths = [String]()
let prefix = bundle.bundlePath + "/Dependency"
let suffix = ".swift"
while idx < dependencyFilePathsCount {
let path = prefix + "\(idx)" + suffix
paths.append(path)
idx += 1
}
return paths
}()

override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
let created = FileManager.default.createFile(atPath: defaultDstFilePath, contents: nil, attributes: nil)
Expand All @@ -52,9 +66,12 @@ class MockoloTestCase: XCTestCase {
for mockpath in mockFilePaths {
try? FileManager.default.removeItem(atPath: mockpath)
}
for dependencypath in dependencyFilePaths {
try? FileManager.default.removeItem(atPath: dependencypath)
}
}

func verify(srcContent: String, mockContent: String? = nil, dstContent: String, header: String = "", declType: FindTargetDeclType = .protocolType, useTemplateFunc: Bool = false, testableImports: [String] = [], allowSetCallCount: Bool = false, mockFinal: Bool = false, enableFuncArgsHistory: Bool = false, dstFilePath: String? = nil, concurrencyLimit: Int? = 1, disableCombineDefaultValues: Bool = false, file: StaticString = #filePath, line: UInt = #line) {
func verify(srcContent: String, mockContent: String? = nil, dependencyContent: String? = nil, dstContent: String, header: String = "", declType: FindTargetDeclType = .protocolType, useTemplateFunc: Bool = false, testableImports: [String] = [], allowSetCallCount: Bool = false, mockFinal: Bool = false, enableFuncArgsHistory: Bool = false, dstFilePath: String? = nil, concurrencyLimit: Int? = 1, disableCombineDefaultValues: Bool = false, file: StaticString = #filePath, line: UInt = #line) {
let dstFilePath = dstFilePath ?? defaultDstFilePath
var mockList: [String]?
if let mock = mockContent {
Expand All @@ -63,7 +80,7 @@ class MockoloTestCase: XCTestCase {
}
mockList?.append(mock)
}
try? verify(srcContents: [srcContent], mockContents: mockList, dstContent: dstContent, header: header, declType: declType, useTemplateFunc: useTemplateFunc, testableImports: testableImports, allowSetCallCount: allowSetCallCount, mockFinal: mockFinal, enableFuncArgsHistory: enableFuncArgsHistory, dstFilePath: dstFilePath, concurrencyLimit: concurrencyLimit, disableCombineDefaultValues: disableCombineDefaultValues, file: file, line: line)
try? verify(srcContents: [srcContent], mockContents: mockList, dependencyContents: dependencyContent.map { [$0] }, dstContent: dstContent, header: header, declType: declType, useTemplateFunc: useTemplateFunc, testableImports: testableImports, allowSetCallCount: allowSetCallCount, mockFinal: mockFinal, enableFuncArgsHistory: enableFuncArgsHistory, dstFilePath: dstFilePath, concurrencyLimit: concurrencyLimit, disableCombineDefaultValues: disableCombineDefaultValues, file: file, line: line)
}

func verify(srcContents: [String], mockContent: String? = nil, dstContent: String, header: String = "", declType: FindTargetDeclType = .protocolType, useTemplateFunc: Bool = false, testableImports: [String] = [], allowSetCallCount: Bool = false, mockFinal: Bool = false, enableFuncArgsHistory: Bool = false, dstFilePath: String? = nil, concurrencyLimit: Int? = 1, disableCombineDefaultValues: Bool = false, file: StaticString = #filePath, line: UInt = #line) {
Expand Down Expand Up @@ -94,10 +111,11 @@ class MockoloTestCase: XCTestCase {
)
}

func verify(srcContents: [String], mockContents: [String]?, dstContent: String, header: String, declType: FindTargetDeclType, useTemplateFunc: Bool, testableImports: [String] = [], allowSetCallCount: Bool, mockFinal: Bool, enableFuncArgsHistory: Bool, dstFilePath: String, concurrencyLimit: Int?, disableCombineDefaultValues: Bool, file: StaticString = #filePath, line: UInt = #line) throws {
func verify(srcContents: [String], mockContents: [String]?, dependencyContents: [String]? = nil, dstContent: String, header: String, declType: FindTargetDeclType, useTemplateFunc: Bool, testableImports: [String] = [], allowSetCallCount: Bool, mockFinal: Bool, enableFuncArgsHistory: Bool, dstFilePath: String, concurrencyLimit: Int?, disableCombineDefaultValues: Bool, file: StaticString = #filePath, line: UInt = #line) throws {
var index = 0
srcFilePathsCount = srcContents.count
mockFilePathsCount = mockContents?.count ?? 0
dependencyFilePathsCount = dependencyContents?.count ?? 0

for src in srcContents {
if index < srcContents.count {
Expand All @@ -124,8 +142,14 @@ class MockoloTestCase: XCTestCase {
}
}

for (index, dependencyContent) in (dependencyContents ?? []).enumerated() {
let dependencyCreated = FileManager.default.createFile(atPath: dependencyFilePaths[index], contents: dependencyContent.data(using: .utf8), attributes: nil)
XCTAssert(dependencyCreated, file: file, line: line)
}

try generate(sourceDirs: [],
sourceFiles: srcFilePaths,
dependencySourceFiles: dependencyFilePaths,
exclusionSuffixes: ["Mocks", "Tests"],
mockFilePaths: mockFilePaths,
annotation: String.mockAnnotation,
Expand Down
29 changes: 29 additions & 0 deletions Tests/TestPATs/FixturePAT.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,35 @@ import MockoloFramework
}
}

@Fixture enum patWithSameTypeConstraintInDependency {
/// @mockable
public protocol Foo {
associatedtype Value
func reset()
}

@Fixture enum dependent {
/// @mockable
public protocol NarrowFoo: Foo where Value == Int {}

@Fixture enum expected {
public class NarrowFooMock: NarrowFoo {
public init() { }


public private(set) var resetCallCount = 0
public var resetHandler: (() -> ())?
public func reset() {
resetCallCount += 1
if let resetHandler = resetHandler {
resetHandler()
}
}
}
}
}
}

@Fixture enum patWithMixedConstraints {
public protocol Foo {
associatedtype T
Expand Down
6 changes: 6 additions & 0 deletions Tests/TestPATs/ProtocolAssociatedTypeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ class ProtocolAssociatedTypeTests: MockoloTestCase {
dstContent: patWithSameTypeConstraint.expected._source)
}

func testPATWithSameTypeConstraintInDependency() {
verify(srcContent: patWithSameTypeConstraintInDependency.dependent._source,
dependencyContent: patWithSameTypeConstraintInDependency._source,
dstContent: patWithSameTypeConstraintInDependency.dependent.expected._source)
}

func testPATWithMixedConstraints() {
verify(srcContent: patWithMixedConstraints._source,
dstContent: patWithMixedConstraints.expected._source)
Expand Down