Skip to content

Commit

Permalink
Replace runThrowingTask with syncAwait (#703)
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-fowler authored Dec 11, 2023
1 parent b6af7b9 commit 98ff727
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 81 deletions.
24 changes: 14 additions & 10 deletions Tests/SotoTests/Services/APIGateway/APIGatewayTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,23 @@ class APIGatewayTests: XCTestCase {
} else {
print("Connecting to AWS")
}
/// If we create a rest api for each test, when we delete them APIGateway will
/// throttle and we will most likely not delete the all APIs so we create one API to be used by all tests
XCTAssertNoThrow(try runThrowingTask(on: self.client.eventLoopGroup.any()) {
let response = try await self.createRestApi(name: self.restApiName)
Self.restApiId = try XCTUnwrap(response.id)
})
Task {
/// If we create a rest api for each test, when we delete them APIGateway will
/// throttle and we will most likely not delete the all APIs so we create one API to be used by all tests
await XCTAsyncAssertNoThrow {
let response = try await self.createRestApi(name: self.restApiName)
Self.restApiId = try XCTUnwrap(response.id)
}
}.syncAwait()
}

override class func tearDown() {
XCTAssertNoThrow(try runThrowingTask(on: self.client.eventLoopGroup.any()) {
_ = try await self.deleteRestApi(id: self.restApiId)
})
XCTAssertNoThrow(try self.client.syncShutdown())
Task {
await XCTAsyncAssertNoThrow {
_ = try await self.deleteRestApi(id: self.restApiId)
try await self.client.shutdown()
}
}.syncAwait()
}

static func createRestApi(name: String) async throws -> APIGateway.RestApi {
Expand Down
26 changes: 15 additions & 11 deletions Tests/SotoTests/Services/ApiGatewayV2/APIGatewayV2Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,26 @@ class APIGatewayV2Tests: XCTestCase {
}
/// If we create a rest api for each test, when we delete them APIGateway will throttle
/// and we will most likely not delete the all APIs so we create one API to be used by all tests
XCTAssertNoThrow(try runThrowingTask(on: Self.client.eventLoopGroup.any()) {
do {
Self.restApiId = try await self.createRestApi(name: self.restApiName)
} catch {
print("Failed to create APIGateway rest api, error: \(error)")
throw error
Task {
await XCTAsyncAssertNoThrow {
do {
Self.restApiId = try await self.createRestApi(name: self.restApiName)
} catch {
print("Failed to create APIGateway rest api, error: \(error)")
throw error
}
}
})
}.syncAwait()
}

override class func tearDown() {
guard !TestEnvironment.isUsingLocalstack else { return }
XCTAssertNoThrow(try runThrowingTask(on: Self.client.eventLoopGroup.any()) {
_ = try await self.deleteRestApi(id: self.restApiId)
})
XCTAssertNoThrow(try self.client.syncShutdown())
Task {
await XCTAsyncAssertNoThrow {
_ = try await self.deleteRestApi(id: self.restApiId)
try await self.client.shutdown()
}
}.syncAwait()
}

static func createRestApi(name: String) async throws -> String {
Expand Down
52 changes: 28 additions & 24 deletions Tests/SotoTests/Services/DynamoDB/DynamoDBTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,35 +39,39 @@ class DynamoDBTests: XCTestCase {
)
/// If we create a rest api for each test, when we delete them APIGateway will
/// throttle and we will most likely not delete the all APIs so we create one API to be used by all tests
XCTAssertNoThrow(try runThrowingTask(on: self.client.eventLoopGroup.any()) {
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
self.tableName = TestEnvironment.generateResourceName("soto-dynamodb-tests")
_ = try await Self.createTable(
name: self.tableName,
attributeDefinitions: [.init(attributeName: "id", attributeType: .s)],
keySchema: [.init(attributeName: "id", keyType: .hash)]
)
Task {
await XCTAsyncAssertNoThrow {
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
self.tableName = TestEnvironment.generateResourceName("soto-dynamodb-tests")
_ = try await Self.createTable(
name: self.tableName,
attributeDefinitions: [.init(attributeName: "id", attributeType: .s)],
keySchema: [.init(attributeName: "id", keyType: .hash)]
)
}
group.addTask {
self.tableWithValueName = TestEnvironment.generateResourceName("soto-dynamodb-tests_value")
_ = try await Self.createTable(
name: self.tableWithValueName,
attributeDefinitions: [.init(attributeName: "id", attributeType: .s), .init(attributeName: "version", attributeType: .n)],
keySchema: [.init(attributeName: "id", keyType: .hash), .init(attributeName: "version", keyType: .range)]
)
}
try await group.waitForAll()
}
group.addTask {
self.tableWithValueName = TestEnvironment.generateResourceName("soto-dynamodb-tests_value")
_ = try await Self.createTable(
name: self.tableWithValueName,
attributeDefinitions: [.init(attributeName: "id", attributeType: .s), .init(attributeName: "version", attributeType: .n)],
keySchema: [.init(attributeName: "id", keyType: .hash), .init(attributeName: "version", keyType: .range)]
)
}
try await group.waitForAll()
}
})
}.syncAwait()
}

override class func tearDown() {
XCTAssertNoThrow(try runThrowingTask(on: self.client.eventLoopGroup.any()) {
_ = try await Self.deleteTable(name: self.tableName)
_ = try await Self.deleteTable(name: self.tableWithValueName)
})
XCTAssertNoThrow(try Self.client.syncShutdown())
Task {
await XCTAsyncAssertNoThrow {
_ = try await Self.deleteTable(name: self.tableName)
_ = try await Self.deleteTable(name: self.tableWithValueName)
try await Self.client.shutdown()
}
}.syncAwait()
}

