Skip to content

Commit 2e476d5

Browse files
committed
Adds ability to add @testable import statements
Adds the ability to add @testable import statements via the -i flag. This is useful for users who want to only add their mocks to a project's Test target and use the @testable import functionality to import the main projects target
1 parent 64c1825 commit 2e476d5

9 files changed

+143
-7
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ OPTIONS:
9595
--annotation, -a A custom annotation string used to indicate if a type should be mocked (default = @mockable).
9696
--header, -h A custom header documentation to be added to the beginning of a generated mock file.
9797
--macro, -m If set, #if [macro] / #endif will be added to the generated mock file content to guard compilation.
98+
--testable-imports, -i If set, @testable import statments will be added for each module name in this list.
9899
--concurrency-limit, -j Maximum number of threads to execute concurrently (default = number of cores on the running machine).
99100
--logging-level, -v The logging level to use. Default is set to 0 (info only). Set 1 for verbose, 2 for warning, and 3 for error.
100101
--use-sourcekit If this argument is added, it will use SourceKit for parsing. By default it uses SwiftSyntax.

Sources/Mockolo/Executor.swift

+7
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class Executor {
3232
private var exclusionSuffixes: OptionArgument<[String]>!
3333
private var header: OptionArgument<String>!
3434
private var macro: OptionArgument<String>!
35+
private var testableImports: OptionArgument<[String]>!
3536
private var annotation: OptionArgument<String>!
3637
private var concurrencyLimit: OptionArgument<Int>!
3738
private var useSourceKit: OptionArgument<Bool>!
@@ -97,6 +98,10 @@ class Executor {
9798
shortName: "-m",
9899
kind: String.self,
99100
usage: "If set, #if [macro] / #endif will be added to the generated mock file content to guard compilation.")
101+
testableImports = parser.add(option: "--testable-imports",
102+
shortName: "-i",
103+
kind: [String].self,
104+
usage: "If set, @testable import statments will be added for each module name in this list.")
100105
header = parser.add(option: "--header",
101106
shortName: "-h",
102107
kind: String.self,
@@ -158,6 +163,7 @@ class Executor {
158163
let header = arguments.get(self.header)
159164
let loggingLevel = arguments.get(self.loggingLevel) ?? 0
160165
let macro = arguments.get(self.macro)
166+
let testableImports = arguments.get(self.testableImports)
161167
let shouldUseSourceKit = arguments.get(useSourceKit) ?? false
162168

163169
do {
@@ -169,6 +175,7 @@ class Executor {
169175
annotation: annotation,
170176
header: header,
171177
macro: macro,
178+
testableImports: testableImports,
172179
to: outputFilePath,
173180
loggingLevel: loggingLevel,
174181
concurrencyLimit: concurrencyLimit,

Sources/MockoloFramework/Operations/Generator.swift

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public func generate(sourceDirs: [String]?,
3636
annotation: String,
3737
header: String?,
3838
macro: String?,
39+
testableImports: [String]?,
3940
to outputFilePath: String,
4041
loggingLevel: Int,
4142
concurrencyLimit: Int?,
@@ -160,6 +161,7 @@ public func generate(sourceDirs: [String]?,
160161
pathToContentMap: pathToContentMap,
161162
header: header,
162163
macro: macro,
164+
testableImports: testableImports,
163165
to: outputFilePath)
164166
signpost_end(name: "Write results")
165167
let t5 = CFAbsoluteTimeGetCurrent()

Sources/MockoloFramework/Operations/OutputWriter.swift

+22-3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func write(candidates: [(String, Int64)],
2323
pathToContentMap: [(String, Data, Int64)],
2424
header: String?,
2525
macro: String?,
26+
testableImports: [String]?,
2627
to outputFilePath: String) -> String {
2728

2829
var importLines = [String]()
@@ -36,9 +37,27 @@ func write(candidates: [(String, Int64)],
3637
importLines.append(contentsOf: v)
3738
break
3839
}
39-
40-
let importsSet = Set(importLines.map{$0.trimmingCharacters(in: .whitespaces)})
41-
let importLineStr = importsSet.sorted().joined(separator: "\n")
40+
41+
var importLineStr = ""
42+
43+
if let testableImports = testableImports {
44+
var imports = importLines.compactMap { (importLine) -> String? in
45+
return importLine.moduleName
46+
}
47+
imports.append(contentsOf: testableImports)
48+
importLineStr = Set(imports)
49+
.sorted()
50+
.map { testableModuleName -> String in
51+
guard testableImports.contains(testableModuleName) else {
52+
return testableModuleName.asImport
53+
}
54+
return testableModuleName.asTestableImport
55+
}
56+
.joined(separator: "\n")
57+
} else {
58+
let importsSet = Set(importLines.map{$0.trimmingCharacters(in: .whitespaces)})
59+
importLineStr = importsSet.sorted().joined(separator: "\n")
60+
}
4261

4362
let entities = candidates
4463
.sorted { (left: (String, Int64), right: (String, Int64)) -> Bool in

Sources/MockoloFramework/Utils/InheritanceResolver.swift

-1
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,3 @@ func findImportLines(data: Data, offset: Int64?) -> [String] {
188188

189189
return []
190190
}
191-

Sources/MockoloFramework/Utils/StringExtensions.swift

+12
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,16 @@ extension StringProtocol {
129129
return ret.components(separatedBy: separatorsForDisplay)
130130
}
131131

132+
var asTestableImport: String {
133+
return "@testable \(self.asImport)"
134+
}
135+
136+
var asImport: String {
137+
return "import \(self)"
138+
}
139+
140+
var moduleName: String? {
141+
guard self.hasPrefix(String.import) else { return nil }
142+
return self.dropFirst(String.import.count).trimmingCharacters(in: CharacterSet.whitespaces)
143+
}
132144
}

Tests/MockoloTestCase.swift

+4-3
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,18 @@ class MockoloTestCase: XCTestCase {
5454
}
5555
}
5656

57-
func verify(srcContent: String, mockContent: String? = nil, dstContent: String, header: String = "", concurrencyLimit: Int? = 1, parser: ParserType = .random) {
57+
func verify(srcContent: String, mockContent: String? = nil, dstContent: String, header: String = "", testableImports: [String]? = [], concurrencyLimit: Int? = 1, parser: ParserType = .random) {
5858
var mockList: [String]?
5959
if let mock = mockContent {
6060
if mockList == nil {
6161
mockList = [String]()
6262
}
6363
mockList?.append(mock)
6464
}
65-
verify(srcContents: [srcContent], mockContents: mockList, dstContent: dstContent, header: header, concurrencyLimit: concurrencyLimit, parser: parser)
65+
verify(srcContents: [srcContent], mockContents: mockList, dstContent: dstContent, header: header, testableImports: testableImports, concurrencyLimit: concurrencyLimit, parser: parser)
6666
}
6767

68-
func verify(srcContents: [String], mockContents: [String]?, dstContent: String, header: String, concurrencyLimit: Int?, parser: ParserType) {
68+
func verify(srcContents: [String], mockContents: [String]?, dstContent: String, header: String, testableImports: [String]?, concurrencyLimit: Int?, parser: ParserType) {
6969
var index = 0
7070
srcFilePathsCount = srcContents.count
7171
mockFilePathsCount = mockContents?.count ?? 0
@@ -124,6 +124,7 @@ class MockoloTestCase: XCTestCase {
124124
annotation: String.mockAnnotation,
125125
header: header,
126126
macro: "MOCK",
127+
testableImports: testableImports,
127128
to: dstFilePath,
128129
loggingLevel: 3,
129130
concurrencyLimit: concurrencyLimit,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import MockoloFramework
2+
3+
let testableImports = """
4+
\(String.headerDoc)
5+
import Foundation
6+
7+
/// \(String.mockAnnotation)
8+
protocol SimpleVar {
9+
var name: Int { get set }
10+
}
11+
"""
12+
13+
let testableImportsMock =
14+
"""
15+
16+
import Foundation
17+
@testable import SomeImport1
18+
@testable import SomeImport2
19+
20+
21+
class SimpleVarMock: SimpleVar {
22+
23+
private var _doneInit = false
24+
init() { _doneInit = true }
25+
init(name: Int = 0) {
26+
self.name = name
27+
_doneInit = true
28+
}
29+
30+
var nameSetCallCount = 0
31+
var underlyingName: Int = 0
32+
var name: Int {
33+
get { return underlyingName }
34+
set {
35+
underlyingName = newValue
36+
if _doneInit { nameSetCallCount += 1 }
37+
}
38+
}
39+
}
40+
"""
41+
42+
let testableImportsWithOverlap = """
43+
\(String.headerDoc)
44+
import Foundation
45+
import SomeImport1
46+
47+
/// \(String.mockAnnotation)
48+
protocol SimpleVar {
49+
var name: Int { get set }
50+
}
51+
"""
52+
53+
let testableImportsWithOverlapMock =
54+
"""
55+
56+
import Foundation
57+
@testable import SomeImport1
58+
59+
60+
class SimpleVarMock: SimpleVar {
61+
62+
private var _doneInit = false
63+
init() { _doneInit = true }
64+
init(name: Int = 0) {
65+
self.name = name
66+
_doneInit = true
67+
}
68+
69+
var nameSetCallCount = 0
70+
var underlyingName: Int = 0
71+
var name: Int {
72+
get { return underlyingName }
73+
set {
74+
underlyingName = newValue
75+
if _doneInit { nameSetCallCount += 1 }
76+
}
77+
}
78+
}
79+
"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Foundation
2+
3+
class TestableImportStatementsTests: MockoloTestCase {
4+
5+
func testTesableImportStatements() {
6+
verify(srcContent: testableImports,
7+
dstContent: testableImportsMock,
8+
testableImports: ["SomeImport1", "SomeImport2"])
9+
}
10+
11+
func testTesableImportStatementsWithOverlap() {
12+
verify(srcContent: testableImportsWithOverlap,
13+
dstContent: testableImportsWithOverlapMock,
14+
testableImports: ["SomeImport1"])
15+
}
16+
}

0 commit comments

Comments
 (0)