From eb4e376c315c254fe974d8337e2a9cbbb1cedbad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= <sebastien.stormacq@gmail.com> Date: Fri, 7 Mar 2025 19:42:18 +0100 Subject: [PATCH 1/3] add a unit test for the LambdaHTTPServer Pool --- Package@swift-6.0.swift | 7 +- .../AWSLambdaRuntime/Lambda+LocalServer.swift | 4 +- Tests/AWSLambdaRuntimeTests/PoolTests.swift | 135 ++++++++++++++++++ 3 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 Tests/AWSLambdaRuntimeTests/PoolTests.swift diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index fb4e82d0..4ee6690c 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -56,7 +56,12 @@ let package = Package( .byName(name: "AWSLambdaRuntime"), .product(name: "NIOTestUtils", package: "swift-nio"), .product(name: "NIOFoundationCompat", package: "swift-nio"), - ] + ], + swiftSettings: [ + .define("FoundationJSONSupport"), + .define("ServiceLifecycleSupport"), + .define("LocalServerSupport"), + ] ), // for perf testing .executableTarget( diff --git a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift index 4d85f7b2..c340efd5 100644 --- a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift @@ -75,7 +75,7 @@ extension Lambda { /// 1. POST /invoke - the client posts the event to the lambda function /// /// This server passes the data received from /invoke POST request to the lambda function (GET /next) and then forwards the response back to the client. -private struct LambdaHTTPServer { +internal struct LambdaHTTPServer { private let invocationEndpoint: String private let invocationPool = Pool<LocalServerInvocation>() @@ -425,7 +425,7 @@ private struct LambdaHTTPServer { /// A shared data structure to store the current invocation or response requests and the continuation objects. /// This data structure is shared between instances of the HTTPHandler /// (one instance to serve requests from the Lambda function and one instance to serve requests from the client invoking the lambda function). - private final class Pool<T>: AsyncSequence, AsyncIteratorProtocol, Sendable where T: Sendable { + internal final class Pool<T>: AsyncSequence, AsyncIteratorProtocol, Sendable where T: Sendable { typealias Element = T enum State: ~Copyable { diff --git a/Tests/AWSLambdaRuntimeTests/PoolTests.swift b/Tests/AWSLambdaRuntimeTests/PoolTests.swift new file mode 100644 index 00000000..e720e0e2 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/PoolTests.swift @@ -0,0 +1,135 @@ +import Testing +@testable import AWSLambdaRuntime + +struct PoolTests { + + @Test + func testBasicPushAndIteration() async throws { + let pool = LambdaHTTPServer.Pool<String>() + + // Push values + await pool.push("first") + await pool.push("second") + + // Iterate and verify order + var values = [String]() + for try await value in pool { + values.append(value) + if values.count == 2 { break } + } + + #expect(values == ["first", "second"]) + } + + @Test + func testCancellation() async throws { + let pool = LambdaHTTPServer.Pool<String>() + + // Create a task that will be cancelled + let task = Task { + for try await _ in pool { + Issue.record("Should not receive any values after cancellation") + } + } + + // Cancel the task immediately + task.cancel() + + // This should complete without receiving any values + try await task.value + } + + @Test + func testConcurrentPushAndIteration() async throws { + let pool = LambdaHTTPServer.Pool<Int>() + let iterations = 1000 + var receivedValues = Set<Int>() + + // Start consumer task first + let consumer = Task { + var count = 0 + for try await value in pool { + receivedValues.insert(value) + count += 1 + if count >= iterations { break } + } + } + + // Create multiple producer tasks + try await withThrowingTaskGroup(of: Void.self) { group in + for i in 0..<iterations { + group.addTask { + await pool.push(i) + } + } + try await group.waitForAll() + } + + // Wait for consumer to complete + try await consumer.value + + // Verify all values were received exactly once + #expect(receivedValues.count == iterations) + #expect(Set(0..<iterations) == receivedValues) + } + + @Test + func testPushToWaitingConsumer() async throws { + let pool = LambdaHTTPServer.Pool<String>() + let expectedValue = "test value" + + // Start a consumer that will wait for a value + let consumer = Task { + for try await value in pool { + #expect(value == expectedValue) + break + } + } + + // Give consumer time to start waiting + try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds + + // Push a value + await pool.push(expectedValue) + + // Wait for consumer to complete + try await consumer.value + } + + @Test + func testStressTest() async throws { + let pool = LambdaHTTPServer.Pool<Int>() + let producerCount = 10 + let messagesPerProducer = 1000 + var receivedValues = [Int]() + + // Start consumer + let consumer = Task { + var count = 0 + for try await value in pool { + receivedValues.append(value) + count += 1 + if count >= producerCount * messagesPerProducer { break } + } + } + + // Create multiple producers + try await withThrowingTaskGroup(of: Void.self) { group in + for p in 0..<producerCount { + group.addTask { + for i in 0..<messagesPerProducer { + await pool.push(p * messagesPerProducer + i) + } + } + } + try await group.waitForAll() + } + + // Wait for consumer to complete + try await consumer.value + + // Verify we received all values + #expect(receivedValues.count == producerCount * messagesPerProducer) + #expect(Set(receivedValues).count == producerCount * messagesPerProducer) + } +} \ No newline at end of file From 593adcc5bc6f8945fb555a6528eddc309f475bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= <sebastien.stormacq@gmail.com> Date: Fri, 7 Mar 2025 19:44:16 +0100 Subject: [PATCH 2/3] swift format --- Package@swift-6.0.swift | 2 +- Tests/AWSLambdaRuntimeTests/PoolTests.swift | 51 +++++++++++---------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 4ee6690c..a10bb0b8 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -61,7 +61,7 @@ let package = Package( .define("FoundationJSONSupport"), .define("ServiceLifecycleSupport"), .define("LocalServerSupport"), - ] + ] ), // for perf testing .executableTarget( diff --git a/Tests/AWSLambdaRuntimeTests/PoolTests.swift b/Tests/AWSLambdaRuntimeTests/PoolTests.swift index e720e0e2..a9309a21 100644 --- a/Tests/AWSLambdaRuntimeTests/PoolTests.swift +++ b/Tests/AWSLambdaRuntimeTests/PoolTests.swift @@ -1,50 +1,51 @@ import Testing + @testable import AWSLambdaRuntime struct PoolTests { - + @Test func testBasicPushAndIteration() async throws { let pool = LambdaHTTPServer.Pool<String>() - + // Push values await pool.push("first") await pool.push("second") - + // Iterate and verify order var values = [String]() for try await value in pool { values.append(value) if values.count == 2 { break } } - + #expect(values == ["first", "second"]) } - + @Test func testCancellation() async throws { let pool = LambdaHTTPServer.Pool<String>() - + // Create a task that will be cancelled let task = Task { for try await _ in pool { Issue.record("Should not receive any values after cancellation") } } - + // Cancel the task immediately task.cancel() - + // This should complete without receiving any values try await task.value } - + @Test func testConcurrentPushAndIteration() async throws { let pool = LambdaHTTPServer.Pool<Int>() let iterations = 1000 var receivedValues = Set<Int>() - + // Start consumer task first let consumer = Task { var count = 0 @@ -54,7 +55,7 @@ struct PoolTests { if count >= iterations { break } } } - + // Create multiple producer tasks try await withThrowingTaskGroup(of: Void.self) { group in for i in 0..<iterations { @@ -64,20 +65,20 @@ struct PoolTests { } try await group.waitForAll() } - + // Wait for consumer to complete try await consumer.value - + // Verify all values were received exactly once #expect(receivedValues.count == iterations) #expect(Set(0..<iterations) == receivedValues) } - + @Test func testPushToWaitingConsumer() async throws { let pool = LambdaHTTPServer.Pool<String>() let expectedValue = "test value" - + // Start a consumer that will wait for a value let consumer = Task { for try await value in pool { @@ -85,24 +86,24 @@ struct PoolTests { break } } - + // Give consumer time to start waiting - try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds - + try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds + // Push a value await pool.push(expectedValue) - + // Wait for consumer to complete try await consumer.value } - + @Test func testStressTest() async throws { let pool = LambdaHTTPServer.Pool<Int>() let producerCount = 10 let messagesPerProducer = 1000 var receivedValues = [Int]() - + // Start consumer let consumer = Task { var count = 0 @@ -112,7 +113,7 @@ struct PoolTests { if count >= producerCount * messagesPerProducer { break } } } - + // Create multiple producers try await withThrowingTaskGroup(of: Void.self) { group in for p in 0..<producerCount { @@ -124,12 +125,12 @@ struct PoolTests { } try await group.waitForAll() } - + // Wait for consumer to complete try await consumer.value - + // Verify we received all values #expect(receivedValues.count == producerCount * messagesPerProducer) #expect(Set(receivedValues).count == producerCount * messagesPerProducer) } -} \ No newline at end of file +} From ce1f959290be68f845c20a6352708ab2a2505b4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= <sebastien.stormacq@gmail.com> Date: Fri, 7 Mar 2025 19:46:08 +0100 Subject: [PATCH 3/3] license header --- Sources/AWSLambdaRuntime/Lambda+LocalServer.swift | 2 +- Tests/AWSLambdaRuntimeTests/PoolTests.swift | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift index c340efd5..d959a776 100644 --- a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Tests/AWSLambdaRuntimeTests/PoolTests.swift b/Tests/AWSLambdaRuntimeTests/PoolTests.swift index a9309a21..a5def86d 100644 --- a/Tests/AWSLambdaRuntimeTests/PoolTests.swift +++ b/Tests/AWSLambdaRuntimeTests/PoolTests.swift @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2025 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 Testing @testable import AWSLambdaRuntime