Skip to content

Commit 6254ea0

Browse files
committed
FoundationTest: add URLSession tests for swiftlang#4791
1 parent 9005e9f commit 6254ea0

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) {
@@ -1099,14 +1099,31 @@ enum InternalServerError : Error {
10991099
case badHeaders
11001100
}
11011101

1102+
extension LoopbackServerTest {
1103+
struct Options {
1104+
var serverBacklog: Int32
1105+
var isAsynchronous: Bool
1106+
1107+
static let `default` = Options(serverBacklog: SOMAXCONN, isAsynchronous: true)
1108+
}
1109+
}
11021110

11031111
class LoopbackServerTest : XCTestCase {
11041112
private static let staticSyncQ = DispatchQueue(label: "org.swift.TestFoundation.HTTPServer.StaticSyncQ")
11051113

11061114
private static var _serverPort: Int = -1
11071115
private static var _serverActive = false
11081116
private static var testServer: _HTTPServer? = nil
1109-
1117+
private static var _options: Options = .default
1118+
1119+
static var options: Options {
1120+
get {
1121+
return staticSyncQ.sync { _options }
1122+
}
1123+
set {
1124+
staticSyncQ.sync { _options = newValue }
1125+
}
1126+
}
11101127

11111128
static var serverPort: Int {
11121129
get {
@@ -1124,27 +1141,42 @@ class LoopbackServerTest : XCTestCase {
11241141

11251142
override class func setUp() {
11261143
super.setUp()
1144+
Self.startServer()
1145+
}
11271146

1147+
override class func tearDown() {
1148+
Self.stopServer()
1149+
super.tearDown()
1150+
}
1151+
1152+
static func startServer() {
11281153
var _serverPort = 0
11291154
let dispatchGroup = DispatchGroup()
11301155

11311156
func runServer() throws {
1132-
testServer = try _HTTPServer(port: nil)
1157+
testServer = try _HTTPServer(port: nil, backlog: options.serverBacklog)
11331158
_serverPort = Int(testServer!.port)
11341159
serverActive = true
11351160
dispatchGroup.leave()
11361161

11371162
while serverActive {
11381163
do {
11391164
let httpServer = try testServer!.listen()
1140-
globalDispatchQueue.async {
1165+
1166+
func handleRequest() {
11411167
let subServer = TestURLSessionServer(httpServer: httpServer)
11421168
do {
11431169
try subServer.readAndRespond()
11441170
} catch {
11451171
NSLog("readAndRespond: \(error)")
11461172
}
11471173
}
1174+
1175+
if options.isAsynchronous {
1176+
globalDispatchQueue.async(execute: handleRequest)
1177+
} else {
1178+
handleRequest()
1179+
}
11481180
} catch {
11491181
if (serverActive) { // Ignore errors thrown on shutdown
11501182
NSLog("httpServer: \(error)")
@@ -1170,11 +1202,11 @@ class LoopbackServerTest : XCTestCase {
11701202
fatalError("Timedout waiting for server to be ready")
11711203
}
11721204
serverPort = _serverPort
1205+
debugLog("Listening on \(serverPort)")
11731206
}
11741207

1175-
override class func tearDown() {
1208+
static func stopServer() {
11761209
serverActive = false
11771210
try? testServer?.stop()
1178-
super.tearDown()
11791211
}
11801212
}

Tests/Foundation/Tests/TestURLSession.swift

+116-5
Original file line numberDiff line numberDiff line change
@@ -519,21 +519,122 @@ class TestURLSession: LoopbackServerTest {
519519
waitForExpectations(timeout: 30)
520520
}
521521

522-
func test_timeoutInterval() {
522+
func test_httpTimeout() {
523523
let config = URLSessionConfiguration.default
524524
config.timeoutIntervalForRequest = 10
525-
let urlString = "http://127.0.0.1:-1/Peru"
525+
let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/Peru"
526526
let session = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
527527
let expect = expectation(description: "GET \(urlString): will timeout")
528-
var req = URLRequest(url: URL(string: "http://127.0.0.1:-1/Peru")!)
528+
var req = URLRequest(url: URL(string: urlString)!)
529+
req.setValue("3", forHTTPHeaderField: "x-pause")
529530
req.timeoutInterval = 1
530531
let task = session.dataTask(with: req) { (data, _, error) -> Void in
531532
defer { expect.fulfill() }
532-
XCTAssertNotNil(error)
533+
XCTAssertEqual((error as? URLError)?.code, .timedOut, "Task should fail with URLError.timedOut error")
533534
}
534535
task.resume()
536+
waitForExpectations(timeout: 30)
537+
}
538+
539+
func test_connectTimeout() {
540+
// Reconfigure http server for this specific scenario:
541+
// a slow request keeps web server busy, while other
542+
// request times out on connection attempt.
543+
Self.stopServer()
544+
Self.options = Options(serverBacklog: 1, isAsynchronous: false)
545+
Self.startServer()
546+
547+
let config = URLSessionConfiguration.default
548+
let slowUrlString = "http://127.0.0.1:\(TestURLSession.serverPort)/Peru"
549+
let fastUrlString = "http://127.0.0.1:\(TestURLSession.serverPort)/Italy"
550+
let session = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
551+
let slowReqExpect = expectation(description: "GET \(slowUrlString): will complete")
552+
let fastReqExpect = expectation(description: "GET \(fastUrlString): will timeout")
553+
554+
var slowReq = URLRequest(url: URL(string: slowUrlString)!)
555+
slowReq.setValue("3", forHTTPHeaderField: "x-pause")
556+
557+
var fastReq = URLRequest(url: URL(string: fastUrlString)!)
558+
fastReq.timeoutInterval = 1
559+
560+
let slowTask = session.dataTask(with: slowReq) { (data, _, error) -> Void in
561+
slowReqExpect.fulfill()
562+
}
563+
let fastTask = session.dataTask(with: fastReq) { (data, _, error) -> Void in
564+
defer { fastReqExpect.fulfill() }
565+
XCTAssertEqual((error as? URLError)?.code, .timedOut, "Task should fail with URLError.timedOut error")
566+
}
567+
slowTask.resume()
568+
Thread.sleep(forTimeInterval: 0.1) // Give slow task some time to start
569+
fastTask.resume()
535570

536571
waitForExpectations(timeout: 30)
572+
573+
// Reconfigure http server back to default settings
574+
Self.stopServer()
575+
Self.options = .default
576+
Self.startServer()
577+
}
578+
579+
func test_repeatedRequestsStress() throws {
580+
// TODO: try disabling curl connection cache to force socket close early. Or create several url sessions (they have cleanup in deinit)
581+
582+
let config = URLSessionConfiguration.default
583+
let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/Peru"
584+
let session = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
585+
let req = URLRequest(url: URL(string: urlString)!)
586+
587+
var requestsLeft = 3000
588+
let expect = expectation(description: "\(requestsLeft) x GET \(urlString)")
589+
590+
func doRequests(completion: @escaping () -> Void) {
591+
// We only care about completion of one of the tasks,
592+
// so we could move to next cycle.
593+
// Some overlapping would happen and that's what we
594+
// want actually to provoke issue with socket reuse
595+
// on Windows.
596+
let task = session.dataTask(with: req) { (_, _, _) -> Void in
597+
}
598+
task.resume()
599+
let task2 = session.dataTask(with: req) { (_, _, _) -> Void in
600+
}
601+
task2.resume()
602+
let task3 = session.dataTask(with: req) { (_, _, _) -> Void in
603+
completion()
604+
}
605+
task3.resume()
606+
}
607+
608+
func checkCountAndRunNext() {
609+
guard requestsLeft > 0 else {
610+
expect.fulfill()
611+
return
612+
}
613+
requestsLeft -= 1
614+
doRequests(completion: checkCountAndRunNext)
615+
}
616+
617+
checkCountAndRunNext()
618+
619+
waitForExpectations(timeout: 30)
620+
}
621+
622+
func test_largePost() throws {
623+
let session = URLSession(configuration: URLSessionConfiguration.default)
624+
var dataTask: URLSessionDataTask? = nil
625+
626+
let data = Data((0 ..< 131076).map { _ in UInt8.random(in: UInt8.min ... UInt8.max) })
627+
var req = URLRequest(url: URL(string: "http://127.0.0.1:\(TestURLSession.serverPort)/POST")!)
628+
req.httpMethod = "POST"
629+
req.httpBody = data
630+
631+
let e = expectation(description: "POST completed")
632+
dataTask = session.uploadTask(with: req, from: data) { data, response, error in
633+
e.fulfill()
634+
}
635+
dataTask?.resume()
636+
637+
waitForExpectations(timeout: 5)
537638
}
538639

539640
func test_httpRedirectionWithCode300() throws {
@@ -2130,7 +2231,8 @@ class TestURLSession: LoopbackServerTest {
21302231
("test_taskTimeout", test_taskTimeout),
21312232
("test_verifyRequestHeaders", test_verifyRequestHeaders),
21322233
("test_verifyHttpAdditionalHeaders", test_verifyHttpAdditionalHeaders),
2133-
("test_timeoutInterval", test_timeoutInterval),
2234+
("test_httpTimeout", test_httpTimeout),
2235+
("test_connectTimeout", test_connectTimeout),
21342236
("test_httpRedirectionWithCode300", test_httpRedirectionWithCode300),
21352237
("test_httpRedirectionWithCode301_302", test_httpRedirectionWithCode301_302),
21362238
("test_httpRedirectionWithCode303", test_httpRedirectionWithCode303),
@@ -2181,6 +2283,7 @@ class TestURLSession: LoopbackServerTest {
21812283
/* ⚠️ */ testExpectedToFail(test_noDoubleCallbackWhenCancellingAndProtocolFailsFast, "This test crashes nondeterministically: https://bugs.swift.org/browse/SR-11310")),
21822284
/* ⚠️ */ ("test_cancelledTasksCannotBeResumed", testExpectedToFail(test_cancelledTasksCannotBeResumed, "Breaks on Ubuntu 18.04")),
21832285
]
2286+
#if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT
21842287
if #available(macOS 12.0, *) {
21852288
retVal.append(contentsOf: [
21862289
("test_webSocket", asyncTest(test_webSocket)),
@@ -2189,6 +2292,14 @@ class TestURLSession: LoopbackServerTest {
21892292
("test_webSocketSemiAbruptClose", asyncTest(test_webSocketSemiAbruptClose)),
21902293
])
21912294
}
2295+
#endif
2296+
// This is heavy test and it could time out in CI environment giving false negative result.
2297+
// Uncomment to use for local URLSession stability testing.
2298+
// #if os(Windows)
2299+
// retVal.append(contentsOf: [
2300+
// ("test_repeatedRequestsStress", test_repeatedRequestsStress),
2301+
// ])
2302+
// #endif
21922303
return retVal
21932304
}
21942305

0 commit comments

Comments
 (0)