Skip to content

Commit bfb7038

Browse files
committed
Clean up the tests thoroughly - add proper logging setup, add better temp working area handling, use modern URL-based APIs
1 parent 199b8b9 commit bfb7038

File tree

2 files changed

+157
-101
lines changed

2 files changed

+157
-101
lines changed

Tests/ShellOutTests/ShellOutTests.swift

Lines changed: 112 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,34 @@
44
* Licensed under the MIT license. See LICENSE file.
55
*/
66

7+
import Logging
8+
import TSCBasic
79
import XCTest
810
@testable import ShellOut
911

10-
func XCTAssertEqualAsync<T>(
11-
_ expression1: @autoclosure () async throws -> T,
12-
_ expression2: @autoclosure () async throws -> T,
13-
_ message: @autoclosure () -> String = "",
14-
file: StaticString = #filePath,
15-
line: UInt = #line
16-
) async where T: Equatable {
17-
do {
18-
let expr1 = try await expression1()
19-
let expr2 = try await expression2()
20-
21-
return XCTAssertEqual(expr1, expr2, message(), file: file, line: line)
22-
} catch {
23-
// Trick XCTest into behaving correctly for a thrown error.
24-
return XCTAssertEqual(try { () -> Bool in throw error }(), false, message(), file: file, line: line)
12+
final class ShellOutTests: XCTestCase {
13+
static var temporaryDirectoryUrl: URL!
14+
static var logger: Logger!
15+
16+
override class func setUp() {
17+
XCTAssert(isLoggingConfigured)
18+
self.logger = .init(label: "test")
19+
// This is technically a misuse of the `withTemporaryDirectory()` utility, but there's no equivalent of
20+
// the `TemporaryFile` API for drectories and this is (much) easier than directly invoking `mkdtemp(3)`.
21+
// This is done in the class setUp rather than the instance method so it happens only once per test run,
22+
// rather than for each single test.
23+
self.temporaryDirectoryUrl = try! withTemporaryDirectory(prefix: "ShellOutTests", removeTreeOnDeinit: false, { $0.asURL })
2524
}
26-
}
27-
28-
class ShellOutTests: XCTestCase {
25+
26+
override class func tearDown() {
27+
try? FileManager.default.removeItem(at: self.temporaryDirectoryUrl)
28+
}
29+
30+
var logger: Logger { Self.logger }
31+
var tempUrl: URL { Self.temporaryDirectoryUrl }
32+
func tempUrl(filename: String) -> URL { Self.temporaryDirectoryUrl.appendingPathComponent(filename, isDirectory: false) }
33+
func tempUrl(directory: String) -> URL { Self.temporaryDirectoryUrl.appendingPathComponent(directory, isDirectory: true) }
34+
2935
func test_appendArguments() throws {
3036
var cmd = ShellOutCommand(command: "foo")
3137
XCTAssertEqual(cmd.description, "foo")
@@ -50,26 +56,27 @@ class ShellOutTests: XCTestCase {
5056
}
5157

5258
func testWithoutArguments() async throws {
53-
let uptime = try await shellOut(to: "uptime").stdout
59+
let uptime = try await shellOut(to: "uptime", logger: logger).stdout
5460
XCTAssertTrue(uptime.contains("load average"))
5561
}
5662

5763
func testWithArguments() async throws {
58-
let echo = try await shellOut(to: "echo", arguments: ["Hello world"]).stdout
64+
let echo = try await shellOut(to: "echo", arguments: ["Hello world"], logger: logger).stdout
5965
XCTAssertEqual(echo, "Hello world")
6066
}
6167

6268
func testSingleCommandAtPath() async throws {
63-
let tempDir = NSTemporaryDirectory()
6469
try await shellOut(
6570
to: "bash",
66-
arguments: ["-c", #"echo Hello > "\#(tempDir)/ShellOutTests-SingleCommand.txt""#]
71+
arguments: ["-c", #"echo Hello > "\#(tempUrl(filename: "ShellOutTests-SingleCommand.txt").path)""#],
72+
logger: logger
6773
)
6874

6975
let textFileContent = try await shellOut(
7076
to: "cat",
7177
arguments: ["ShellOutTests-SingleCommand.txt"],
72-
at: tempDir
78+
at: tempUrl.path,
79+
logger: logger
7380
).stdout
7481

7582
XCTAssertEqual(textFileContent, "Hello")
@@ -78,31 +85,33 @@ class ShellOutTests: XCTestCase {
7885
func testSingleCommandAtPathContainingSpace() async throws {
7986
try await shellOut(to: "mkdir",
8087
arguments: ["-p", "ShellOut Test Folder"],
81-
at: NSTemporaryDirectory())
88+
at: tempUrl.path,
89+
logger: logger)
90+
let testFolderUrl = tempUrl(directory: "ShellOut Test Folder")
8291
try await shellOut(to: "bash", arguments: ["-c", "echo Hello > File"],
83-
at: NSTemporaryDirectory() + "ShellOut Test Folder")
92+
at: testFolderUrl.path,
93+
logger: logger)
8494

8595
let output = try await shellOut(
8696
to: "cat",
87-
arguments: ["\(NSTemporaryDirectory())ShellOut Test Folder/File"]).stdout
97+
arguments: [testFolderUrl.appendingPathComponent("File", isDirectory: false).path],
98+
logger: logger).stdout
8899
XCTAssertEqual(output, "Hello")
89100
}
90101

91102
func testSingleCommandAtPathContainingTilde() async throws {
92-
let homeContents = try await shellOut(to: "ls", arguments: ["-a"], at: "~").stdout
103+
let homeContents = try await shellOut(to: "ls", arguments: ["-a"], at: "~", logger: logger).stdout
93104
XCTAssertFalse(homeContents.isEmpty)
94105
}
95106

96107
func testThrowingError() async {
97-
do {
98-
try await shellOut(to: .bash(arguments: ["cd notADirectory"]))
99-
XCTFail("Expected expression to throw")
100-
} catch let error as ShellOutError {
108+
await XCTAssertThrowsErrorAsync(try await shellOut(to: .bash(arguments: ["cd notADirectory"]), logger: logger)) {
109+
guard let error = $0 as? ShellOutError else {
110+
return XCTFail("Expected ShellOutError, got \(String(reflecting: $0))")
111+
}
101112
XCTAssertTrue(error.message.contains("notADirectory"))
102113
XCTAssertTrue(error.output.isEmpty)
103114
XCTAssertTrue(error.terminationStatus != 0)
104-
} catch {
105-
XCTFail("Invalid error type: \(error)")
106115
}
107116
}
108117

@@ -131,6 +140,7 @@ class ShellOutTests: XCTestCase {
131140
let pipe = Pipe()
132141
let output = try await shellOut(to: "echo",
133142
arguments: ["Hello"],
143+
logger: logger,
134144
outputHandle: pipe.fileHandleForWriting).stdout
135145
let capturedData = pipe.fileHandleForReading.readDataToEndOfFile()
136146
XCTAssertEqual(output, "Hello")
@@ -140,72 +150,75 @@ class ShellOutTests: XCTestCase {
140150
func testCapturingErrorWithHandle() async throws {
141151
let pipe = Pipe()
142152

143-
do {
144-
try await shellOut(to: .bash(arguments: ["cd notADirectory"]),
145-
errorHandle: pipe.fileHandleForWriting)
146-
XCTFail("Expected expression to throw")
147-
} catch let error as ShellOutError {
153+
await XCTAssertThrowsErrorAsync(
154+
try await shellOut(to: .bash(arguments: ["cd notADirectory"]), logger: logger, errorHandle: pipe.fileHandleForWriting)
155+
) {
156+
guard let error = $0 as? ShellOutError else {
157+
return XCTFail("Expected ShellOutError, got \(String(reflecting: $0))")
158+
}
148159
XCTAssertTrue(error.message.contains("notADirectory"))
149160
XCTAssertTrue(error.output.isEmpty)
150161
XCTAssertTrue(error.terminationStatus != 0)
151162

152163
let capturedData = pipe.fileHandleForReading.readDataToEndOfFile()
153-
XCTAssertEqual(error.message + "\n", String(data: capturedData, encoding: .utf8))
154-
} catch {
155-
XCTFail("Invalid error type: \(error)")
164+
XCTAssertEqual(error.message + "\n", String(decoding: capturedData, as: UTF8.self))
156165
}
157166
}
158167

159168
func testGitCommands() async throws {
160169
// Setup & clear state
161-
let tempFolderPath = NSTemporaryDirectory()
162-
try await shellOut(to: "rm",
163-
arguments: ["-rf", "GitTestOrigin"],
164-
at: tempFolderPath, logger: .init(label: "test"))
165-
try await shellOut(to: "rm",
166-
arguments: ["-rf", "GitTestClone"],
167-
at: tempFolderPath, logger: .init(label: "test"))
170+
try await shellOut(
171+
to: "rm",
172+
arguments: ["-rf", "GitTestOrigin"],
173+
at: tempUrl.path,
174+
logger: logger
175+
)
176+
try await shellOut(
177+
to: "rm",
178+
arguments: ["-rf", "GitTestClone"],
179+
at: tempUrl.path,
180+
logger: logger
181+
)
168182

169183
// Create a origin repository and make a commit with a file
170-
let originPath = tempFolderPath + "/GitTestOrigin"
171-
try await shellOut(to: .createFolder(named: "GitTestOrigin"), at: tempFolderPath, logger: .init(label: "test"))
172-
try await shellOut(to: .gitInit(), at: originPath, logger: .init(label: "test"))
173-
try await shellOut(to: .createFile(named: "Test", contents: "Hello world"), at: originPath, logger: .init(label: "test"))
174-
try await shellOut(to: "git", arguments: ["add", "."], at: originPath, logger: .init(label: "test"))
175-
try await shellOut(to: .gitCommit(message: "Commit"), at: originPath, logger: .init(label: "test"))
184+
let originDir = tempUrl(directory: "GitTestOrigin")
185+
try await shellOut(to: .createFolder(named: "GitTestOrigin"), at: tempUrl.path, logger: logger)
186+
try await shellOut(to: .gitInit(), at: originDir.path, logger: logger)
187+
try await shellOut(to: .createFile(named: "Test", contents: "Hello world"), at: originDir.path, logger: logger)
188+
try await shellOut(to: "git", arguments: ["add", "."], at: originDir.path, logger: logger)
189+
try await shellOut(to: .gitCommit(message: "Commit"), at: originDir.path, logger: logger)
176190

177191
// Clone to a new repository and read the file
178-
let clonePath = tempFolderPath + "/GitTestClone"
179-
let cloneURL = URL(fileURLWithPath: originPath)
180-
try await shellOut(to: .gitClone(url: cloneURL, to: "GitTestClone"), at: tempFolderPath, logger: .init(label: "test"))
192+
let cloneDir = tempUrl(directory: "GitTestClone")
193+
try await shellOut(to: .gitClone(url: originDir, to: "GitTestClone"), at: tempUrl.path, logger: logger)
181194

182-
let filePath = clonePath + "/Test"
183-
await XCTAssertEqualAsync(try await shellOut(to: .readFile(at: filePath), logger: .init(label: "test")).stdout, "Hello world")
195+
let fileUrl = cloneDir.appendingPathComponent("Test", isDirectory: false)
196+
await XCTAssertEqualAsync(try await shellOut(to: .readFile(at: fileUrl.path), logger: logger).stdout, "Hello world")
184197

185198
// Make a new commit in the origin repository
186-
try await shellOut(to: .createFile(named: "Test", contents: "Hello again"), at: originPath, logger: .init(label: "test"))
187-
try await shellOut(to: .gitCommit(message: "Commit"), at: originPath, logger: .init(label: "test"))
199+
try await shellOut(to: .createFile(named: "Test", contents: "Hello again"), at: originDir.path, logger: logger)
200+
try await shellOut(to: .gitCommit(message: "Commit"), at: originDir.path, logger: logger)
188201

189202
// Pull the commit in the clone repository and read the file again
190-
try await shellOut(to: .gitPull(), at: clonePath)
191-
await XCTAssertEqualAsync(try await shellOut(to: .readFile(at: filePath), logger: .init(label: "test")).stdout, "Hello again")
203+
try await shellOut(to: .gitPull(), at: cloneDir.path, logger: logger)
204+
await XCTAssertEqualAsync(try await shellOut(to: .readFile(at: fileUrl.path), logger: logger).stdout, "Hello again")
192205
}
193206

194207
func testBash() async throws {
195208
// Without explicit -c parameter
196-
await XCTAssertEqualAsync(try await shellOut(to: .bash(arguments: ["echo", "foo"])).stdout,
209+
await XCTAssertEqualAsync(try await shellOut(to: .bash(arguments: ["echo", "foo"]), logger: logger).stdout,
197210
"foo")
198211
// With explicit -c parameter
199-
await XCTAssertEqualAsync(try await shellOut(to: .bash(arguments: ["-c", "echo", "foo"])).stdout,
212+
await XCTAssertEqualAsync(try await shellOut(to: .bash(arguments: ["-c", "echo", "foo"]), logger: logger).stdout,
200213
"foo")
201214
}
202215

203216
func testBashArgumentQuoting() async throws {
204217
await XCTAssertEqualAsync(try await shellOut(to: .bash(arguments: ["echo",
205-
"foo ; echo bar".quoted])).stdout,
218+
"foo ; echo bar".quoted]), logger: logger).stdout,
206219
"foo ; echo bar")
207220
await XCTAssertEqualAsync(try await shellOut(to: .bash(arguments: ["echo",
208-
"foo ; echo bar".verbatim])).stdout,
221+
"foo ; echo bar".verbatim]), logger: logger).stdout,
209222
"foo\nbar")
210223
}
211224

@@ -221,47 +234,45 @@ class ShellOutTests: XCTestCase {
221234

222235
func test_git_tags() async throws {
223236
// setup
224-
let tempDir = NSTemporaryDirectory().appending("test_stress_\(UUID())")
225-
defer {
226-
try? Foundation.FileManager.default.removeItem(atPath: tempDir)
227-
}
237+
let tempDir = tempUrl(directory: "test_stress_\(UUID())")
228238
let sampleGitRepoName = "ErrNo"
229-
let sampleGitRepoZipFile = fixturesDirectory()
230-
.appendingPathComponent("\(sampleGitRepoName).zip").path
231-
let path = "\(tempDir)/\(sampleGitRepoName)"
232-
try! Foundation.FileManager.default.createDirectory(atPath: tempDir, withIntermediateDirectories: false, attributes: nil)
233-
try! await ShellOut.shellOut(to: .init(command: "unzip", arguments: [sampleGitRepoZipFile]), at: tempDir)
239+
let sampleGitRepoZipFile = fixturesDirectory().appendingPathComponent(sampleGitRepoName, isDirectory: false).appendingPathExtension("zip").path
240+
let sampleGitRepoDir = tempDir.appendingPathComponent(sampleGitRepoName, isDirectory: true)
241+
242+
try Foundation.FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: false, attributes: nil)
243+
try await ShellOut.shellOut(to: .init(command: "unzip", arguments: [sampleGitRepoZipFile]), at: tempDir.path, logger: logger)
234244

235245
// MUT
236-
await XCTAssertEqualAsync(try await shellOut(to: ShellOutCommand(command: "git", arguments: ["tag"]),
237-
at: path).stdout, """
238-
0.2.0
239-
0.2.1
240-
0.2.2
241-
0.2.3
242-
0.2.4
243-
0.2.5
244-
0.3.0
245-
0.4.0
246-
0.4.1
247-
0.4.2
248-
0.5.0
249-
0.5.1
250-
0.5.2
251-
v0.0.1
252-
v0.0.2
253-
v0.0.3
254-
v0.0.4
255-
v0.0.5
256-
v0.1.0
257-
""")
246+
await XCTAssertEqualAsync(
247+
try await shellOut(to: ShellOutCommand(command: "git", arguments: ["tag"]), at: sampleGitRepoDir.path, logger: logger).stdout,
248+
"""
249+
0.2.0
250+
0.2.1
251+
0.2.2
252+
0.2.3
253+
0.2.4
254+
0.2.5
255+
0.3.0
256+
0.4.0
257+
0.4.1
258+
0.4.2
259+
0.5.0
260+
0.5.1
261+
0.5.2
262+
v0.0.1
263+
v0.0.2
264+
v0.0.3
265+
v0.0.4
266+
v0.0.5
267+
v0.1.0
268+
""")
258269
}
259270
}
260271

261272
extension ShellOutTests {
262-
func fixturesDirectory(path: String = #file) -> URL {
263-
let url = URL(fileURLWithPath: path)
273+
func fixturesDirectory(path: String = #filePath) -> URL {
274+
let url = URL(fileURLWithPath: path, isDirectory: false)
264275
let testsDir = url.deletingLastPathComponent()
265-
return testsDir.appendingPathComponent("Fixtures")
276+
return testsDir.appendingPathComponent("Fixtures", isDirectory: true)
266277
}
267278
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import Logging
2+
import XCTest
3+
4+
func XCTAssertEqualAsync<T>(
5+
_ expression1: @autoclosure () async throws -> T,
6+
_ expression2: @autoclosure () async throws -> T,
7+
_ message: @autoclosure () -> String = "",
8+
file: StaticString = #filePath,
9+
line: UInt = #line
10+
) async where T: Equatable {
11+
do {
12+
let expr1 = try await expression1()
13+
let expr2 = try await expression2()
14+
15+
return XCTAssertEqual(expr1, expr2, message(), file: file, line: line)
16+
} catch {
17+
// Trick XCTest into behaving correctly for a thrown error.
18+
return XCTAssertEqual(try { () -> Bool in throw error }(), false, message(), file: file, line: line)
19+
}
20+
}
21+
22+
func XCTAssertThrowsErrorAsync<ResultType>(
23+
_ expression: @autoclosure () async throws -> ResultType,
24+
_ message: @autoclosure () -> String = "",
25+
file: StaticString = #filePath, line: UInt = #line,
26+
_ callback: Optional<(Error) -> Void> = nil
27+
) async {
28+
do {
29+
_ = try await expression()
30+
XCTFail("Did not throw: \(message())", file: file, line: line)
31+
} catch {
32+
callback?(error)
33+
}
34+
}
35+
36+
let isLoggingConfigured: Bool = {
37+
LoggingSystem.bootstrap { label in
38+
var handler = StreamLogHandler.standardOutput(label: label)
39+
handler.logLevel = ProcessInfo.processInfo.environment["LOG_LEVEL"].flatMap { Logger.Level(rawValue: $0) } ?? .debug
40+
return handler
41+
}
42+
return true
43+
}()
44+
45+

0 commit comments

Comments
 (0)