Skip to content

Commit ac922eb

Browse files
committed
Add tests for URLSession._MultiHandle Windows issue
This adds test cases for swiftlang#4791. Test HTTPServer needs to be reconfigured for one of the test scenarios, so there are options now.
1 parent 1b514e4 commit ac922eb

File tree

2 files changed

+142
-16
lines changed

2 files changed

+142
-16
lines changed

Tests/Foundation/HTTPServer.swift

+43-11
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class _TCPSocket: CustomStringConvertible {
9999
listening = false
100100
}
101101

102-
init(port: UInt16?) throws {
102+
init(port: UInt16?, backlog: Int32) throws {
103103
listening = true
104104
self.port = 0
105105

@@ -124,7 +124,7 @@ class _TCPSocket: CustomStringConvertible {
124124
try socketAddress.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size, {
125125
let addr = UnsafePointer<sockaddr>($0)
126126
_ = try attempt("bind", valid: isZero, bind(_socket, addr, socklen_t(MemoryLayout<sockaddr>.size)))
127-
_ = try attempt("listen", valid: isZero, listen(_socket, SOMAXCONN))
127+
_ = try attempt("listen", valid: isZero, listen(_socket, backlog))
128128
})
129129

130130
var actualSA = sockaddr_in()
@@ -295,8 +295,8 @@ class _HTTPServer: CustomStringConvertible {
295295
let tcpSocket: _TCPSocket
296296
var port: UInt16 { tcpSocket.port }
297297

298-
init(port: UInt16?) throws {
299-
tcpSocket = try _TCPSocket(port: port)
298+
init(port: UInt16?, backlog: Int32 = SOMAXCONN) throws {
299+
tcpSocket = try _TCPSocket(port: port, backlog: backlog)
300300
}
301301

302302
init(socket: _TCPSocket) {
@@ -1094,15 +1094,32 @@ enum InternalServerError : Error {
10941094
case badHeaders
10951095
}
10961096

1097+
extension LoopbackServerTest {
1098+
struct Options {
1099+
var serverBacklog: Int32
1100+
var isAsynchronous: Bool
1101+
1102+
static let `default` = Options(serverBacklog: SOMAXCONN, isAsynchronous: true)
1103+
}
1104+
}
10971105

10981106
class LoopbackServerTest : XCTestCase {
10991107
private static let staticSyncQ = DispatchQueue(label: "org.swift.TestFoundation.HTTPServer.StaticSyncQ")
11001108

11011109
private static var _serverPort: Int = -1
11021110
private static var _serverActive = false
11031111
private static var testServer: _HTTPServer? = nil
1104-
1105-
1112+
private static var _options: Options = .default
1113+
1114+
static var options: Options {
1115+
get {
1116+
return staticSyncQ.sync { _options }
1117+
}
1118+
set {
1119+
staticSyncQ.sync { _options = newValue }
1120+
}
1121+
}
1122+
11061123
static var serverPort: Int {
11071124
get {
11081125
return staticSyncQ.sync { _serverPort }
@@ -1119,27 +1136,42 @@ class LoopbackServerTest : XCTestCase {
11191136

11201137
override class func setUp() {
11211138
super.setUp()
1139+
Self.startServer()
1140+
}
11221141

1142+
override class func tearDown() {
1143+
Self.stopServer()
1144+
super.tearDown()
1145+
}
1146+
1147+
static func startServer() {
11231148
var _serverPort = 0
11241149
let dispatchGroup = DispatchGroup()
11251150

11261151
func runServer() throws {
1127-
testServer = try _HTTPServer(port: nil)
1152+
testServer = try _HTTPServer(port: nil, backlog: options.serverBacklog)
11281153
_serverPort = Int(testServer!.port)
11291154
serverActive = true
11301155
dispatchGroup.leave()
11311156

11321157
while serverActive {
11331158
do {
11341159
let httpServer = try testServer!.listen()
1135-
globalDispatchQueue.async {
1160+
1161+
func handleRequest() {
11361162
let subServer = TestURLSessionServer(httpServer: httpServer)
11371163
do {
11381164
try subServer.readAndRespond()
11391165
} catch {
11401166
NSLog("readAndRespond: \(error)")
11411167
}
11421168
}
1169+
1170+
if options.isAsynchronous {
1171+
globalDispatchQueue.async(execute: handleRequest)
1172+
} else {
1173+
handleRequest()
1174+
}
11431175
} catch {
11441176
if (serverActive) { // Ignore errors thrown on shutdown
11451177
NSLog("httpServer: \(error)")
@@ -1165,11 +1197,11 @@ class LoopbackServerTest : XCTestCase {
11651197
fatalError("Timedout waiting for server to be ready")
11661198
}
11671199
serverPort = _serverPort
1200+
debugLog("Listening on \(serverPort)")
11681201
}
1169-
1170-
override class func tearDown() {
1202+
1203+
static func stopServer() {
11711204
serverActive = false
11721205
try? testServer?.stop()
1173-
super.tearDown()
11741206
}
11751207
}

Tests/Foundation/Tests/TestURLSession.swift

+99-5
Original file line numberDiff line numberDiff line change
@@ -495,20 +495,101 @@ class TestURLSession: LoopbackServerTest {
495495
waitForExpectations(timeout: 30)
496496
}
497497

498-
func test_timeoutInterval() {
498+
func test_httpTimeout() {
499499
let config = URLSessionConfiguration.default
500500
config.timeoutIntervalForRequest = 10
501-
let urlString = "http://127.0.0.1:-1/Peru"
501+
let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/Peru"
502502
let session = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
503503
let expect = expectation(description: "GET \(urlString): will timeout")
504-
var req = URLRequest(url: URL(string: "http://127.0.0.1:-1/Peru")!)
504+
var req = URLRequest(url: URL(string: urlString)!)
505+
req.setValue("3", forHTTPHeaderField: "x-pause")
505506
req.timeoutInterval = 1
506507
let task = session.dataTask(with: req) { (data, _, error) -> Void in
507508
defer { expect.fulfill() }
508-
XCTAssertNotNil(error)
509+
XCTAssertEqual((error as? URLError)?.code, .timedOut, "Task should fail with URLError.timedOut error")
509510
}
510511
task.resume()
512+
waitForExpectations(timeout: 30)
513+
}
514+
515+
func test_connectTimeout() {
516+
// Reconfigure http server for this specific scenario:
517+
// a slow request keeps web server busy, while other
518+
// request times out on connection attempt.
519+
Self.stopServer()
520+
Self.options = Options(serverBacklog: 1, isAsynchronous: false)
521+
Self.startServer()
522+
523+
let config = URLSessionConfiguration.default
524+
let slowUrlString = "http://127.0.0.1:\(TestURLSession.serverPort)/Peru"
525+
let fastUrlString = "http://127.0.0.1:\(TestURLSession.serverPort)/Italy"
526+
let session = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
527+
let slowReqExpect = expectation(description: "GET \(slowUrlString): will complete")
528+
let fastReqExpect = expectation(description: "GET \(fastUrlString): will timeout")
529+
530+
var slowReq = URLRequest(url: URL(string: slowUrlString)!)
531+
slowReq.setValue("3", forHTTPHeaderField: "x-pause")
532+
533+
var fastReq = URLRequest(url: URL(string: fastUrlString)!)
534+
fastReq.timeoutInterval = 1
535+
536+
let slowTask = session.dataTask(with: slowReq) { (data, _, error) -> Void in
537+
slowReqExpect.fulfill()
538+
}
539+
let fastTask = session.dataTask(with: fastReq) { (data, _, error) -> Void in
540+
defer { fastReqExpect.fulfill() }
541+
XCTAssertEqual((error as? URLError)?.code, .timedOut, "Task should fail with URLError.timedOut error")
542+
}
543+
slowTask.resume()
544+
Thread.sleep(forTimeInterval: 0.1) // Give slow task some time to start
545+
fastTask.resume()
546+
547+
waitForExpectations(timeout: 30)
548+
549+
// Reconfigure http server back to default settings
550+
Self.stopServer()
551+
Self.options = .default
552+
Self.startServer()
553+
}
554+
555+
func test_repeatedRequestsStress() throws {
556+
let config = URLSessionConfiguration.default
557+
let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/Peru"
558+
let session = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
559+
let req = URLRequest(url: URL(string: urlString)!)
560+
561+
var requestsLeft = 3000
562+
let expect = expectation(description: "\(requestsLeft) x GET \(urlString)")
563+
564+
func doRequests(completion: @escaping () -> Void) {
565+
// We only care about completion of one of the tasks,
566+
// so we could move to next cycle.
567+
// Some overlapping would happen and that's what we
568+
// want actually to provoke issue with socket reuse
569+
// on Windows.
570+
let task = session.dataTask(with: req) { (_, _, _) -> Void in
571+
}
572+
task.resume()
573+
let task2 = session.dataTask(with: req) { (_, _, _) -> Void in
574+
}
575+
task2.resume()
576+
let task3 = session.dataTask(with: req) { (_, _, _) -> Void in
577+
completion()
578+
}
579+
task3.resume()
580+
}
581+
582+
func checkCountAndRunNext() {
583+
guard requestsLeft > 0 else {
584+
expect.fulfill()
585+
return
586+
}
587+
requestsLeft -= 1
588+
doRequests(completion: checkCountAndRunNext)
589+
}
511590

591+
checkCountAndRunNext()
592+
512593
waitForExpectations(timeout: 30)
513594
}
514595

@@ -2049,7 +2130,6 @@ class TestURLSession: LoopbackServerTest {
20492130
("test_taskTimeout", test_taskTimeout),
20502131
("test_verifyRequestHeaders", test_verifyRequestHeaders),
20512132
("test_verifyHttpAdditionalHeaders", test_verifyHttpAdditionalHeaders),
2052-
("test_timeoutInterval", test_timeoutInterval),
20532133
("test_httpRedirectionWithCode300", test_httpRedirectionWithCode300),
20542134
("test_httpRedirectionWithCode301_302", test_httpRedirectionWithCode301_302),
20552135
("test_httpRedirectionWithCode303", test_httpRedirectionWithCode303),
@@ -2098,6 +2178,7 @@ class TestURLSession: LoopbackServerTest {
20982178
/* ⚠️ */ testExpectedToFail(test_noDoubleCallbackWhenCancellingAndProtocolFailsFast, "This test crashes nondeterministically: https://bugs.swift.org/browse/SR-11310")),
20992179
/* ⚠️ */ ("test_cancelledTasksCannotBeResumed", testExpectedToFail(test_cancelledTasksCannotBeResumed, "Breaks on Ubuntu 18.04")),
21002180
]
2181+
#if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT
21012182
if #available(macOS 12.0, *) {
21022183
retVal.append(contentsOf: [
21032184
("test_webSocket", asyncTest(test_webSocket)),
@@ -2106,6 +2187,19 @@ class TestURLSession: LoopbackServerTest {
21062187
("test_webSocketSemiAbruptClose", asyncTest(test_webSocketSemiAbruptClose)),
21072188
])
21082189
}
2190+
#endif
2191+
#if os(Windows)
2192+
retVal.append(contentsOf: [
2193+
("test_httpTimeout", testExpectedToFail(test_httpTimeout, "Crashes: https://github.com/apple/swift-corelibs-foundation/issues/4791")),
2194+
("test_connectTimeout", testExpectedToFail(test_connectTimeout, "Crashes: https://github.com/apple/swift-corelibs-foundation/issues/4791")),
2195+
("test_repeatedRequestsStress", testExpectedToFail(test_repeatedRequestsStress, "Crashes: https://github.com/apple/swift-corelibs-foundation/issues/4791")),
2196+
])
2197+
#else
2198+
retVal.append(contentsOf: [
2199+
("test_httpTimeout", test_httpTimeout),
2200+
("test_connectTimeout", test_connectTimeout),
2201+
])
2202+
#endif
21092203
return retVal
21102204
}
21112205

0 commit comments

Comments
 (0)