Skip to content

Commit 0397ea8

Browse files
authored
Fix sendability issues in tests (#841)
Motivation: The tests shouldn't be making sendability violations. Modifications: Fix the warnings Result: - No warnings - Strict concurrency is adopted!
1 parent 6b5f8c9 commit 0397ea8

18 files changed

+383
-190
lines changed

Package.swift

+28-8
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,36 @@
1515

1616
import PackageDescription
1717

18+
let strictConcurrencyDevelopment = false
19+
20+
let strictConcurrencySettings: [SwiftSetting] = {
21+
var initialSettings: [SwiftSetting] = []
22+
initialSettings.append(contentsOf: [
23+
.enableUpcomingFeature("StrictConcurrency"),
24+
.enableUpcomingFeature("InferSendableFromCaptures"),
25+
])
26+
27+
if strictConcurrencyDevelopment {
28+
// -warnings-as-errors here is a workaround so that IDE-based development can
29+
// get tripped up on -require-explicit-sendable.
30+
initialSettings.append(.unsafeFlags(["-Xfrontend", "-require-explicit-sendable", "-warnings-as-errors"]))
31+
}
32+
33+
return initialSettings
34+
}()
35+
1836
let package = Package(
1937
name: "async-http-client",
2038
products: [
2139
.library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"])
2240
],
2341
dependencies: [
24-
.package(url: "https://github.com/apple/swift-nio.git", from: "2.78.0"),
25-
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.27.1"),
26-
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.19.0"),
27-
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.13.0"),
28-
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.19.0"),
29-
.package(url: "https://github.com/apple/swift-log.git", from: "1.4.4"),
42+
.package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"),
43+
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.30.0"),
44+
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.36.0"),
45+
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.26.0"),
46+
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.24.0"),
47+
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.0"),
3048
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"),
3149
.package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"),
3250
],
@@ -55,7 +73,8 @@ let package = Package(
5573
.product(name: "Logging", package: "swift-log"),
5674
.product(name: "Atomics", package: "swift-atomics"),
5775
.product(name: "Algorithms", package: "swift-algorithms"),
58-
]
76+
],
77+
swiftSettings: strictConcurrencySettings
5978
),
6079
.testTarget(
6180
name: "AsyncHTTPClientTests",
@@ -79,7 +98,8 @@ let package = Package(
7998
.copy("Resources/self_signed_key.pem"),
8099
.copy("Resources/example.com.cert.pem"),
81100
.copy("Resources/example.com.private-key.pem"),
82-
]
101+
],
102+
swiftSettings: strictConcurrencySettings
83103
),
84104
]
85105
)

Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift

+5-3
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,8 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
526526
}
527527

528528
func testConnectTimeout() {
529+
let serverGroup = self.serverGroup!
530+
let clientGroup = self.clientGroup!
529531
XCTAsyncTest(timeout: 60) {
530532
#if os(Linux)
531533
// 198.51.100.254 is reserved for documentation only and therefore should not accept any TCP connection
@@ -542,7 +544,7 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
542544
XCTAssertNoThrow(try group.syncShutdownGracefully())
543545
}
544546

545-
let serverChannel = try await ServerBootstrap(group: self.serverGroup)
547+
let serverChannel = try await ServerBootstrap(group: serverGroup)
546548
.serverChannelOption(ChannelOptions.backlog, value: 1)
547549
.serverChannelOption(ChannelOptions.autoRead, value: false)
548550
.bind(host: "127.0.0.1", port: 0)
@@ -551,7 +553,7 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
551553
XCTAssertNoThrow(try serverChannel.close().wait())
552554
}
553555
let port = serverChannel.localAddress!.port!
554-
let firstClientChannel = try await ClientBootstrap(group: self.serverGroup)
556+
let firstClientChannel = try await ClientBootstrap(group: serverGroup)
555557
.connect(host: "127.0.0.1", port: port)
556558
.get()
557559
defer {
@@ -561,7 +563,7 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
561563
#endif
562564

563565
let httpClient = HTTPClient(
564-
eventLoopGroupProvider: .shared(self.clientGroup),
566+
eventLoopGroupProvider: .shared(clientGroup),
565567
configuration: .init(timeout: .init(connect: .milliseconds(100), read: .milliseconds(150)))
566568
)
567569

Tests/AsyncHTTPClientTests/AsyncTestHelpers.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import NIOCore
1717

1818
/// ``AsyncSequenceWriter`` is `Sendable` because its state is protected by a Lock
1919
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
20-
final class AsyncSequenceWriter<Element>: AsyncSequence, @unchecked Sendable {
20+
final class AsyncSequenceWriter<Element: Sendable>: AsyncSequence, @unchecked Sendable {
2121
typealias AsyncIterator = Iterator
2222

2323
struct Iterator: AsyncIteratorProtocol {

Tests/AsyncHTTPClientTests/HTTP1ClientChannelHandlerTests.swift

+40-30
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import Logging
16+
import NIOConcurrencyHelpers
1617
import NIOCore
1718
import NIOEmbedded
1819
import NIOHTTP1
@@ -833,10 +834,11 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
833834
)
834835
try channel.connect(to: .init(ipAddress: "127.0.0.1", port: 80)).wait()
835836

836-
let request = MockHTTPExecutableRequest()
837837
// non empty body is important to trigger this bug as we otherwise finish the request in a single flush
838-
request.requestFramingMetadata.body = .fixedSize(1)
839-
request.raiseErrorIfUnimplementedMethodIsCalled = false
838+
let request = MockHTTPExecutableRequest(
839+
framingMetadata: RequestFramingMetadata(connectionClose: false, body: .fixedSize(1)),
840+
raiseErrorIfUnimplementedMethodIsCalled: false
841+
)
840842
channel.writeAndFlush(request, promise: nil)
841843
XCTAssertEqual(request.events.map(\.kind), [.willExecuteRequest, .requestHeadSent])
842844
}
@@ -897,34 +899,43 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
897899
}
898900
}
899901

