Skip to content

Commit 6a1cc4d

Browse files
author
Pushkar N Kulkarni
committed
Loopback tests for URLSession
1 parent 58b375f commit 6a1cc4d

File tree

3 files changed

+276
-26
lines changed

3 files changed

+276
-26
lines changed

TestFoundation/HTTPServer.swift

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See http://swift.org/LICENSE.txt for license information
7+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
8+
//
9+
10+
11+
//This is a very rudimentary HTTP server written plainly for testing URLSession.
12+
//It is not concurrent. It listens on a port, reads once and writes back only once.
13+
//We can make it better everytime we need more functionality to test different the aspects of URLSession.
14+
15+
import Foundation
16+
import Dispatch
17+
import Glibc
18+
19+
struct _HTTPUtils {
20+
static let CRLF = "\r\n"
21+
static let VERSION = "HTTP/1.1"
22+
static let SPACE = " "
23+
static let CRLF2 = CRLF + CRLF
24+
}
25+
26+
class _TCPSocket {
27+
28+
private var listenSocket: Int32!
29+
private var socketAddress = UnsafeMutablePointer<sockaddr_in>.allocate(capacity: 1)
30+
private var connectionSocket: Int32!
31+
32+
private func isNotNegative(r: CInt) -> Bool {
33+
return r != -1
34+
}
35+
36+
private func isZero(r: CInt) -> Bool {
37+
return r == 0
38+
}
39+
40+
private func attempt(_ name: String, file: String = #file, line: UInt = #line, valid: (CInt) -> Bool, _ b: @autoclosure () -> CInt) throws -> CInt {
41+
let r = b()
42+
guard valid(r) else { throw ServerError(operation: name, errno: r, file: file, line: line) }
43+
return r
44+
}
45+
46+
init(port: UInt16) throws {
47+
listenSocket = try attempt("socket", valid: isNotNegative, socket(AF_INET, Int32(SOCK_STREAM.rawValue), Int32(IPPROTO_TCP)))
48+
var on: Int = 1
49+
_ = try attempt("setsockopt", valid: isZero, setsockopt(listenSocket, SOL_SOCKET, SO_REUSEADDR, &on, socklen_t(MemoryLayout<Int>.size)))
50+
let sa = sockaddr_in(sin_family: sa_family_t(AF_INET), sin_port: htons(port), sin_addr: in_addr(s_addr: INADDR_ANY), sin_zero: (0,0,0,0,0,0,0,0))
51+
socketAddress.initialize(to: sa)
52+
try socketAddress.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size, {
53+
let addr = UnsafePointer<sockaddr>($0)
54+
_ = try attempt("bind", valid: isZero, bind(listenSocket, addr, socklen_t(MemoryLayout<sockaddr>.size)))
55+
})
56+
}
57+
58+
func acceptConnection(notify: DispatchSemaphore) throws {
59+
_ = try attempt("listen", valid: isZero, listen(listenSocket, SOMAXCONN))
60+
try socketAddress.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size, {
61+
let addr = UnsafeMutablePointer<sockaddr>($0)
62+
var sockLen = socklen_t(MemoryLayout<sockaddr>.size)
63+
notify.signal()
64+
connectionSocket = try attempt("accept", valid: isNotNegative, accept(listenSocket, addr, &sockLen))
65+
})
66+
}
67+
68+
func readData() throws -> String {
69+
var buffer = [UInt8](repeating: 0, count: 4096)
70+
_ = try attempt("read", valid: isNotNegative, CInt(read(connectionSocket, &buffer, 4096)))
71+
return String(cString: &buffer)
72+
}
73+
74+
func writeData(data: String) throws {
75+
var bytes = Array(data.utf8)
76+
_ = try attempt("write", valid: isNotNegative, CInt(write(connectionSocket, &bytes, data.utf8.count)))
77+
}
78+
79+
func shutdown() {
80+
close(connectionSocket)
81+
close(listenSocket)
82+
}
83+
}
84+
85+
class _HTTPServer {
86+
87+
let socket: _TCPSocket
88+
89+
init(port: UInt16) throws {
90+
socket = try _TCPSocket(port: port)
91+
}
92+
93+
public class func create(port: UInt16) throws -> _HTTPServer {
94+
return try _HTTPServer(port: port)
95+
}
96+
97+
public func listen(notify: DispatchSemaphore) throws {
98+
try socket.acceptConnection(notify: notify)
99+
}
100+
101+
public func stop() {
102+
socket.shutdown()
103+
}
104+
105+
public func request() throws -> _HTTPRequest {
106+
return _HTTPRequest(request: try socket.readData())
107+
}
108+
109+
public func respond(with response: _HTTPResponse) throws {
110+
try socket.writeData(data: response.description)
111+
}
112+
}
113+
114+
struct _HTTPRequest {
115+
enum Method : String {
116+
case GET
117+
case POST
118+
case PUT
119+
}
120+
let method: Method
121+
let uri: String
122+
let body: String
123+
let headers: [String]
124+
125+
public init(request: String) {
126+
let lines = request.components(separatedBy: _HTTPUtils.CRLF2)[0].components(separatedBy: _HTTPUtils.CRLF)
127+
headers = Array(lines[0...lines.count-2])
128+
method = Method(rawValue: headers[0].components(separatedBy: " ")[0])!
129+
uri = headers[0].components(separatedBy: " ")[1]
130+
body = lines.last!
131+
}
132+
133+
}
134+
135+
struct _HTTPResponse {
136+
enum Response : Int {
137+
case OK = 200
138+
}
139+
private let responseCode: Response
140+
private let headers: String
141+
private let body: String
142+
143+
public init(response: Response, headers: String = "", body: String) {
144+
self.responseCode = response
145+
self.headers = headers
146+
self.body = body
147+
}
148+
149+
public var description: String {
150+
let statusLine = _HTTPUtils.VERSION + _HTTPUtils.SPACE + "\(responseCode.rawValue)" + _HTTPUtils.SPACE + "\(responseCode)"
151+
return statusLine + _HTTPUtils.CRLF2 + body
152+
}
153+
}
154+
155+
public class TestURLSessionServer {
156+
let capitals: [String:String] = ["Nepal":"Kathmandu", "Peru":"Lima", "Italy":"Rome", "USA":"Washington, D.C."]
157+
let httpServer: _HTTPServer
158+
159+
public init (port: UInt16) throws {
160+
httpServer = try _HTTPServer.create(port: port)
161+
}
162+
public func start(started: DispatchSemaphore) throws {
163+
started.signal()
164+
try httpServer.listen(notify: started)
165+
}
166+
167+
public func readAndRespond() throws {
168+
try httpServer.respond(with: process(request: httpServer.request()))
169+
}
170+
171+
func process(request: _HTTPRequest) -> _HTTPResponse {
172+
if request.method == .GET {
173+
return getResponse(uri: request.uri)
174+
} else {
175+
fatalError("Unsupported method!")
176+
}
177+
}
178+
179+
func getResponse(uri: String) -> _HTTPResponse {
180+
return _HTTPResponse(response: .OK, body: capitals[String(uri.characters.dropFirst())]!)
181+
}
182+
183+
func stop() {
184+
httpServer.stop()
185+
}
186+
}
187+
188+
struct ServerError : Error {
189+
let operation: String
190+
let errno: CInt
191+
let file: String
192+
let line: UInt
193+
var _code: Int { return Int(errno) }
194+
var _domain: String { return NSPOSIXErrorDomain }
195+
}
196+
197+
198+
extension ServerError : CustomStringConvertible {
199+
var description: String {
200+
let s = String(validatingUTF8: strerror(errno)) ?? ""
201+
return "\(operation) failed: \(s) (\(_code))"
202+
}
203+
}

