/
SwiftTestTool.swift
201 lines (167 loc) · 6.28 KB
/
SwiftTestTool.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
/*
This source file is part of the Swift.org open source project
Copyright 2015 - 2016 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception
See http://swift.org/LICENSE.txt for license information
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/
import class Foundation.NSProcessInfo
import Basic
import Utility
import func POSIX.chdir
import func libc.exit
private enum TestError: ErrorProtocol {
case testsExecutableNotFound
}
extension TestError: CustomStringConvertible {
var description: String {
switch self {
case .testsExecutableNotFound:
return "no tests found to execute, create a module in your `Tests' directory"
}
}
}
private enum Mode: Argument, Equatable, CustomStringConvertible {
case usage
case run(String?)
init?(argument: String, pop: () -> String?) throws {
switch argument {
case "--help", "-h":
self = .usage
case "-s", "--specifier":
guard let specifier = pop() else { throw OptionParserError.expectedAssociatedValue(argument) }
self = .run(specifier)
default:
return nil
}
}
var description: String {
switch self {
case .usage:
return "--help"
case .run(let specifier):
return specifier ?? ""
}
}
}
private func ==(lhs: Mode, rhs: Mode) -> Bool {
return lhs.description == rhs.description
}
private enum TestToolFlag: Argument {
case chdir(String)
init?(argument: String, pop: () -> String?) throws {
switch argument {
case "--chdir", "-C":
guard let path = pop() else { throw OptionParserError.expectedAssociatedValue(argument) }
self = .chdir(path)
default:
return nil
}
}
}
/// swift-test tool namespace
public struct SwiftTestTool {
let args: [String]
public init(args: [String]) {
self.args = args
}
public func run() {
do {
let (mode, opts) = try parseOptions(commandLineArguments: args)
if let dir = opts.chdir {
try chdir(dir)
}
switch mode {
case .usage:
usage()
case .run(let specifier):
let configuration = "debug" //FIXME should swift-test support configuration option?
func determineTestPath() throws -> String {
//FIXME better, ideally without parsing manifest since
// that makes us depend on the whole Manifest system
let packageName = opts.path.root.basename //FIXME probably not true
let maybePath = Path.join(opts.path.build, configuration, "\(packageName)Tests.xctest")
if maybePath.exists {
return maybePath
} else {
let possiblePaths = walk(opts.path.build).filter {
$0.basename != "Package.xctest" && // this was our hardcoded name, may still exist if no clean
$0.hasSuffix(".xctest")
}
guard let path = possiblePaths.first else {
throw TestError.testsExecutableNotFound
}
return path
}
}
let yamlPath = Path.join(opts.path.build, "\(configuration).yaml")
try build(YAMLPath: yamlPath, target: "test")
let success = try test(path: determineTestPath(), xctestArg: specifier)
exit(success ? 0 : 1)
}
} catch Error.buildYAMLNotFound {
print("error: you must run `swift build` first", to: &stderr)
exit(1)
} catch {
handle(error: error, usage: usage)
}
}
private func usage(_ print: (String) -> Void = { print($0) }) {
// .........10.........20.........30.........40.........50.........60.........70..
print("OVERVIEW: Build and run tests")
print("")
print("USAGE: swift test [specifier] [options]")
print("")
print("SPECIFIER:")
print(" -s, --specifier <test-module>.<test-case> Run a test case subclass")
print(" -s, --specifier <test-module>.<test-case>/<test> Run a specific test method")
print("")
print("OPTIONS:")
print(" -C, --chdir <path> Change working directory before any other operation")
print(" --build-path <path> Specify build directory")
print("")
print("NOTE: Use `swift package` to perform other functions on packages")
}
private func parseOptions(commandLineArguments args: [String]) throws -> (Mode, Options) {
let (mode, flags): (Mode?, [TestToolFlag]) = try Basic.parseOptions(arguments: args)
let opts = Options()
for flag in flags {
switch flag {
case .chdir(let path):
opts.chdir = path
}
}
return (mode ?? .run(nil), opts)
}
private func test(path: String, xctestArg: String? = nil) throws -> Bool {
guard path.isValidTest else {
throw TestError.testsExecutableNotFound
}
var args: [String] = []
#if os(OSX)
args = ["xcrun", "xctest"]
if let xctestArg = xctestArg {
args += ["-XCTest", xctestArg]
}
args += [path]
#else
args += [path]
if let xctestArg = xctestArg {
args += [xctestArg]
}
#endif
// Execute the XCTest with inherited environment as it is convenient to pass senstive
// information like username, password etc to test cases via enviornment variables.
let result: Void? = try? system(args, environment: NSProcessInfo.processInfo().environment)
return result != nil
}
}
private extension String {
var isValidTest: Bool {
#if os(OSX)
return isDirectory // ${foo}.xctest is dir on OSX
#else
return isFile // otherwise ${foo}.xctest is executable file
#endif
}
}