diff --git a/Examples/LambdaFunctions/Sources/CurrencyExchange/CurrencyExchangeHandler.swift b/Examples/LambdaFunctions/Sources/CurrencyExchange/CurrencyExchangeHandler.swift index 85a48f4f..2df62afa 100644 --- a/Examples/LambdaFunctions/Sources/CurrencyExchange/CurrencyExchangeHandler.swift +++ b/Examples/LambdaFunctions/Sources/CurrencyExchange/CurrencyExchangeHandler.swift @@ -24,7 +24,7 @@ import Logging // MARK: - Run Lambda @main -struct CurrencyExchangeHandler: AsyncLambdaHandler { +struct CurrencyExchangeHandler: LambdaHandler { typealias In = Request typealias Out = [Exchange] diff --git a/Examples/LambdaFunctions/Sources/ErrorHandling/ErrorsHappenHandler.swift b/Examples/LambdaFunctions/Sources/ErrorHandling/ErrorsHappenHandler.swift index 866b7322..10f5cfd5 100644 --- a/Examples/LambdaFunctions/Sources/ErrorHandling/ErrorsHappenHandler.swift +++ b/Examples/LambdaFunctions/Sources/ErrorHandling/ErrorsHappenHandler.swift @@ -17,7 +17,7 @@ import AWSLambdaRuntime // MARK: - Run Lambda @main -struct ErrorsHappenHandler: AsyncLambdaHandler { +struct ErrorsHappenHandler: LambdaHandler { typealias In = Request typealias Out = Response diff --git a/Examples/LambdaFunctions/Sources/HelloWorld/HelloWorldHandler.swift b/Examples/LambdaFunctions/Sources/HelloWorld/HelloWorldHandler.swift index 8a0d7ce6..06214244 100644 --- a/Examples/LambdaFunctions/Sources/HelloWorld/HelloWorldHandler.swift +++ b/Examples/LambdaFunctions/Sources/HelloWorld/HelloWorldHandler.swift @@ -16,7 +16,7 @@ import AWSLambdaRuntime // introductory example, the obligatory "hello, world!" @main -struct HelloWorldHandler: AsyncLambdaHandler { +struct HelloWorldHandler: LambdaHandler { typealias In = String typealias Out = String diff --git a/Examples/LocalDebugging/MyLambda/Sources/MyLambda/MyLambdaHandler.swift b/Examples/LocalDebugging/MyLambda/Sources/MyLambda/MyLambdaHandler.swift index f078371f..0c2225fb 100644 --- a/Examples/LocalDebugging/MyLambda/Sources/MyLambda/MyLambdaHandler.swift +++ b/Examples/LocalDebugging/MyLambda/Sources/MyLambda/MyLambdaHandler.swift @@ -18,7 +18,7 @@ import Shared // set LOCAL_LAMBDA_SERVER_ENABLED env variable to "true" to start // a local server simulator which will allow local debugging @main -struct MyLambdaHandler: AsyncLambdaHandler { +struct MyLambdaHandler: LambdaHandler { typealias In = Request typealias Out = Response diff --git a/Sources/AWSLambdaRuntime/Lambda+Codable.swift b/Sources/AWSLambdaRuntime/Lambda+Codable.swift index 0175c8af..960e068e 100644 --- a/Sources/AWSLambdaRuntime/Lambda+Codable.swift +++ b/Sources/AWSLambdaRuntime/Lambda+Codable.swift @@ -19,65 +19,6 @@ import class Foundation.JSONEncoder import NIOCore import NIOFoundationCompat -/// Extension to the `Lambda` companion to enable execution of Lambdas that take and return `Codable` events. -extension Lambda { - /// An asynchronous Lambda Closure that takes a `In: Decodable` and returns a `Result` via a completion handler. - public typealias CodableClosure = (Lambda.Context, In, @escaping (Result) -> Void) -> Void - - /// Run a Lambda defined by implementing the `CodableClosure` function. - /// - /// - parameters: - /// - closure: `CodableClosure` based Lambda. - /// - /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - public static func run(_ closure: @escaping CodableClosure) { - self.run(CodableClosureWrapper(closure)) - } - - /// An asynchronous Lambda Closure that takes a `In: Decodable` and returns a `Result` via a completion handler. - public typealias CodableVoidClosure = (Lambda.Context, In, @escaping (Result) -> Void) -> Void - - /// Run a Lambda defined by implementing the `CodableVoidClosure` function. - /// - /// - parameters: - /// - closure: `CodableVoidClosure` based Lambda. - /// - /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - public static func run(_ closure: @escaping CodableVoidClosure) { - self.run(CodableVoidClosureWrapper(closure)) - } -} - -internal struct CodableClosureWrapper: LambdaHandler { - typealias In = In - typealias Out = Out - - private let closure: Lambda.CodableClosure - - init(_ closure: @escaping Lambda.CodableClosure) { - self.closure = closure - } - - func handle(context: Lambda.Context, event: In, callback: @escaping (Result) -> Void) { - self.closure(context, event, callback) - } -} - -internal struct CodableVoidClosureWrapper: LambdaHandler { - typealias In = In - typealias Out = Void - - private let closure: Lambda.CodableVoidClosure - - init(_ closure: @escaping Lambda.CodableVoidClosure) { - self.closure = closure - } - - func handle(context: Lambda.Context, event: In, callback: @escaping (Result) -> Void) { - self.closure(context, event, callback) - } -} - // MARK: - Codable support /// Implementation of a`ByteBuffer` to `In` decoding diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+String.swift b/Sources/AWSLambdaRuntimeCore/Lambda+String.swift index a96b6add..800afc15 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda+String.swift +++ b/Sources/AWSLambdaRuntimeCore/Lambda+String.swift @@ -13,79 +13,6 @@ //===----------------------------------------------------------------------===// import NIOCore -/// Extension to the `Lambda` companion to enable execution of Lambdas that take and return `String` events. -extension Lambda { - /// An asynchronous Lambda Closure that takes a `String` and returns a `Result` via a completion handler. - public typealias StringClosure = (Lambda.Context, String, @escaping (Result) -> Void) -> Void - - /// Run a Lambda defined by implementing the `StringClosure` function. - /// - /// - parameters: - /// - closure: `StringClosure` based Lambda. - /// - /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - public static func run(_ closure: @escaping StringClosure) { - if case .failure(let error) = self.run(closure: closure) { - fatalError("\(error)") - } - } - - /// An asynchronous Lambda Closure that takes a `String` and returns a `Result` via a completion handler. - public typealias StringVoidClosure = (Lambda.Context, String, @escaping (Result) -> Void) -> Void - - /// Run a Lambda defined by implementing the `StringVoidClosure` function. - /// - /// - parameters: - /// - closure: `StringVoidClosure` based Lambda. - /// - /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - public static func run(_ closure: @escaping StringVoidClosure) { - if case .failure(let error) = self.run(closure: closure) { - fatalError("\(error)") - } - } - - // for testing - internal static func run(configuration: Configuration = .init(), closure: @escaping StringClosure) -> Result { - self.run(configuration: configuration, handler: StringClosureWrapper(closure)) - } - - // for testing - internal static func run(configuration: Configuration = .init(), closure: @escaping StringVoidClosure) -> Result { - self.run(configuration: configuration, handler: StringVoidClosureWrapper(closure)) - } -} - -internal struct StringClosureWrapper: LambdaHandler { - typealias In = String - typealias Out = String - - private let closure: Lambda.StringClosure - - init(_ closure: @escaping Lambda.StringClosure) { - self.closure = closure - } - - func handle(context: Lambda.Context, event: In, callback: @escaping (Result) -> Void) { - self.closure(context, event, callback) - } -} - -internal struct StringVoidClosureWrapper: LambdaHandler { - typealias In = String - typealias Out = Void - - private let closure: Lambda.StringVoidClosure - - init(_ closure: @escaping Lambda.StringVoidClosure) { - self.closure = closure - } - - func handle(context: Lambda.Context, event: In, callback: @escaping (Result) -> Void) { - self.closure(context, event, callback) - } -} - extension EventLoopLambdaHandler where In == String { /// Implementation of a `ByteBuffer` to `String` decoding @inlinable diff --git a/Sources/AWSLambdaRuntimeCore/Lambda.swift b/Sources/AWSLambdaRuntimeCore/Lambda.swift index c1eccaeb..4a9267e8 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda.swift +++ b/Sources/AWSLambdaRuntimeCore/Lambda.swift @@ -32,18 +32,6 @@ public enum Lambda { /// A function that takes a `InitializationContext` and returns an `EventLoopFuture` of a `ByteBufferLambdaHandler` public typealias HandlerFactory = (InitializationContext) -> EventLoopFuture - /// Run a Lambda defined by implementing the `LambdaHandler` protocol. - /// - /// - parameters: - /// - handler: `ByteBufferLambdaHandler` based Lambda. - /// - /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - public static func run(_ handler: Handler) { - if case .failure(let error) = self.run(handler: handler) { - fatalError("\(error)") - } - } - /// Run a Lambda defined by implementing the `LambdaHandler` protocol provided via a `LambdaHandlerFactory`. /// Use this to initialize all your resources that you want to cache between invocations. This could be database connections and HTTP clients for example. /// It is encouraged to use the given `EventLoop`'s conformance to `EventLoopGroup` when initializing NIO dependencies. This will improve overall performance. @@ -58,18 +46,6 @@ public enum Lambda { } } - /// Run a Lambda defined by implementing the `LambdaHandler` protocol provided via a factory, typically a constructor. - /// - /// - parameters: - /// - factory: A `ByteBufferLambdaHandler` factory. - /// - /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - public static func run(_ factory: @escaping (InitializationContext) throws -> Handler) { - if case .failure(let error) = self.run(factory: factory) { - fatalError("\(error)") - } - } - /// Utility to access/read environment variables public static func env(_ name: String) -> String? { guard let value = getenv(name) else { @@ -78,27 +54,19 @@ public enum Lambda { return String(cString: value) } + #if swift(>=5.5) // for testing and internal use - internal static func run(configuration: Configuration = .init(), handler: Handler) -> Result { - self.run(configuration: configuration, factory: { $0.eventLoop.makeSucceededFuture(handler) }) - } - - // for testing and internal use - internal static func run(configuration: Configuration = .init(), factory: @escaping (InitializationContext) throws -> Handler) -> Result { - self.run(configuration: configuration, factory: { context -> EventLoopFuture in - let promise = context.eventLoop.makePromise(of: Handler.self) - // if we have a callback based handler factory, we offload the creation of the handler - // onto the default offload queue, to ensure that the eventloop is never blocked. - Lambda.defaultOffloadQueue.async { - do { - promise.succeed(try factory(context)) - } catch { - promise.fail(error) - } + @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) + internal static func run(configuration: Configuration = .init(), handlerType: Handler.Type) -> Result { + self.run(configuration: configuration, factory: { context -> EventLoopFuture in + let promise = context.eventLoop.makePromise(of: ByteBufferLambdaHandler.self) + promise.completeWithTask { + try await Handler(context: context) } return promise.futureResult }) } + #endif // for testing and internal use internal static func run(configuration: Configuration = .init(), factory: @escaping HandlerFactory) -> Result { diff --git a/Sources/AWSLambdaRuntimeCore/LambdaContext.swift b/Sources/AWSLambdaRuntimeCore/LambdaContext.swift index c67e0e29..8b20a245 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaContext.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaContext.swift @@ -36,11 +36,23 @@ extension Lambda { /// `ByteBufferAllocator` to allocate `ByteBuffer` public let allocator: ByteBufferAllocator - internal init(logger: Logger, eventLoop: EventLoop, allocator: ByteBufferAllocator) { + init(logger: Logger, eventLoop: EventLoop, allocator: ByteBufferAllocator) { self.eventLoop = eventLoop self.logger = logger self.allocator = allocator } + + /// This interface is not part of the public API and must not be used by adopters. This API is not part of semver versioning. + public static func __forTestsOnly( + logger: Logger, + eventLoop: EventLoop + ) -> InitializationContext { + InitializationContext( + logger: logger, + eventLoop: eventLoop, + allocator: ByteBufferAllocator() + ) + } } } @@ -138,24 +150,26 @@ extension Lambda { self.storage.allocator } - internal init(requestID: String, - traceID: String, - invokedFunctionARN: String, - deadline: DispatchWallTime, - cognitoIdentity: String? = nil, - clientContext: String? = nil, - logger: Logger, - eventLoop: EventLoop, - allocator: ByteBufferAllocator) { - self.storage = _Storage(requestID: requestID, - traceID: traceID, - invokedFunctionARN: invokedFunctionARN, - deadline: deadline, - cognitoIdentity: cognitoIdentity, - clientContext: clientContext, - logger: logger, - eventLoop: eventLoop, - allocator: allocator) + init(requestID: String, + traceID: String, + invokedFunctionARN: String, + deadline: DispatchWallTime, + cognitoIdentity: String? = nil, + clientContext: String? = nil, + logger: Logger, + eventLoop: EventLoop, + allocator: ByteBufferAllocator) { + self.storage = _Storage( + requestID: requestID, + traceID: traceID, + invokedFunctionARN: invokedFunctionARN, + deadline: deadline, + cognitoIdentity: cognitoIdentity, + clientContext: clientContext, + logger: logger, + eventLoop: eventLoop, + allocator: allocator + ) } public func getRemainingTime() -> TimeAmount { @@ -169,6 +183,26 @@ extension Lambda { public var debugDescription: String { "\(Self.self)(requestID: \(self.requestID), traceID: \(self.traceID), invokedFunctionARN: \(self.invokedFunctionARN), cognitoIdentity: \(self.cognitoIdentity ?? "nil"), clientContext: \(self.clientContext ?? "nil"), deadline: \(self.deadline))" } + + /// This interface is not part of the public API and must not be used by adopters. This API is not part of semver versioning. + public static func __forTestsOnly( + requestID: String, + traceID: String, + invokedFunctionARN: String, + timeout: DispatchTimeInterval, + logger: Logger, + eventLoop: EventLoop + ) -> Context { + Context( + requestID: requestID, + traceID: traceID, + invokedFunctionARN: invokedFunctionARN, + deadline: .now() + timeout, + logger: logger, + eventLoop: eventLoop, + allocator: ByteBufferAllocator() + ) + } } } diff --git a/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift b/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift index ec6914f6..5411de01 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift @@ -18,80 +18,10 @@ import NIOCore // MARK: - LambdaHandler -/// Strongly typed, callback based processing protocol for a Lambda that takes a user defined `In` and returns a user defined `Out` asynchronously. -/// `LambdaHandler` implements `EventLoopLambdaHandler`, performing callback to `EventLoopFuture` mapping, over a `DispatchQueue` for safety. -/// -/// - note: To implement a Lambda, implement either `LambdaHandler` or the `EventLoopLambdaHandler` protocol. -/// The `LambdaHandler` will offload the Lambda execution to a `DispatchQueue` making processing safer but slower. -/// The `EventLoopLambdaHandler` will execute the Lambda on the same `EventLoop` as the core runtime engine, making the processing faster but requires -/// more care from the implementation to never block the `EventLoop`. -public protocol LambdaHandler: EventLoopLambdaHandler { - /// Defines to which `DispatchQueue` the Lambda execution is offloaded to. - var offloadQueue: DispatchQueue { get } - - /// The Lambda handling method - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - context: Runtime `Context`. - /// - event: Event of type `In` representing the event or request. - /// - callback: Completion handler to report the result of the Lambda back to the runtime engine. - /// The completion handler expects a `Result` with either a response of type `Out` or an `Error` - func handle(context: Lambda.Context, event: In, callback: @escaping (Result) -> Void) -} - -extension Lambda { - @usableFromInline - internal static let defaultOffloadQueue = DispatchQueue(label: "LambdaHandler.offload") -} - -extension LambdaHandler { - /// The queue on which `handle` is invoked on. - public var offloadQueue: DispatchQueue { - Lambda.defaultOffloadQueue - } - - /// `LambdaHandler` is offloading the processing to a `DispatchQueue` - /// This is slower but safer, in case the implementation blocks the `EventLoop` - /// Performance sensitive Lambdas should be based on `EventLoopLambdaHandler` which does not offload. - @inlinable - public func handle(context: Lambda.Context, event: In) -> EventLoopFuture { - let promise = context.eventLoop.makePromise(of: Out.self) - // FIXME: reusable DispatchQueue - self.offloadQueue.async { - self.handle(context: context, event: event, callback: promise.completeWith) - } - return promise.futureResult - } -} - -extension LambdaHandler { - public func shutdown(context: Lambda.ShutdownContext) -> EventLoopFuture { - let promise = context.eventLoop.makePromise(of: Void.self) - self.offloadQueue.async { - do { - try self.syncShutdown(context: context) - promise.succeed(()) - } catch { - promise.fail(error) - } - } - return promise.futureResult - } - - /// Clean up the Lambda resources synchronously. - /// Concrete Lambda handlers implement this method to shutdown resources like `HTTPClient`s and database connections. - public func syncShutdown(context: Lambda.ShutdownContext) throws { - // noop - } -} - -// MARK: - AsyncLambdaHandler - #if compiler(>=5.5) /// Strongly typed, processing protocol for a Lambda that takes a user defined `In` and returns a user defined `Out` async. @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -public protocol AsyncLambdaHandler: EventLoopLambdaHandler { +public protocol LambdaHandler: EventLoopLambdaHandler { /// The Lambda initialization method /// Use this method to initialize resources that will be used in every request. /// @@ -112,7 +42,7 @@ public protocol AsyncLambdaHandler: EventLoopLambdaHandler { } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -extension AsyncLambdaHandler { +extension LambdaHandler { public func handle(context: Lambda.Context, event: In) -> EventLoopFuture { let promise = context.eventLoop.makePromise(of: Out.self) promise.completeWithTask { @@ -123,15 +53,9 @@ extension AsyncLambdaHandler { } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -extension AsyncLambdaHandler { +extension LambdaHandler { public static func main() { - Lambda.run { context -> EventLoopFuture in - let promise = context.eventLoop.makePromise(of: ByteBufferLambdaHandler.self) - promise.completeWithTask { - try await Self(context: context) - } - return promise.futureResult - } + _ = Lambda.run(handlerType: Self.self) } } #endif diff --git a/Sources/AWSLambdaTesting/Lambda+Testing.swift b/Sources/AWSLambdaTesting/Lambda+Testing.swift index ab260bdb..6d0018c3 100644 --- a/Sources/AWSLambdaTesting/Lambda+Testing.swift +++ b/Sources/AWSLambdaTesting/Lambda+Testing.swift @@ -18,29 +18,33 @@ // For exmaple: // // func test() { -// struct MyLambda: EventLoopLambdaHandler { +// struct MyLambda: LambdaHandler { // typealias In = String // typealias Out = String // -// func handle(context: Lambda.Context, event: String) -> EventLoopFuture { -// return context.eventLoop.makeSucceededFuture("echo" + event) +// init(context: Lambda.InitializationContext) {} +// +// func handle(event: String, context: Lambda.Context) async throws -> String { +// "echo" + event // } // } // // let input = UUID().uuidString // var result: String? -// XCTAssertNoThrow(result = try Lambda.test(MyLambda(), with: input)) +// XCTAssertNoThrow(result = try Lambda.test(MyLambda.self, with: input)) // XCTAssertEqual(result, "echo" + input) // } -#if DEBUG -@testable import AWSLambdaRuntime -@testable import AWSLambdaRuntimeCore +#if swift(>=5.5) +import _NIOConcurrency +import AWSLambdaRuntime +import AWSLambdaRuntimeCore import Dispatch import Logging import NIOCore import NIOPosix +@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) extension Lambda { public struct TestConfig { public var requestID: String @@ -60,52 +64,37 @@ extension Lambda { } } - public static func test(_ closure: @escaping Lambda.StringClosure, - with event: String, - using config: TestConfig = .init()) throws -> String { - try Self.test(StringClosureWrapper(closure), with: event, using: config) - } - - public static func test(_ closure: @escaping Lambda.StringVoidClosure, - with event: String, - using config: TestConfig = .init()) throws { - _ = try Self.test(StringVoidClosureWrapper(closure), with: event, using: config) - } - - public static func test( - _ closure: @escaping Lambda.CodableClosure, - with event: In, - using config: TestConfig = .init() - ) throws -> Out { - try Self.test(CodableClosureWrapper(closure), with: event, using: config) - } - - public static func test( - _ closure: @escaping Lambda.CodableVoidClosure, - with event: In, + public static func test( + _ handlerType: Handler.Type, + with event: Handler.In, using config: TestConfig = .init() - ) throws { - _ = try Self.test(CodableVoidClosureWrapper(closure), with: event, using: config) - } - - public static func test( - _ handler: Handler, - with event: In, - using config: TestConfig = .init() - ) throws -> Out where Handler.In == In, Handler.Out == Out { + ) throws -> Handler.Out { let logger = Logger(label: "test") let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { try! eventLoopGroup.syncShutdownGracefully() } let eventLoop = eventLoopGroup.next() - let context = Context(requestID: config.requestID, - traceID: config.traceID, - invokedFunctionARN: config.invokedFunctionARN, - deadline: .now() + config.timeout, - logger: logger, - eventLoop: eventLoop, - allocator: ByteBufferAllocator()) + + let promise = eventLoop.makePromise(of: Handler.self) + let initContext = Lambda.InitializationContext.__forTestsOnly( + logger: logger, + eventLoop: eventLoop + ) + + let context = Lambda.Context.__forTestsOnly( + requestID: config.requestID, + traceID: config.traceID, + invokedFunctionARN: config.invokedFunctionARN, + timeout: config.timeout, + logger: logger, + eventLoop: eventLoop + ) + + promise.completeWithTask { + try await Handler(context: initContext) + } + let handler = try promise.futureResult.wait() return try eventLoop.flatSubmit { handler.handle(context: context, event: event) diff --git a/Sources/CodableSample/main.swift b/Sources/CodableSample/main.swift index aa2f577b..987ea832 100644 --- a/Sources/CodableSample/main.swift +++ b/Sources/CodableSample/main.swift @@ -35,7 +35,7 @@ struct Handler: EventLoopLambdaHandler { } } -Lambda.run(Handler()) +Lambda.run { $0.eventLoop.makeSucceededFuture(Handler()) } // MARK: - this can also be expressed as a closure: diff --git a/Sources/StringSample/main.swift b/Sources/StringSample/main.swift index 0577fdd7..599fa889 100644 --- a/Sources/StringSample/main.swift +++ b/Sources/StringSample/main.swift @@ -26,12 +26,4 @@ struct Handler: EventLoopLambdaHandler { } } -Lambda.run(Handler()) - -// MARK: - this can also be expressed as a closure: - -/* - Lambda.run { (_, event: String, callback) in - callback(.success(String(event.reversed()))) - } - */ +Lambda.run { $0.eventLoop.makeSucceededFuture(Handler()) } diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlerTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlerTest.swift index 747db169..acb3a071 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlerTest.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlerTest.swift @@ -17,79 +17,75 @@ import NIOCore import XCTest class LambdaHandlerTest: XCTestCase { - // MARK: - Callback + #if compiler(>=5.5) + + // MARK: - LambdaHandler - func testCallbackSuccess() { + @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) + func testBootstrapSuccess() { let server = MockLambdaServer(behavior: Behavior()) XCTAssertNoThrow(try server.start().wait()) defer { XCTAssertNoThrow(try server.stop().wait()) } - struct Handler: LambdaHandler { + struct TestBootstrapHandler: LambdaHandler { typealias In = String typealias Out = String - func handle(context: Lambda.Context, event: String, callback: (Result) -> Void) { - callback(.success(event)) + var initialized = false + + init(context: Lambda.InitializationContext) async throws { + XCTAssertFalse(self.initialized) + try await Task.sleep(nanoseconds: 100 * 1000 * 1000) // 0.1 seconds + self.initialized = true + } + + func handle(event: String, context: Lambda.Context) async throws -> String { + event } } - let maxTimes = Int.random(in: 1 ... 10) + let maxTimes = Int.random(in: 10 ... 20) let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: Handler()) + let result = Lambda.run(configuration: configuration, handlerType: TestBootstrapHandler.self) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } - func testVoidCallbackSuccess() { - let server = MockLambdaServer(behavior: Behavior(result: .success(nil))) + @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) + func testBootstrapFailure() { + let server = MockLambdaServer(behavior: FailedBootstrapBehavior()) XCTAssertNoThrow(try server.start().wait()) defer { XCTAssertNoThrow(try server.stop().wait()) } - struct Handler: LambdaHandler { + struct TestBootstrapHandler: LambdaHandler { typealias In = String typealias Out = Void - func handle(context: Lambda.Context, event: String, callback: (Result) -> Void) { - callback(.success(())) - } - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: Handler()) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testCallbackFailure() { - let server = MockLambdaServer(behavior: Behavior(result: .failure(TestError("boom")))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } + var initialized = false - struct Handler: LambdaHandler { - typealias In = String - typealias Out = String + init(context: Lambda.InitializationContext) async throws { + XCTAssertFalse(self.initialized) + try await Task.sleep(nanoseconds: 100 * 1000 * 1000) // 0.1 seconds + throw TestError("kaboom") + } - func handle(context: Lambda.Context, event: String, callback: (Result) -> Void) { - callback(.failure(TestError("boom"))) + func handle(event: String, context: Lambda.Context) async throws { + XCTFail("How can this be called if init failed") } } - let maxTimes = Int.random(in: 1 ... 10) + let maxTimes = Int.random(in: 10 ... 20) let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: Handler()) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) + let result = Lambda.run(configuration: configuration, handlerType: TestBootstrapHandler.self) + assertLambdaLifecycleResult(result, shouldFailWithError: TestError("kaboom")) } - #if compiler(>=5.5) - - // MARK: - AsyncLambdaHandler - @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) - func testAsyncHandlerSuccess() { + func testHandlerSuccess() { let server = MockLambdaServer(behavior: Behavior()) XCTAssertNoThrow(try server.start().wait()) defer { XCTAssertNoThrow(try server.stop().wait()) } - struct Handler: AsyncLambdaHandler { + struct Handler: LambdaHandler { typealias In = String typealias Out = String @@ -102,17 +98,17 @@ class LambdaHandlerTest: XCTestCase { let maxTimes = Int.random(in: 1 ... 10) let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, factory: Handler.init) + let result = Lambda.run(configuration: configuration, handlerType: Handler.self) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) - func testVoidAsyncHandlerSuccess() { + func testVoidHandlerSuccess() { let server = MockLambdaServer(behavior: Behavior(result: .success(nil))) XCTAssertNoThrow(try server.start().wait()) defer { XCTAssertNoThrow(try server.stop().wait()) } - struct Handler: AsyncLambdaHandler { + struct Handler: LambdaHandler { typealias In = String typealias Out = Void @@ -123,17 +119,18 @@ class LambdaHandlerTest: XCTestCase { let maxTimes = Int.random(in: 1 ... 10) let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, factory: Handler.init) + + let result = Lambda.run(configuration: configuration, handlerType: Handler.self) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) - func testAsyncHandlerFailure() { + func testHandlerFailure() { let server = MockLambdaServer(behavior: Behavior(result: .failure(TestError("boom")))) XCTAssertNoThrow(try server.start().wait()) defer { XCTAssertNoThrow(try server.stop().wait()) } - struct Handler: AsyncLambdaHandler { + struct Handler: LambdaHandler { typealias In = String typealias Out = String @@ -146,12 +143,12 @@ class LambdaHandlerTest: XCTestCase { let maxTimes = Int.random(in: 1 ... 10) let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, factory: Handler.init) + let result = Lambda.run(configuration: configuration, handlerType: Handler.self) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } #endif - // MARK: - EventLoop + // MARK: - EventLoopLambdaHandler func testEventLoopSuccess() { let server = MockLambdaServer(behavior: Behavior()) @@ -169,7 +166,9 @@ class LambdaHandlerTest: XCTestCase { let maxTimes = Int.random(in: 1 ... 10) let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: Handler()) + let result = Lambda.run(configuration: configuration, factory: { context in + context.eventLoop.makeSucceededFuture(Handler()) + }) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } @@ -189,7 +188,9 @@ class LambdaHandlerTest: XCTestCase { let maxTimes = Int.random(in: 1 ... 10) let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: Handler()) + let result = Lambda.run(configuration: configuration, factory: { context in + context.eventLoop.makeSucceededFuture(Handler()) + }) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } @@ -209,70 +210,20 @@ class LambdaHandlerTest: XCTestCase { let maxTimes = Int.random(in: 1 ... 10) let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: Handler()) + let result = Lambda.run(configuration: configuration, factory: { context in + context.eventLoop.makeSucceededFuture(Handler()) + }) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } - // MARK: - Closure - - func testClosureSuccess() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration) { (_, event: String, callback) in - callback(.success(event)) - } - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testVoidClosureSuccess() { - let server = MockLambdaServer(behavior: Behavior(result: .success(nil))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration) { (_, _: String, callback) in - callback(.success(())) - } - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testClosureFailure() { - let server = MockLambdaServer(behavior: Behavior(result: .failure(TestError("boom")))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result: Result = Lambda.run(configuration: configuration) { (_, _: String, callback: (Result) -> Void) in - callback(.failure(TestError("boom"))) - } - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testBootstrapFailure() { + func testEventLoopBootstrapFailure() { let server = MockLambdaServer(behavior: FailedBootstrapBehavior()) XCTAssertNoThrow(try server.start().wait()) defer { XCTAssertNoThrow(try server.stop().wait()) } - struct Handler: LambdaHandler { - typealias In = String - typealias Out = String - - init(context: Lambda.InitializationContext) throws { - throw TestError("kaboom") - } - - func handle(context: Lambda.Context, event: String, callback: (Result) -> Void) { - callback(.failure(TestError("should not be called"))) - } - } - - let result = Lambda.run(factory: Handler.init) + let result = Lambda.run(configuration: .init(), factory: { context in + context.eventLoop.makeFailedFuture(TestError("kaboom")) + }) assertLambdaLifecycleResult(result, shouldFailWithError: TestError("kaboom")) } } diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlers.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlers.swift new file mode 100644 index 00000000..94202a01 --- /dev/null +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlers.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntimeCore +import NIOCore + +struct EchoHandler: EventLoopLambdaHandler { + typealias In = String + typealias Out = String + + func handle(context: Lambda.Context, event: String) -> EventLoopFuture { + context.eventLoop.makeSucceededFuture(event) + } +} + +struct FailedHandler: EventLoopLambdaHandler { + typealias In = String + typealias Out = Void + + private let reason: String + + init(_ reason: String) { + self.reason = reason + } + + func handle(context: Lambda.Context, event: String) -> EventLoopFuture { + context.eventLoop.makeFailedFuture(TestError(self.reason)) + } +} diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift index b2dd2936..f27600dd 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift @@ -26,7 +26,9 @@ class LambdaTest: XCTestCase { let maxTimes = Int.random(in: 10 ... 20) let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: EchoHandler()) + let result = Lambda.run(configuration: configuration, factory: { + $0.eventLoop.makeSucceededFuture(EchoHandler()) + }) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } @@ -37,34 +39,9 @@ class LambdaTest: XCTestCase { let maxTimes = Int.random(in: 10 ... 20) let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: FailedHandler("boom")) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testBootstrapOnce() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: LambdaHandler { - typealias In = String - typealias Out = String - - var initialized = false - - init(context: Lambda.InitializationContext) { - XCTAssertFalse(self.initialized) - self.initialized = true - } - - func handle(context: Lambda.Context, event: String, callback: (Result) -> Void) { - callback(.success(event)) - } - } - - let maxTimes = Int.random(in: 10 ... 20) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, factory: Handler.init) + let result = Lambda.run(configuration: configuration, factory: { + $0.eventLoop.makeSucceededFuture(FailedHandler("boom")) + }) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } @@ -77,28 +54,6 @@ class LambdaTest: XCTestCase { assertLambdaLifecycleResult(result, shouldFailWithError: TestError("kaboom")) } - func testBootstrapFailure2() { - let server = MockLambdaServer(behavior: FailedBootstrapBehavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: LambdaHandler { - typealias In = String - typealias Out = Void - - init(context: Lambda.InitializationContext) throws { - throw TestError("kaboom") - } - - func handle(context: Lambda.Context, event: String, callback: (Result) -> Void) { - callback(.failure(TestError("should not be called"))) - } - } - - let result = Lambda.run(factory: Handler.init) - assertLambdaLifecycleResult(result, shouldFailWithError: TestError("kaboom")) - } - func testBootstrapFailureAndReportErrorFailure() { struct Behavior: LambdaServerBehavior { func getInvocation() -> GetInvocationResult { @@ -163,7 +118,9 @@ class LambdaTest: XCTestCase { let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: 1), runtimeEngine: .init(requestTimeout: .milliseconds(timeout))) - let result = Lambda.run(configuration: configuration, handler: EchoHandler()) + let result = Lambda.run(configuration: configuration, factory: { + $0.eventLoop.makeSucceededFuture(EchoHandler()) + }) assertLambdaLifecycleResult(result, shouldFailWithError: Lambda.RuntimeError.upstreamError("timeout")) } @@ -173,7 +130,9 @@ class LambdaTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: 1)) - let result = Lambda.run(configuration: configuration, handler: EchoHandler()) + let result = Lambda.run(configuration: configuration, factory: { + $0.eventLoop.makeSucceededFuture(EchoHandler()) + }) assertLambdaLifecycleResult(result, shouldFailWithError: Lambda.RuntimeError.upstreamError("connectionResetByPeer")) } @@ -184,7 +143,9 @@ class LambdaTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: 1)) - let result = Lambda.run(configuration: configuration, handler: EchoHandler()) + let result = Lambda.run(configuration: configuration, factory: { + $0.eventLoop.makeSucceededFuture(EchoHandler()) + }) assertLambdaLifecycleResult(result, shoudHaveRun: 1) } @@ -195,7 +156,9 @@ class LambdaTest: XCTestCase { let maxTimes = 10 let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: EchoHandler()) + let result = Lambda.run(configuration: configuration, factory: { + $0.eventLoop.makeSucceededFuture(EchoHandler()) + }) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } @@ -206,7 +169,9 @@ class LambdaTest: XCTestCase { let maxTimes = 10 let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: EchoHandler()) + let result = Lambda.run(configuration: configuration, factory: { + $0.eventLoop.makeSucceededFuture(EchoHandler()) + }) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } @@ -234,7 +199,9 @@ class LambdaTest: XCTestCase { } } - let result = Lambda.run(handler: EchoHandler()) + let result = Lambda.run(configuration: .init(), factory: { + $0.eventLoop.makeSucceededFuture(EchoHandler()) + }) assertLambdaLifecycleResult(result, shouldFailWithError: Lambda.RuntimeError.badStatusCode(.internalServerError)) } diff --git a/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift b/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift index c0d649be..c66162e6 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift @@ -30,7 +30,7 @@ internal final class MockLambdaServer { private var channel: Channel? private var shutdown = false - public init(behavior: LambdaServerBehavior, host: String = "127.0.0.1", port: Int = 7000, keepAlive: Bool = true) { + init(behavior: LambdaServerBehavior, host: String = "127.0.0.1", port: Int = 7000, keepAlive: Bool = true) { self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) self.behavior = behavior self.host = host @@ -73,8 +73,8 @@ internal final class MockLambdaServer { } internal final class HTTPHandler: ChannelInboundHandler { - public typealias InboundIn = HTTPServerRequestPart - public typealias OutboundOut = HTTPServerResponsePart + typealias InboundIn = HTTPServerRequestPart + typealias OutboundOut = HTTPServerResponsePart private let logger: Logger private let keepAlive: Bool @@ -82,7 +82,7 @@ internal final class HTTPHandler: ChannelInboundHandler { private var pending = CircularBuffer<(head: HTTPRequestHead, body: ByteBuffer?)>() - public init(logger: Logger, keepAlive: Bool, behavior: LambdaServerBehavior) { + init(logger: Logger, keepAlive: Bool, behavior: LambdaServerBehavior) { self.logger = logger self.keepAlive = keepAlive self.behavior = behavior diff --git a/Tests/AWSLambdaRuntimeCoreTests/Utils.swift b/Tests/AWSLambdaRuntimeCoreTests/Utils.swift index 9e080076..b6ebf8da 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/Utils.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/Utils.swift @@ -35,30 +35,6 @@ func runLambda(behavior: LambdaServerBehavior, factory: @escaping Lambda.Handler }.wait() } -struct EchoHandler: LambdaHandler { - typealias In = String - typealias Out = String - - func handle(context: Lambda.Context, event: String, callback: (Result) -> Void) { - callback(.success(event)) - } -} - -struct FailedHandler: LambdaHandler { - typealias In = String - typealias Out = Void - - private let reason: String - - public init(_ reason: String) { - self.reason = reason - } - - func handle(context: Lambda.Context, event: String, callback: (Result) -> Void) { - callback(.failure(TestError(self.reason))) - } -} - func assertLambdaLifecycleResult(_ result: Result, shoudHaveRun: Int = 0, shouldFailWithError: Error? = nil, file: StaticString = #file, line: UInt = #line) { switch result { case .success where shouldFailWithError != nil: diff --git a/Tests/AWSLambdaRuntimeTests/Lambda+CodeableTest.swift b/Tests/AWSLambdaRuntimeTests/Lambda+CodeableTest.swift index 3c228b2c..21f32a59 100644 --- a/Tests/AWSLambdaRuntimeTests/Lambda+CodeableTest.swift +++ b/Tests/AWSLambdaRuntimeTests/Lambda+CodeableTest.swift @@ -29,41 +29,122 @@ class CodableLambdaTest: XCTestCase { } override func tearDown() { - try! self.eventLoopGroup.syncShutdownGracefully() + XCTAssertNoThrow(try self.eventLoopGroup.syncShutdownGracefully()) } - func testCodableVoidClosureWrapper() { + func testCodableVoidEventLoopFutureHandler() { let request = Request(requestId: UUID().uuidString) var inputBuffer: ByteBuffer? var outputBuffer: ByteBuffer? - let closureWrapper = CodableVoidClosureWrapper { (_, _: Request, completion) in - XCTAssertEqual(request, request) - completion(.success(())) + struct Handler: EventLoopLambdaHandler { + typealias In = Request + typealias Out = Void + + let expected: Request + + func handle(context: Lambda.Context, event: Request) -> EventLoopFuture { + XCTAssertEqual(event, self.expected) + return context.eventLoop.makeSucceededVoidFuture() + } } + let handler = Handler(expected: request) + XCTAssertNoThrow(inputBuffer = try JSONEncoder().encode(request, using: self.allocator)) - XCTAssertNoThrow(outputBuffer = try closureWrapper.handle(context: self.newContext(), event: XCTUnwrap(inputBuffer)).wait()) + XCTAssertNoThrow(outputBuffer = try handler.handle(context: self.newContext(), event: XCTUnwrap(inputBuffer)).wait()) XCTAssertNil(outputBuffer) } - func testCodableClosureWrapper() { + func testCodableEventLoopFutureHandler() { let request = Request(requestId: UUID().uuidString) var inputBuffer: ByteBuffer? var outputBuffer: ByteBuffer? var response: Response? - let closureWrapper = CodableClosureWrapper { (_, req: Request, completion: (Result) -> Void) in - XCTAssertEqual(request, req) - completion(.success(Response(requestId: req.requestId))) + struct Handler: EventLoopLambdaHandler { + typealias In = Request + typealias Out = Response + + let expected: Request + + func handle(context: Lambda.Context, event: Request) -> EventLoopFuture { + XCTAssertEqual(event, self.expected) + return context.eventLoop.makeSucceededFuture(Response(requestId: event.requestId)) + } } + let handler = Handler(expected: request) + XCTAssertNoThrow(inputBuffer = try JSONEncoder().encode(request, using: self.allocator)) - XCTAssertNoThrow(outputBuffer = try closureWrapper.handle(context: self.newContext(), event: XCTUnwrap(inputBuffer)).wait()) + XCTAssertNoThrow(outputBuffer = try handler.handle(context: self.newContext(), event: XCTUnwrap(inputBuffer)).wait()) XCTAssertNoThrow(response = try JSONDecoder().decode(Response.self, from: XCTUnwrap(outputBuffer))) XCTAssertEqual(response?.requestId, request.requestId) } + #if swift(>=5.5) + @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) + func testCodableVoidHandler() { + struct Handler: LambdaHandler { + typealias In = Request + typealias Out = Void + + var expected: Request? + + init(context: Lambda.InitializationContext) async throws {} + + func handle(event: Request, context: Lambda.Context) async throws { + XCTAssertEqual(event, self.expected) + } + } + + XCTAsyncTest { + let request = Request(requestId: UUID().uuidString) + var inputBuffer: ByteBuffer? + var outputBuffer: ByteBuffer? + + var handler = try await Handler(context: self.newInitContext()) + handler.expected = request + + XCTAssertNoThrow(inputBuffer = try JSONEncoder().encode(request, using: self.allocator)) + XCTAssertNoThrow(outputBuffer = try handler.handle(context: self.newContext(), event: XCTUnwrap(inputBuffer)).wait()) + XCTAssertNil(outputBuffer) + } + } + + @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) + func testCodableHandler() { + struct Handler: LambdaHandler { + typealias In = Request + typealias Out = Response + + var expected: Request? + + init(context: Lambda.InitializationContext) async throws {} + + func handle(event: Request, context: Lambda.Context) async throws -> Response { + XCTAssertEqual(event, self.expected) + return Response(requestId: event.requestId) + } + } + + XCTAsyncTest { + let request = Request(requestId: UUID().uuidString) + var response: Response? + var inputBuffer: ByteBuffer? + var outputBuffer: ByteBuffer? + + var handler = try await Handler(context: self.newInitContext()) + handler.expected = request + + XCTAssertNoThrow(inputBuffer = try JSONEncoder().encode(request, using: self.allocator)) + XCTAssertNoThrow(outputBuffer = try handler.handle(context: self.newContext(), event: XCTUnwrap(inputBuffer)).wait()) + XCTAssertNoThrow(response = try JSONDecoder().decode(Response.self, from: XCTUnwrap(outputBuffer))) + XCTAssertEqual(response?.requestId, request.requestId) + } + } + #endif + // convencience method func newContext() -> Lambda.Context { Lambda.Context(requestID: UUID().uuidString, @@ -76,6 +157,12 @@ class CodableLambdaTest: XCTestCase { eventLoop: self.eventLoopGroup.next(), allocator: ByteBufferAllocator()) } + + func newInitContext() -> Lambda.InitializationContext { + Lambda.InitializationContext(logger: Logger(label: "test"), + eventLoop: self.eventLoopGroup.next(), + allocator: ByteBufferAllocator()) + } } private struct Request: Codable, Equatable { @@ -91,3 +178,29 @@ private struct Response: Codable, Equatable { self.requestId = requestId } } + +#if swift(>=5.5) +// NOTE: workaround until we have async test support on linux +// https://github.com/apple/swift-corelibs-xctest/pull/326 +extension XCTestCase { + @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) + func XCTAsyncTest( + expectationDescription: String = "Async operation", + timeout: TimeInterval = 3, + file: StaticString = #file, + line: Int = #line, + operation: @escaping () async throws -> Void + ) { + let expectation = self.expectation(description: expectationDescription) + Task { + do { try await operation() } + catch { + XCTFail("Error thrown while executing async function @ \(file):\(line): \(error)") + Thread.callStackSymbols.forEach { print($0) } + } + expectation.fulfill() + } + self.wait(for: [expectation], timeout: timeout) + } +} +#endif diff --git a/Tests/AWSLambdaTestingTests/Tests.swift b/Tests/AWSLambdaTestingTests/Tests.swift index 218547d2..ae0efbcc 100644 --- a/Tests/AWSLambdaTestingTests/Tests.swift +++ b/Tests/AWSLambdaTestingTests/Tests.swift @@ -12,11 +12,13 @@ // //===----------------------------------------------------------------------===// +#if swift(>=5.5) import AWSLambdaRuntime import AWSLambdaTesting import NIOCore import XCTest +@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) class LambdaTestingTests: XCTestCase { func testCodableClosure() { struct Request: Codable { @@ -27,121 +29,81 @@ class LambdaTestingTests: XCTestCase { let message: String } - let myLambda = { (_: Lambda.Context, request: Request, callback: (Result) -> Void) in - callback(.success(Response(message: "echo" + request.name))) + struct MyLambda: LambdaHandler { + typealias In = Request + typealias Out = Response + + init(context: Lambda.InitializationContext) {} + + func handle(event: Request, context: Lambda.Context) async throws -> Response { + Response(message: "echo" + event.name) + } } let request = Request(name: UUID().uuidString) var response: Response? - XCTAssertNoThrow(response = try Lambda.test(myLambda, with: request)) + XCTAssertNoThrow(response = try Lambda.test(MyLambda.self, with: request)) XCTAssertEqual(response?.message, "echo" + request.name) } + // DIRTY HACK: To verify the handler was actually invoked, we change a global variable. + static var VoidLambdaHandlerInvokeCount: Int = 0 func testCodableVoidClosure() { struct Request: Codable { let name: String } - let myLambda = { (_: Lambda.Context, _: Request, callback: (Result) -> Void) in - callback(.success(())) - } - - let request = Request(name: UUID().uuidString) - XCTAssertNoThrow(try Lambda.test(myLambda, with: request)) - } - - func testLambdaHandler() { - struct Request: Codable { - let name: String - } - - struct Response: Codable { - let message: String - } - struct MyLambda: LambdaHandler { typealias In = Request - typealias Out = Response - - func handle(context: Lambda.Context, event: In, callback: @escaping (Result) -> Void) { - XCTAssertFalse(context.eventLoop.inEventLoop) - callback(.success(Response(message: "echo" + event.name))) - } - } - - let request = Request(name: UUID().uuidString) - var response: Response? - XCTAssertNoThrow(response = try Lambda.test(MyLambda(), with: request)) - XCTAssertEqual(response?.message, "echo" + request.name) - } + typealias Out = Void - func testEventLoopLambdaHandler() { - struct MyLambda: EventLoopLambdaHandler { - typealias In = String - typealias Out = String + init(context: Lambda.InitializationContext) {} - func handle(context: Lambda.Context, event: String) -> EventLoopFuture { - XCTAssertTrue(context.eventLoop.inEventLoop) - return context.eventLoop.makeSucceededFuture("echo" + event) + func handle(event: Request, context: Lambda.Context) async throws { + LambdaTestingTests.VoidLambdaHandlerInvokeCount += 1 } } - let input = UUID().uuidString - var result: String? - XCTAssertNoThrow(result = try Lambda.test(MyLambda(), with: input)) - XCTAssertEqual(result, "echo" + input) + Self.VoidLambdaHandlerInvokeCount = 0 + let request = Request(name: UUID().uuidString) + XCTAssertNoThrow(try Lambda.test(MyLambda.self, with: request)) + XCTAssertEqual(Self.VoidLambdaHandlerInvokeCount, 1) } - func testFailure() { + func testInvocationFailure() { struct MyError: Error {} struct MyLambda: LambdaHandler { typealias In = String typealias Out = Void - func handle(context: Lambda.Context, event: In, callback: @escaping (Result) -> Void) { - callback(.failure(MyError())) + init(context: Lambda.InitializationContext) {} + + func handle(event: String, context: Lambda.Context) async throws { + throw MyError() } } - XCTAssertThrowsError(try Lambda.test(MyLambda(), with: UUID().uuidString)) { error in + XCTAssertThrowsError(try Lambda.test(MyLambda.self, with: UUID().uuidString)) { error in XCTAssert(error is MyError) } } func testAsyncLongRunning() { - var executed: Bool = false - let myLambda = { (_: Lambda.Context, _: String, callback: @escaping (Result) -> Void) in - DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.5) { - executed = true - callback(.success(())) - } - } - - XCTAssertNoThrow(try Lambda.test(myLambda, with: UUID().uuidString)) - XCTAssertTrue(executed) - } - - func testConfigValues() { - let timeout: TimeInterval = 4 - let config = Lambda.TestConfig( - requestID: UUID().uuidString, - traceID: UUID().uuidString, - invokedFunctionARN: "arn:\(UUID().uuidString)", - timeout: .seconds(4) - ) - - let myLambda = { (ctx: Lambda.Context, _: String, callback: @escaping (Result) -> Void) in - XCTAssertEqual(ctx.requestID, config.requestID) - XCTAssertEqual(ctx.traceID, config.traceID) - XCTAssertEqual(ctx.invokedFunctionARN, config.invokedFunctionARN) + struct MyLambda: LambdaHandler { + typealias In = String + typealias Out = String - let secondsSinceEpoch = Double(Int64(bitPattern: ctx.deadline.rawValue)) / -1_000_000_000 - XCTAssertEqual(Date(timeIntervalSince1970: secondsSinceEpoch).timeIntervalSinceNow, timeout, accuracy: 0.1) + init(context: Lambda.InitializationContext) {} - callback(.success(())) + func handle(event: String, context: Lambda.Context) async throws -> String { + try await Task.sleep(nanoseconds: 500 * 1000 * 1000) + return event + } } - XCTAssertNoThrow(try Lambda.test(myLambda, with: UUID().uuidString, using: config)) + let uuid = UUID().uuidString + XCTAssertEqual(try Lambda.test(MyLambda.self, with: uuid), uuid) } } +#endif