900-
class TestBackpressureWriter {
902+
final class TestBackpressureWriter: Sendable {
901903
let eventLoop: EventLoop
902904

903905
let parts: Int
904906

905907
var finishFuture: EventLoopFuture<Void> { self.finishPromise.futureResult }
906908
private let finishPromise: EventLoopPromise<Void>
907-
private(set) var written: Int = 0
908909

909-
private var channelIsWritable: Bool = false
910+
private struct State {
911+
var written = 0
912+
var channelIsWritable = false
913+
}
914+
915+
var written: Int {
916+
self.state.value.written
917+
}
918+
919+
private let state: NIOLoopBoundBox<State>
910920

911921
init(eventLoop: EventLoop, parts: Int) {
912922
self.eventLoop = eventLoop
913923
self.parts = parts
914-
924+
self.state = .makeBoxSendingValue(State(), eventLoop: eventLoop)
915925
self.finishPromise = eventLoop.makePromise(of: Void.self)
916926
}
917927

918928
func start(writer: HTTPClient.Body.StreamWriter, expectedErrors: [HTTPClientError] = []) -> EventLoopFuture<Void> {
929+
@Sendable
919930
func recursive() {
920931
XCTAssert(self.eventLoop.inEventLoop)
921-
XCTAssert(self.channelIsWritable)
922-
if self.written == self.parts {
932+
XCTAssert(self.state.value.channelIsWritable)
933+
if self.state.value.written == self.parts {
923934
self.finishPromise.succeed(())
924935
} else {
925936
self.eventLoop.execute {
926937
let future = writer.write(.byteBuffer(.init(bytes: [0, 1])))
927-
self.written += 1
938+
self.state.value.written += 1
928939
future.whenComplete { result in
929940
switch result {
930941
case .success:
@@ -951,36 +962,35 @@ class TestBackpressureWriter {
951962
}
952963

953964
func writabilityChanged(_ newValue: Bool) {
954-
self.channelIsWritable = newValue
965+
self.state.value.channelIsWritable = newValue
955966
}
956967
}
957968

958-
class ResponseBackpressureDelegate: HTTPClientResponseDelegate {
969+
final class ResponseBackpressureDelegate: HTTPClientResponseDelegate {
959970
typealias Response = Void
960971

961-
enum State {
972+
enum State: Sendable {
962973
case consuming(EventLoopPromise<Void>)
963974
case waitingForRemote(CircularBuffer<EventLoopPromise<ByteBuffer?>>)
964975
case buffering((ByteBuffer?, EventLoopPromise<Void>)?)
965976
case done
966977
}
967978

968979
let eventLoop: EventLoop
969-
private var state: State = .buffering(nil)
980+
private let state: NIOLoopBoundBox<State>
970981

971982
init(eventLoop: EventLoop) {
972983
self.eventLoop = eventLoop
973-
974-
self.state = .consuming(self.eventLoop.makePromise(of: Void.self))
984+
self.state = .makeBoxSendingValue(.consuming(eventLoop.makePromise(of: Void.self)), eventLoop: eventLoop)
975985
}
976986

977987
func next() -> EventLoopFuture<ByteBuffer?> {
978-
switch self.state {
988+
switch self.state.value {
979989
case .consuming(let backpressurePromise):
980990
var promiseBuffer = CircularBuffer<EventLoopPromise<ByteBuffer?>>()
981991
let newPromise = self.eventLoop.makePromise(of: ByteBuffer?.self)
982992
promiseBuffer.append(newPromise)
983-
self.state = .waitingForRemote(promiseBuffer)
993+
self.state.value = .waitingForRemote(promiseBuffer)
984994
backpressurePromise.succeed(())
985995
return newPromise.futureResult
986996

@@ -991,18 +1001,18 @@ class ResponseBackpressureDelegate: HTTPClientResponseDelegate {
9911001
)
9921002
let promise = self.eventLoop.makePromise(of: ByteBuffer?.self)
9931003
promiseBuffer.append(promise)
994-
self.state = .waitingForRemote(promiseBuffer)
1004+
self.state.value = .waitingForRemote(promiseBuffer)
9951005
return promise.futureResult
9961006

9971007
case .buffering(.none):
9981008
var promiseBuffer = CircularBuffer<EventLoopPromise<ByteBuffer?>>()
9991009
let promise = self.eventLoop.makePromise(of: ByteBuffer?.self)
10001010
promiseBuffer.append(promise)
1001-
self.state = .waitingForRemote(promiseBuffer)
1011+
self.state.value = .waitingForRemote(promiseBuffer)
10021012
return promise.futureResult
10031013

10041014
case .buffering(.some((let buffer, let promise))):
1005-
self.state = .buffering(nil)
1015+
self.state.value = .buffering(nil)
10061016
promise.succeed(())
10071017
return self.eventLoop.makeSucceededFuture(buffer)
10081018

@@ -1012,7 +1022,7 @@ class ResponseBackpressureDelegate: HTTPClientResponseDelegate {
10121022
}
10131023

10141024
func didReceiveHead(task: HTTPClient.Task<Void>, _ head: HTTPResponseHead) -> EventLoopFuture<Void> {
1015-
switch self.state {
1025+
switch self.state.value {
10161026
case .consuming(let backpressurePromise):
10171027
return backpressurePromise.futureResult
10181028

@@ -1025,7 +1035,7 @@ class ResponseBackpressureDelegate: HTTPClientResponseDelegate {
10251035
}
10261036

10271037
func didReceiveBodyPart(task: HTTPClient.Task<Void>, _ buffer: ByteBuffer) -> EventLoopFuture<Void> {
1028-
switch self.state {
1038+
switch self.state.value {
10291039
case .waitingForRemote(var promiseBuffer):
10301040
assert(
10311041
!promiseBuffer.isEmpty,
@@ -1034,18 +1044,18 @@ class ResponseBackpressureDelegate: HTTPClientResponseDelegate {
10341044
let promise = promiseBuffer.removeFirst()
10351045
if promiseBuffer.isEmpty {
10361046
let newBackpressurePromise = self.eventLoop.makePromise(of: Void.self)
1037-
self.state = .consuming(newBackpressurePromise)
1047+
self.state.value = .consuming(newBackpressurePromise)
10381048
promise.succeed(buffer)
10391049
return newBackpressurePromise.futureResult
10401050
} else {
1041-
self.state = .waitingForRemote(promiseBuffer)
1051+
self.state.value = .waitingForRemote(promiseBuffer)
10421052
promise.succeed(buffer)
10431053
return self.eventLoop.makeSucceededVoidFuture()
10441054
}
10451055

10461056
case .buffering(.none):
10471057
let promise = self.eventLoop.makePromise(of: Void.self)
1048-
self.state = .buffering((buffer, promise))
1058+
self.state.value = .buffering((buffer, promise))
10491059
return promise.futureResult
10501060

10511061
case .buffering(.some):
@@ -1059,15 +1069,15 @@ class ResponseBackpressureDelegate: HTTPClientResponseDelegate {
10591069
}
10601070

10611071
func didFinishRequest(task: HTTPClient.Task<Void>) throws {
1062-
switch self.state {
1072+
switch self.state.value {
10631073
case .waitingForRemote(let promiseBuffer):
10641074
for promise in promiseBuffer {
10651075
promise.succeed(.none)
10661076
}
1067-
self.state = .done
1077+
self.state.value = .done
10681078

10691079
case .buffering(.none):
1070-
self.state = .done
1080+
self.state.value = .done
10711081

10721082
case .done, .consuming:
10731083
preconditionFailure("Invalid state: \(self.state)")
@@ -1093,7 +1103,7 @@ class ReadEventHitHandler: ChannelOutboundHandler {
10931103
}
10941104
}
10951105

1096-
final class FailEndHandler: ChannelOutboundHandler {
1106+
final class FailEndHandler: ChannelOutboundHandler, Sendable {
10971107
typealias OutboundIn = HTTPClientRequestPart
10981108
typealias OutboundOut = HTTPClientRequestPart
10991109

Tests/AsyncHTTPClientTests/HTTP2ClientRequestHandlerTests.swift

+4-3
Original file line numberDiff line numberDiff line change
@@ -568,10 +568,11 @@ class HTTP2ClientRequestHandlerTests: XCTestCase {
568568
)
569569
try channel.connect(to: .init(ipAddress: "127.0.0.1", port: 80)).wait()
570570

571-
let request = MockHTTPExecutableRequest()
572571
// non empty body is important to trigger this bug as we otherwise finish the request in a single flush
573-
request.requestFramingMetadata.body = .fixedSize(1)
574-
request.raiseErrorIfUnimplementedMethodIsCalled = false
572+
let request = MockHTTPExecutableRequest(
573+
framingMetadata: RequestFramingMetadata(connectionClose: false, body: .fixedSize(1)),
574+
raiseErrorIfUnimplementedMethodIsCalled: false
575+
)
575576
channel.writeAndFlush(request, promise: nil)
576577
XCTAssertEqual(request.events.map(\.kind), [.willExecuteRequest, .requestHeadSent])
577578
}

Tests/AsyncHTTPClientTests/HTTP2ClientTests.swift

+12-8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import AsyncHTTPClient // NOT @testable - tests that really need @testable go into HTTP2ClientInternalTests.swift
1616
import Logging
17+
import NIOConcurrencyHelpers
1718
import NIOCore
1819
import NIOFoundationCompat
1920
import NIOHTTP1
@@ -283,15 +284,16 @@ class HTTP2ClientTests: XCTestCase {
283284
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "https://localhost:\(bin.port)"))
284285
guard let request = maybeRequest else { return }
285286

286-
var task: HTTPClient.Task<Void>!
287+
let taskBox = NIOLockedValueBox<HTTPClient.Task<Void>?>(nil)
287288
let delegate = HeadReceivedCallback { _ in
288289
// request is definitely running because we just received a head from the server
289-
task.cancel()
290+
taskBox.withLockedValue { $0 }!.cancel()
290291
}
291-
task = client.execute(
292+
let task = client.execute(
292293
request: request,
293294
delegate: delegate
294295
)
296+
taskBox.withLockedValue { $0 = task }
295297

296298
XCTAssertThrowsError(try task.futureResult.timeout(after: .seconds(2)).wait()) {
297299
XCTAssertEqualTypeAndValue($0, HTTPClientError.cancelled)
@@ -360,18 +362,20 @@ class HTTP2ClientTests: XCTestCase {
360362
guard let request = maybeRequest else { return }
361363

362364
let tasks = (0..<100).map { _ -> HTTPClient.Task<TestHTTPDelegate.Response> in
363-
var task: HTTPClient.Task<Void>!
365+
let taskBox = NIOLockedValueBox<HTTPClient.Task<Void>?>(nil)
366+
364367
let delegate = HeadReceivedCallback { _ in
365368
// request is definitely running because we just received a head from the server
366369
cancelPool.next().execute {
367370
// canceling from a different thread
368-
task.cancel()
371+
taskBox.withLockedValue { $0 }!.cancel()
369372
}
370373
}
371-
task = client.execute(
374+
let task = client.execute(
372375
request: request,
373376
delegate: delegate
374377
)
378+
taskBox.withLockedValue { $0 = task }
375379
return task
376380
}
377381

@@ -547,8 +551,8 @@ class HTTP2ClientTests: XCTestCase {
547551

548552
private final class HeadReceivedCallback: HTTPClientResponseDelegate {
549553
typealias Response = Void
550-
private let didReceiveHeadCallback: (HTTPResponseHead) -> Void
551-
init(didReceiveHead: @escaping (HTTPResponseHead) -> Void) {
554+
private let didReceiveHeadCallback: @Sendable (HTTPResponseHead) -> Void
555+
init(didReceiveHead: @escaping @Sendable (HTTPResponseHead) -> Void) {
552556
self.didReceiveHeadCallback = didReceiveHead
553557
}
554558

0 commit comments

Comments
 (0)