Skip to content

Commit 912a617

Browse files
committed
FoundationTest: add URLSession tests for swiftlang#4791
1 parent 7258a8d commit 912a617

File tree

2 files changed

+157
-14
lines changed

2 files changed

+157
-14
lines changed

Tests/Foundation/HTTPServer.swift

+41-9
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,14 +1094,31 @@ 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-
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+
}
11051122

11061123
static var serverPort: Int {
11071124
get {
@@ -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
}
11691202

1170-
override class func tearDown() {
1203+
static func stopServer() {
11711204
serverActive = false
11721205
try? testServer?.stop()
1173-
super.tearDown()
11741206
}
11751207
}

Tests/Foundation/Tests/TestURLSession.swift

+116-5
Original file line numberDiff line numberDiff line change
@@ -495,21 +495,122 @@ 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()
511546

512547
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+
// TODO: try disabling curl connection cache to force socket close early. Or create several url sessions (they have cleanup in deinit)
557+
558+
let config = URLSessionConfiguration.default
559+
let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/Peru"
560+
let session = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
561+
let req = URLRequest(url: URL(string: urlString)!)
562+
563+
var requestsLeft = 3000
564+
let expect = expectation(description: "\(requestsLeft) x GET \(urlString)")
565+
566+
func doRequests(completion: @escaping () -> Void) {
567+
// We only care about completion of one of the tasks,
568+
// so we could move to next cycle.
569+
// Some overlapping would happen and that's what we
570+
// want actually to provoke issue with socket reuse
571+
// on Windows.
572+
let task = session.dataTask(with: req) { (_, _, _) -> Void in
573+
}
574+
task.resume()
575+
let task2 = session.dataTask(with: req) { (_, _, _) -> Void in
576+
}
577+
task2.resume()
578+
let task3 = session.dataTask(with: req) { (_, _, _) -> Void in
579+
completion()
580+
}
581+
task3.resume()
582+
}
583+
584+
func checkCountAndRunNext() {
585+
guard requestsLeft > 0 else {
586+
expect.fulfill()
587+
return
588+
}
589+
requestsLeft -= 1
590+
doRequests(completion: checkCountAndRunNext)
591+
}
592+
593+
checkCountAndRunNext()
594+
595+
waitForExpectations(timeout: 30)
596+
}
597+
598+
func test_largePost() throws {
599+
let session = URLSession(configuration: URLSessionConfiguration.default)
600+
var dataTask: URLSessionDataTask? = nil
601+
602+
let data = Data((0 ..< 131076).map { _ in UInt8.random(in: UInt8.min ... UInt8.max) })
603+
var req = URLRequest(url: URL(string: "http://127.0.0.1:\(TestURLSession.serverPort)/POST")!)
604+
req.httpMethod = "POST"
605+
req.httpBody = data
606+
607+
let e = expectation(description: "POST completed")
608+
dataTask = session.uploadTask(with: req, from: data) { data, response, error in
609+
e.fulfill()
610+
}
611+
dataTask?.resume()
612+
613+
waitForExpectations(timeout: 5)
513614
}
514615

515616
func test_httpRedirectionWithCode300() throws {
@@ -2049,7 +2150,8 @@ class TestURLSession: LoopbackServerTest {
20492150
("test_taskTimeout", test_taskTimeout),
20502151
("test_verifyRequestHeaders", test_verifyRequestHeaders),
20512152
("test_verifyHttpAdditionalHeaders", test_verifyHttpAdditionalHeaders),
2052-
("test_timeoutInterval", test_timeoutInterval),
2153+
("test_httpTimeout", test_httpTimeout),
2154+
("test_connectTimeout", test_connectTimeout),
20532155
("test_httpRedirectionWithCode300", test_httpRedirectionWithCode300),
20542156
("test_httpRedirectionWithCode301_302", test_httpRedirectionWithCode301_302),
20552157
("test_httpRedirectionWithCode303", test_httpRedirectionWithCode303),
@@ -2098,6 +2200,7 @@ class TestURLSession: LoopbackServerTest {
20982200
/* ⚠️ */ testExpectedToFail(test_noDoubleCallbackWhenCancellingAndProtocolFailsFast, "This test crashes nondeterministically: https://bugs.swift.org/browse/SR-11310")),
20992201
/* ⚠️ */ ("test_cancelledTasksCannotBeResumed", testExpectedToFail(test_cancelledTasksCannotBeResumed, "Breaks on Ubuntu 18.04")),
21002202
]
2203+
#if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT
21012204
if #available(macOS 12.0, *) {
21022205
retVal.append(contentsOf: [
21032206
("test_webSocket", asyncTest(test_webSocket)),
@@ -2106,6 +2209,14 @@ class TestURLSession: LoopbackServerTest {
21062209
("test_webSocketSemiAbruptClose", asyncTest(test_webSocketSemiAbruptClose)),
21072210
])
21082211
}
2212+
#endif
2213+
// This is heavy test and it could time out in CI environment giving false negative result.
2214+
// Uncomment to use for local URLSession stability testing.
2215+
// #if os(Windows)
2216+
// retVal.append(contentsOf: [
2217+
// ("test_repeatedRequestsStress", test_repeatedRequestsStress),
2218+
// ])
2219+
// #endif
21092220
return retVal
21102221
}
21112222

0 commit comments

Comments
 (0)