Skip to content

Commit b83d90d

Browse files
committed
Use NIOThreadPoolTaskExecutor for NIOAsyncWriter test that hangs on Android emulator
The testSuspendingBufferedYield_whenWriterFinished test requires at least two threads in the concurrency thread pool because it blocks one task, which waits for another task to set a condition. In environments where the global concurrency thread pool doesn’t have at least two threads available, the test will fail, as observed on the Android emulator running with a single virtual core (see discussion in #3044). Using a custom task executor guarantees that at least two threads are available for the test, regardless of the width of the global concurrency thread pool.
1 parent 04e2e44 commit b83d90d

File tree

2 files changed

+44
-36
lines changed

2 files changed

+44
-36
lines changed

Diff for: Package.swift

+1
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ let package = Package(
441441
"NIOCore",
442442
"NIOEmbedded",
443443
"NIOFoundationCompat",
444+
"NIOTestUtils",
444445
swiftAtomics,
445446
],
446447
swiftSettings: strictConcurrencySettings

Diff for: Tests/NIOCoreTests/AsyncSequences/NIOAsyncWriterTests.swift

+43-36
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import DequeModule
1616
import NIOConcurrencyHelpers
17+
import NIOTestUtils
1718
import XCTest
1819

1920
@testable import NIOCore
@@ -607,50 +608,56 @@ final class NIOAsyncWriterTests: XCTestCase {
607608
}
608609

609610
func testSuspendingBufferedYield_whenWriterFinished() async throws {
610-
self.sink.setWritability(to: false)
611-
612-
let bothSuspended = expectation(description: "suspended on both yields")
613-
let suspendedAgain = ConditionLock(value: false)
614-
self.delegate.didSuspendHandler = {
615-
if self.delegate.didSuspendCallCount == 2 {
616-
bothSuspended.fulfill()
617-
} else if self.delegate.didSuspendCallCount > 2 {
618-
suspendedAgain.lock()
619-
suspendedAgain.unlock(withValue: true)
620-
}
621-
}
611+
#if compiler(>=6)
612+
try await withNIOThreadPoolTaskExecutor(numberOfThreads: 2) { taskExecutor in
613+
try await withThrowingTaskGroup(of: Void.self) { group in
614+
self.sink.setWritability(to: false)
615+
616+
let bothSuspended = expectation(description: "suspended on both yields")
617+
let suspendedAgain = ConditionLock(value: false)
618+
self.delegate.didSuspendHandler = {
619+
if self.delegate.didSuspendCallCount == 2 {
620+
bothSuspended.fulfill()
621+
} else if self.delegate.didSuspendCallCount > 2 {
622+
suspendedAgain.lock()
623+
suspendedAgain.unlock(withValue: true)
624+
}
625+
}
622626

623-
self.delegate.didYieldHandler = { _ in
624-
if self.delegate.didYieldCallCount == 1 {
625-
// Delay this yield until the other yield is suspended again.
626-
if suspendedAgain.lock(whenValue: true, timeoutSeconds: 5) {
627-
suspendedAgain.unlock()
628-
} else {
629-
XCTFail("Timeout while waiting for other yield to suspend again.")
627+
self.delegate.didYieldHandler = { _ in
628+
if self.delegate.didYieldCallCount == 1 {
629+
// Delay this yield until the other yield is suspended again.
630+
if suspendedAgain.lock(whenValue: true, timeoutSeconds: 5) {
631+
suspendedAgain.unlock()
632+
} else {
633+
XCTFail("Timeout while waiting for other yield to suspend again.")
634+
}
635+
}
630636
}
631-
}
632-
}
633637

634-
let task1 = Task { [writer] in
635-
try await writer!.yield("message1")
636-
}
637-
let task2 = Task { [writer] in
638-
try await writer!.yield("message2")
639-
}
638+
group.addTask(executorPreference: taskExecutor) { [writer] in
639+
try await writer!.yield("message1")
640+
}
641+
group.addTask(executorPreference: taskExecutor) { [writer] in
642+
try await writer!.yield("message2")
643+
}
640644

641-
await fulfillment(of: [bothSuspended], timeout: 5)
642-
self.writer.finish()
645+
await fulfillment(of: [bothSuspended], timeout: 5)
646+
self.writer.finish()
643647

644-
self.assert(suspendCallCount: 2, yieldCallCount: 0, terminateCallCount: 0)
648+
self.assert(suspendCallCount: 2, yieldCallCount: 0, terminateCallCount: 0)
645649

646-
// We have to become writable again to unbuffer the yields
647-
// The first call to didYield will pause, so that the other yield will be suspended again.
648-
self.sink.setWritability(to: true)
650+
// We have to become writable again to unbuffer the yields
651+
// The first call to didYield will pause, so that the other yield will be suspended again.
652+
self.sink.setWritability(to: true)
649653

650-
await XCTAssertNoThrow(try await task1.value)
651-
await XCTAssertNoThrow(try await task2.value)
654+
await XCTAssertNoThrow(try await group.next())
655+
await XCTAssertNoThrow(try await group.next())
652656

653-
self.assert(suspendCallCount: 3, yieldCallCount: 2, terminateCallCount: 1)
657+
self.assert(suspendCallCount: 3, yieldCallCount: 2, terminateCallCount: 1)
658+
}
659+
}
660+
#endif // compiler(>=6)
654661
}
655662

656663
func testWriterFinish_whenFinished() {

0 commit comments

Comments
 (0)