TestFoundation/TestNSURLSession.swift

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ import XCTest
1515
import SwiftFoundation
1616
import SwiftXCTest
1717
#endif
18+
import Dispatch
1819

1920
class TestURLSession : XCTestCase {
2021

22+
var serverPort: Int = -1
23+
2124
static var allTests: [(String, (TestURLSession) -> () throws -> Void)] {
2225
return [
2326
("test_dataTaskWithURL", test_dataTaskWithURL),
@@ -32,8 +35,35 @@ class TestURLSession : XCTestCase {
3235
]
3336
}
3437

38+
private func runServer(with condition: DispatchSemaphore) throws {
39+
let start = 21961
40+
for port in start...(start+100) { //we must find at least one port to bind
41+
do {
42+
serverPort = port
43+
let test = try TestURLSessionServer(port: UInt16(port))
44+
try test.start(started: condition)
45+
try test.readAndRespond()
46+
test.stop()
47+
} catch let e as ServerError {
48+
if e.operation != "bind" { continue }
49+
throw e
50+
}
51+
}
52+
}
53+
3554
func test_dataTaskWithURL() {
36-
let urlString = "https://restcountries.eu/rest/v1/name/Nepal?fullText=true"
55+
let serverReady = DispatchSemaphore(value: 0)
56+
let queue = DispatchQueue.global()
57+
queue.async {
58+
do {
59+
try self.runServer(with: serverReady)
60+
} catch {
61+
XCTAssertTrue(true)
62+
return
63+
}
64+
}
65+
serverReady.wait()
66+
let urlString = "http://127.0.0.1:\(serverPort)/Nepal"
3767
let url = URL(string: urlString)!
3868
let d = DataTask(with: expectation(description: "data task"))
3969
d.run(with: url)
@@ -44,7 +74,18 @@ class TestURLSession : XCTestCase {
4474
}
4575

4676
func test_dataTaskWithURLCompletionHandler() {
47-
let urlString = "https://restcountries.eu/rest/v1/name/USA?fullText=true"
77+
let serverReady = DispatchSemaphore(value: 0)
78+
let queue = DispatchQueue.global()
79+
queue.async {
80+
do {
81+
try self.runServer(with: serverReady)
82+
} catch {
83+
XCTAssertTrue(true)
84+
return
85+
}
86+
}
87+
serverReady.wait()
88+
let urlString = "http://127.0.0.1:\(serverPort)/USA"
4889
let url = URL(string: urlString)!
4990
let config = URLSessionConfiguration.default
5091
config.timeoutIntervalForRequest = 8
@@ -60,13 +101,7 @@ class TestURLSession : XCTestCase {
60101

61102
let httpResponse = response as! HTTPURLResponse?
62103
XCTAssertEqual(200, httpResponse!.statusCode, "HTTP response code is not 200")
63-
do {
64-
let json = try JSONSerialization.jsonObject(with: data!, options: [])
65-
let arr = json as? Array<Any>
66-
let first = arr![0]
67-
let result = first as? [String : Any]
68-
expectedResult = result!["capital"] as! String
69-
} catch { }
104+
expectedResult = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)!._bridgeToSwift()
70105
XCTAssertEqual("Washington, D.C.", expectedResult, "Did not receive expected value")
71106
expect.fulfill()
72107
}
@@ -75,7 +110,18 @@ class TestURLSession : XCTestCase {
75110
}
76111

77112
func test_dataTaskWithURLRequest() {
78-
let urlString = "https://restcountries.eu/rest/v1/name/Peru?fullText=true"
113+
let serverReady = DispatchSemaphore(value: 0)
114+
let queue = DispatchQueue.global()
115+
queue.async {
116+
do {
117+
try self.runServer(with: serverReady)
118+
} catch {
119+
XCTAssertTrue(true)
120+
return
121+
}
122+
}
123+
serverReady.wait()
124+
let urlString = "http://127.0.0.1:\(serverPort)/Peru"
79125
let urlRequest = URLRequest(url: URL(string: urlString)!)
80126
let d = DataTask(with: expectation(description: "data task"))
81127
d.run(with: urlRequest)
@@ -86,7 +132,18 @@ class TestURLSession : XCTestCase {
86132
}
87133

88134
func test_dataTaskWithURLRequestCompletionHandler() {
89-
let urlString = "https://restcountries.eu/rest/v1/name/Italy?fullText=true"
135+
let serverReady = DispatchSemaphore(value: 0)
136+
let queue = DispatchQueue.global()
137+
queue.async {
138+
do {
139+
try self.runServer(with: serverReady)
140+
} catch {
141+
XCTAssertTrue(true)
142+
return
143+
}
144+
}
145+
serverReady.wait()
146+
let urlString = "http://127.0.0.1:\(serverPort)/Italy"
90147
let urlRequest = URLRequest(url: URL(string: urlString)!)
91148
let config = URLSessionConfiguration.default
92149
config.timeoutIntervalForRequest = 8
@@ -101,13 +158,7 @@ class TestURLSession : XCTestCase {
101158
}
102159
let httpResponse = response as! HTTPURLResponse?
103160
XCTAssertEqual(200, httpResponse!.statusCode, "HTTP response code is not 200")
104-
do {
105-
let json = try JSONSerialization.jsonObject(with: data!, options: [])
106-
let arr = json as? Array<Any>
107-
let first = arr![0]
108-
let result = first as? [String : Any]
109-
expectedResult = result!["capital"] as! String
110-
} catch { }
161+
expectedResult = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)!._bridgeToSwift()
111162
XCTAssertEqual("Rome", expectedResult, "Did not receive expected value")
112163
expect.fulfill()
113164
}
@@ -194,14 +245,7 @@ class DataTask: NSObject {
194245

195246
extension DataTask : URLSessionDataDelegate {
196247
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
197-
do {
198-
let json = try JSONSerialization.jsonObject(with: data, options: [])
199-
let arr = json as? Array<Any>
200-
let first = arr![0]
201-
let result = first as? [String : Any]
202-
capital = result!["capital"] as! String
203-
} catch { }
204-
248+
capital = NSString(data: data, encoding: String.Encoding.utf8.rawValue)!._bridgeToSwift()
205249
dataTaskExpectation.fulfill()
206250
}
207251
}

build.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,8 +458,11 @@
458458
# TODO: Probably this should be another 'product', but for now it's simply a phase
459459
foundation_tests = SwiftExecutable('TestFoundation', [
460460
'TestFoundation/main.swift',
461+
'TestFoundation/HTTPServer.swift',
461462
] + glob.glob('./TestFoundation/Test*.swift')) # all TestSomething.swift are considered sources to the test project in the TestFoundation directory
462463

464+
Configuration.current.extra_ld_flags = '-L'+Configuration.current.variables["LIBDISPATCH_BUILD_DIR"]+'/src/.libs'
465+
463466
foundation_tests.add_dependency(foundation_tests_resources)
464467
foundation.add_phase(foundation_tests_resources)
465468
foundation.add_phase(foundation_tests)

0 commit comments

Comments
 (0)