static func createTable(
Expand Down
35 changes: 19 additions & 16 deletions Tests/SotoTests/Services/Lambda/LambdaTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,27 +122,30 @@ class LambdaTests: XCTestCase {
guard !TestEnvironment.isUsingLocalstack else { return }

// create an IAM role
XCTAssertNoThrow(try runThrowingTask(on: self.client.eventLoopGroup.any()) {
let response = try await Self.createIAMRole()
// IAM needs some time after Role creation,
// before the role can be attached to a Lambda function
// https://stackoverflow.com/a/37438525/663360
print("Sleeping 20 secs, waiting for IAM Role to be ready")
try await Task.sleep(nanoseconds: 20_000_000_000)
try await Self.createLambdaFunction(roleArn: response.role.arn)

})
Task {
await XCTAsyncAssertNoThrow {
let response = try await Self.createIAMRole()
// IAM needs some time after Role creation,
// before the role can be attached to a Lambda function
// https://stackoverflow.com/a/37438525/663360
print("Sleeping 20 secs, waiting for IAM Role to be ready")
try await Task.sleep(nanoseconds: 20_000_000_000)
try await Self.createLambdaFunction(roleArn: response.role.arn)
}
}.syncAwait()
}

override class func tearDown() {
// Role and lambda function are not created with Localstack
XCTAssertNoThrow(try runThrowingTask(on: self.client.eventLoopGroup.any()) {
if !TestEnvironment.isUsingLocalstack {
try await Self.deleteLambdaFunction()
try await Self.deleteIAMRole()
Task {
await XCTAsyncAssertNoThrow {
if !TestEnvironment.isUsingLocalstack {
try await Self.deleteLambdaFunction()
try await Self.deleteIAMRole()
}
try await Self.client.shutdown()
}
})
XCTAssertNoThrow(try Self.client.syncShutdown())
}.syncAwait()
}

// MARK: TESTS
Expand Down
81 changes: 61 additions & 20 deletions Tests/SotoTests/test.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,56 @@ import NIOCore
@testable import SotoCore
import XCTest

@available(*, noasync, message: "runThrowingTask() can block indefinitely")
func runThrowingTask<T>(on eventLoop: EventLoop, _ task: @escaping @Sendable () async throws -> T) throws -> T {
let promise = eventLoop.makePromise(of: T.self)
Task {
do {
let result = try await task()
promise.succeed(result)
} catch {
promise.fail(error)
/// Internal class used by syncAwait
private class SendableBox<Value>: @unchecked Sendable {
var value: Value?
}

extension Task where Failure == Error {
/// Performs an async task in a sync context and wait for result.
///
/// Not to be used in production code.
///
/// - Note: This function blocks the thread until the given operation is finished. The caller is responsible for managing multithreading.
@available(*, noasync, message: "synchronous() can block indefinitely")
internal func syncAwait() throws -> Success {
let semaphore = DispatchSemaphore(value: 0)
let resultBox = SendableBox<Result<Success, Failure>>()

Task<Void, Never> {
resultBox.value = await self.result
semaphore.signal()
}

semaphore.wait()
switch resultBox.value! {
case .success(let value):
return value
case .failure(let error):
throw error
}
}
return try promise.futureResult.wait()
}

@available(*, noasync, message: "runTask() can block indefinitely")
func runTask<T>(on eventLoop: EventLoop, _ task: @escaping @Sendable () async -> T) -> T {
let promise = eventLoop.makePromise(of: T.self)
Task {
let result = await task()
promise.succeed(result)
extension Task where Failure == Never {
/// Performs an async task in a sync context and wait for result.
///
/// Not to be used in production code.
///
/// - Note: This function blocks the thread until the given operation is finished. The caller is responsible for managing multithreading.
@available(*, noasync, message: "synchronous() can block indefinitely")
internal func syncAwait() -> Success {
let semaphore = DispatchSemaphore(value: 0)
let resultBox = SendableBox<Success>()

Task<Void, Never> {
resultBox.value = await self.value
semaphore.signal()
}

semaphore.wait()
return resultBox.value!
}
return try! promise.futureResult.wait()
}

/// Provide various test environment variables
Expand Down Expand Up @@ -96,20 +124,33 @@ func XCTTestAsset<T>(
try await delete(asset)
}

func XCTAsyncAssertNoThrow(
_ expression: () async throws -> Void,
_ message: @autoclosure @escaping () -> String = "",
file: StaticString = #filePath,
line: UInt = #line
) async {
do {
_ = try await expression()
} catch {
XCTFail("\(file):\(line) \(message()) Threw error \(error)")
}
}

/// Test for specific error being thrown when running some code
func XCTAsyncExpectError<E: Error & Equatable>(
_ expectedError: E,
_ expression: () async throws -> Void,
_ message: @autoclosure () -> String = "",
_ message: @autoclosure @escaping () -> String = "",
file: StaticString = #filePath,
line: UInt = #line
) async {
do {
_ = try await expression()
XCTFail("\(file):\(line) was expected to throw an error but it didn't")
XCTFail("\(file):\(line) \(message()) Expected to throw an error but it didn't")
} catch let error as E where error == expectedError {
} catch {
XCTFail("\(file):\(line) expected error \(expectedError) but got \(error)")
XCTFail("\(file):\(line) \(message()) Expected error \(expectedError) but got \(error)")
}
}

Expand Down

0 comments on commit 98ff727

Please sign in to comment.