Skip to content

Commit a4b4fe0

Browse files
Replace some uses of temp_await with await in tests (#7102)
Start using `async`/`await` in testing ### Motivation: This should start to make easier to adopt `async`/`await` by reducing the number of non-async callers ### Modifications: 1 commit which adds `async`/`await` bridges 1 commit which adopts the bridges in a test target (`BasicTests`) ### Result: reduce usage of `temp_await` --------- Co-authored-by: Max Desiatov <[email protected]>
1 parent 979c8dd commit a4b4fe0

File tree

8 files changed

+207
-228
lines changed

8 files changed

+207
-228
lines changed

Sources/Basics/Archiver/Archiver.swift

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public protocol Archiver {
2323
/// - archivePath: The `AbsolutePath` to the archive to extract.
2424
/// - destinationPath: The `AbsolutePath` to the directory to extract to.
2525
/// - completion: The completion handler that will be called when the operation finishes to notify of its success.
26+
@available(*, noasync, message: "Use the async alternative")
2627
func extract(
2728
from archivePath: AbsolutePath,
2829
to destinationPath: AbsolutePath,
@@ -35,6 +36,7 @@ public protocol Archiver {
3536
/// - directory: The `AbsolutePath` to the archive to extract.
3637
/// - destinationPath: The `AbsolutePath` to the directory to extract to.
3738
/// - completion: The completion handler that will be called when the operation finishes to notify of its success.
39+
@available(*, noasync, message: "Use the async alternative")
3840
func compress(
3941
directory: AbsolutePath,
4042
to destinationPath: AbsolutePath,
@@ -46,6 +48,7 @@ public protocol Archiver {
4648
/// - Parameters:
4749
/// - path: The `AbsolutePath` to the archive to validate.
4850
/// - completion: The completion handler that will be called when the operation finishes to notify of its success.
51+
@available(*, noasync, message: "Use the async alternative")
4952
func validate(
5053
path: AbsolutePath,
5154
completion: @escaping (Result<Bool, Error>) -> Void
@@ -57,8 +60,25 @@ extension Archiver {
5760
from archivePath: AbsolutePath,
5861
to destinationPath: AbsolutePath
5962
) async throws {
60-
try await withCheckedThrowingContinuation {
61-
self.extract(from: archivePath, to: destinationPath, completion: $0.resume(with:))
63+
try await safe_async {
64+
self.extract(from: archivePath, to: destinationPath, completion: $0)
65+
}
66+
}
67+
68+
public func compress(
69+
directory: AbsolutePath,
70+
to: AbsolutePath
71+
) async throws {
72+
try await safe_async {
73+
self.compress(directory: directory, to: to, completion: $0)
74+
}
75+
}
76+
77+
public func validate(
78+
path: AbsolutePath
79+
) async throws -> Bool {
80+
try await safe_async {
81+
self.validate(path: path, completion: $0)
6282
}
6383
}
6484
}

Sources/Basics/AuthorizationProvider.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public protocol AuthorizationProvider {
2323
}
2424

2525
public protocol AuthorizationWriter {
26+
@available(*, noasync, message: "Use the async alternative")
2627
func addOrUpdate(
2728
for url: URL,
2829
user: String,
@@ -31,9 +32,34 @@ public protocol AuthorizationWriter {
3132
callback: @escaping (Result<Void, Error>) -> Void
3233
)
3334

35+
@available(*, noasync, message: "Use the async alternative")
3436
func remove(for url: URL, callback: @escaping (Result<Void, Error>) -> Void)
3537
}
3638

39+
public extension AuthorizationWriter {
40+
func addOrUpdate(
41+
for url: URL,
42+
user: String,
43+
password: String,
44+
persist: Bool = true
45+
) async throws {
46+
try await safe_async {
47+
self.addOrUpdate(
48+
for: url,
49+
user: user,
50+
password: password,
51+
persist: persist,
52+
callback: $0)
53+
}
54+
}
55+
56+
func remove(for url: URL) async throws {
57+
try await safe_async {
58+
self.remove(for: url, callback: $0)
59+
}
60+
}
61+
}
62+
3763
public enum AuthorizationProviderError: Error {
3864
case invalidURLHost
3965
case notFound

Sources/Basics/Concurrency/ConcurrencyHelpers.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import _Concurrency
1314
import Dispatch
1415
import class Foundation.NSLock
1516
import class Foundation.ProcessInfo
@@ -46,6 +47,34 @@ extension DispatchQueue {
4647
)
4748
}
4849

50+
/// Bridges between potentially blocking methods that take a result completion closure and async/await
51+
public func safe_async<T, ErrorType: Error>(_ body: @escaping (@escaping (Result<T, ErrorType>) -> Void) -> Void) async throws -> T {
52+
try await withCheckedThrowingContinuation { continuation in
53+
// It is possible that body make block indefinitely on a lock, sempahore,
54+
// or similar then synchrously call the completion handler. For full safety
55+
// it is essential to move the execution off the swift concurrency pool
56+
DispatchQueue.sharedConcurrent.async {
57+
body {
58+
continuation.resume(with: $0)
59+
}
60+
}
61+
}
62+
}
63+
64+
/// Bridges between potentially blocking methods that take a result completion closure and async/await
65+
public func safe_async<T>(_ body: @escaping (@escaping (Result<T, Never>) -> Void) -> Void) async -> T {
66+
await withCheckedContinuation { continuation in
67+
// It is possible that body make block indefinitely on a lock, sempahore,
68+
// or similar then synchrously call the completion handler. For full safety
69+
// it is essential to move the execution off the swift concurrency pool
70+
DispatchQueue.sharedConcurrent.async {
71+
body {
72+
continuation.resume(with: $0)
73+
}
74+
}
75+
}
76+
}
77+
4978
#if !canImport(Darwin)
5079
// As of Swift 5.7 and 5.8 swift-corelibs-foundation doesn't have `Sendable` annotations yet.
5180
extension URL: @unchecked Sendable {}

Sources/SPMTestSupport/XCTAssertHelpers.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,23 @@ public func XCTSkipIfCI(file: StaticString = #filePath, line: UInt = #line) thro
4949
}
5050
}
5151

52+
/// An `async`-friendly replacement for `XCTAssertThrowsError`.
53+
public func XCTAssertAsyncThrowsError<T>(
54+
_ expression: @autoclosure () async throws -> T,
55+
_ message: @autoclosure () -> String = "",
56+
file: StaticString = #filePath,
57+
line: UInt = #line,
58+
_ errorHandler: (_ error: any Error) -> Void = { _ in }
59+
) async {
60+
do {
61+
_ = try await expression()
62+
XCTFail(message(), file: file, line: line)
63+
} catch {
64+
errorHandler(error)
65+
}
66+
}
67+
68+
5269
public func XCTAssertBuilds(
5370
_ path: AbsolutePath,
5471
configurations: Set<Configuration> = [.Debug, .Release],
@@ -133,6 +150,26 @@ public func XCTAssertEqual<T: CustomStringConvertible>(
133150
XCTAssertEqual(actual, expected, file: file, line: line)
134151
}
135152

153+
public func XCTAssertAsyncTrue(
154+
_ expression: @autoclosure () async throws -> Bool,
155+
_ message: @autoclosure () -> String = "",
156+
file: StaticString = #filePath,
157+
line: UInt = #line
158+
) async rethrows {
159+
let result = try await expression()
160+
XCTAssertTrue(result, message(), file: file, line: line)
161+
}
162+
163+
public func XCTAssertAsyncFalse(
164+
_ expression: @autoclosure () async throws -> Bool,
165+
_ message: @autoclosure () -> String = "",
166+
file: StaticString = #filePath,
167+
line: UInt = #line
168+
) async rethrows {
169+
let result = try await expression()
170+
XCTAssertFalse(result, message(), file: file, line: line)
171+
}
172+
136173
public func XCTAssertThrowsCommandExecutionError<T>(
137174
_ expression: @autoclosure () throws -> T,
138175
_ message: @autoclosure () -> String = "",

Tests/BasicsTests/Archiver/TarArchiverTests.swift

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,51 +19,51 @@ import class TSCBasic.InMemoryFileSystem
1919
import struct TSCBasic.FileSystemError
2020

2121
final class TarArchiverTests: XCTestCase {
22-
func testSuccess() throws {
23-
try testWithTemporaryDirectory { tmpdir in
22+
func testSuccess() async throws {
23+
try await testWithTemporaryDirectory { tmpdir in
2424
let archiver = TarArchiver(fileSystem: localFileSystem)
2525
let inputArchivePath = AbsolutePath(#file).parentDirectory
2626
.appending(components: "Inputs", "archive.tar.gz")
27-
try archiver.extract(from: inputArchivePath, to: tmpdir)
27+
try await archiver.extract(from: inputArchivePath, to: tmpdir)
2828
let content = tmpdir.appending("file")
2929
XCTAssert(localFileSystem.exists(content))
3030
XCTAssertEqual((try? localFileSystem.readFileContents(content))?.cString, "Hello World!")
3131
}
3232
}
3333

34-
func testArchiveDoesntExist() {
34+
func testArchiveDoesntExist() async {
3535
let fileSystem = InMemoryFileSystem()
3636
let archiver = TarArchiver(fileSystem: fileSystem)
3737
let archive = AbsolutePath("/archive.tar.gz")
38-
XCTAssertThrowsError(try archiver.extract(from: archive, to: "/")) { error in
38+
await XCTAssertAsyncThrowsError(try await archiver.extract(from: archive, to: "/")) { error in
3939
XCTAssertEqual(error as? FileSystemError, FileSystemError(.noEntry, archive))
4040
}
4141
}
4242

43-
func testDestinationDoesntExist() throws {
43+
func testDestinationDoesntExist() async throws {
4444
let fileSystem = InMemoryFileSystem(emptyFiles: "/archive.tar.gz")
4545
let archiver = TarArchiver(fileSystem: fileSystem)
4646
let destination = AbsolutePath("/destination")
47-
XCTAssertThrowsError(try archiver.extract(from: "/archive.tar.gz", to: destination)) { error in
47+
await XCTAssertAsyncThrowsError(try await archiver.extract(from: "/archive.tar.gz", to: destination)) { error in
4848
XCTAssertEqual(error as? FileSystemError, FileSystemError(.notDirectory, destination))
4949
}
5050
}
5151

52-
func testDestinationIsFile() throws {
52+
func testDestinationIsFile() async {
5353
let fileSystem = InMemoryFileSystem(emptyFiles: "/archive.tar.gz", "/destination")
5454
let archiver = TarArchiver(fileSystem: fileSystem)
5555
let destination = AbsolutePath("/destination")
56-
XCTAssertThrowsError(try archiver.extract(from: "/archive.tar.gz", to: destination)) { error in
56+
await XCTAssertAsyncThrowsError(try await archiver.extract(from: "/archive.tar.gz", to: destination)) { error in
5757
XCTAssertEqual(error as? FileSystemError, FileSystemError(.notDirectory, destination))
5858
}
5959
}
6060

61-
func testInvalidArchive() throws {
62-
try testWithTemporaryDirectory { tmpdir in
61+
func testInvalidArchive() async throws {
62+
try await testWithTemporaryDirectory { tmpdir in
6363
let archiver = TarArchiver(fileSystem: localFileSystem)
6464
let inputArchivePath = AbsolutePath(#file).parentDirectory
6565
.appending(components: "Inputs", "invalid_archive.tar.gz")
66-
XCTAssertThrowsError(try archiver.extract(from: inputArchivePath, to: tmpdir)) { error in
66+
await XCTAssertAsyncThrowsError(try await archiver.extract(from: inputArchivePath, to: tmpdir)) { error in
6767
#if os(Linux)
6868
XCTAssertMatch((error as? StringError)?.description, .contains("not in gzip format"))
6969
#else
@@ -73,39 +73,39 @@ final class TarArchiverTests: XCTestCase {
7373
}
7474
}
7575

76-
func testValidation() throws {
76+
func testValidation() async throws {
7777
// valid
78-
try testWithTemporaryDirectory { _ in
78+
try await testWithTemporaryDirectory { _ in
7979
let archiver = TarArchiver(fileSystem: localFileSystem)
8080
let path = AbsolutePath(#file).parentDirectory
8181
.appending(components: "Inputs", "archive.tar.gz")
82-
XCTAssertTrue(try archiver.validate(path: path))
82+
try await XCTAssertAsyncTrue(try await archiver.validate(path: path))
8383
}
8484
// invalid
85-
try testWithTemporaryDirectory { _ in
85+
try await testWithTemporaryDirectory { _ in
8686
let archiver = TarArchiver(fileSystem: localFileSystem)
8787
let path = AbsolutePath(#file).parentDirectory
8888
.appending(components: "Inputs", "invalid_archive.tar.gz")
89-
XCTAssertFalse(try archiver.validate(path: path))
89+
try await XCTAssertAsyncFalse(try await archiver.validate(path: path))
9090
}
9191
// error
92-
try testWithTemporaryDirectory { _ in
92+
try await testWithTemporaryDirectory { _ in
9393
let archiver = TarArchiver(fileSystem: localFileSystem)
9494
let path = AbsolutePath.root.appending("does_not_exist.tar.gz")
95-
XCTAssertThrowsError(try archiver.validate(path: path)) { error in
95+
await XCTAssertAsyncThrowsError(try await archiver.validate(path: path)) { error in
9696
XCTAssertEqual(error as? FileSystemError, FileSystemError(.noEntry, path))
9797
}
9898
}
9999
}
100100

101-
func testCompress() throws {
101+
func testCompress() async throws {
102102
#if os(Linux)
103103
guard SPM_posix_spawn_file_actions_addchdir_np_supported() else {
104104
throw XCTSkip("working directory not supported on this platform")
105105
}
106106
#endif
107107

108-
try testWithTemporaryDirectory { tmpdir in
108+
try await testWithTemporaryDirectory { tmpdir in
109109
let archiver = TarArchiver(fileSystem: localFileSystem)
110110

111111
let rootDir = tmpdir.appending(component: UUID().uuidString)
@@ -122,12 +122,12 @@ final class TarArchiverTests: XCTestCase {
122122
try localFileSystem.writeFileContents(dir2.appending("file4.txt"), string: "Hello World 4!")
123123

124124
let archivePath = tmpdir.appending(component: UUID().uuidString + ".tar.gz")
125-
try archiver.compress(directory: rootDir, to: archivePath)
125+
try await archiver.compress(directory: rootDir, to: archivePath)
126126
XCTAssertFileExists(archivePath)
127127

128128
let extractRootDir = tmpdir.appending(component: UUID().uuidString)
129129
try localFileSystem.createDirectory(extractRootDir)
130-
try archiver.extract(from: archivePath, to: extractRootDir)
130+
try await archiver.extract(from: archivePath, to: extractRootDir)
131131
try localFileSystem.stripFirstLevel(of: extractRootDir)
132132

133133
XCTAssertFileExists(extractRootDir.appending("file1.txt"))

0 commit comments

Comments
 (0)