Skip to content

Commit 3bd7f80

Browse files
author
Pushkar N Kulkarni
committed
Loopback tests for URLSession
1 parent 1f9d6a6 commit 3bd7f80

File tree

3 files changed

+341
-38
lines changed

3 files changed

+341
-38
lines changed

TestFoundation/HTTPServer.swift

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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 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+
static let EMPTY = ""
25+
}
26+
27+
class _TCPSocket {
28+
29+
private var listenSocket: Int32!
30+
private var socketAddress = UnsafeMutablePointer<sockaddr_in>.allocate(capacity: 1)
31+
private var connectionSocket: Int32!
32+
33+
private func isNotNegative(r: CInt) -> Bool {
34+
return r != -1
35+
}
36+
37+
private func isZero(r: CInt) -> Bool {
38+
return r == 0
39+
}
40+
41+
private func attempt(_ name: String, file: String = #file, line: UInt = #line, valid: (CInt) -> Bool, _ b: @autoclosure () -> CInt) throws -> CInt {
42+
let r = b()
43+
guard valid(r) else { throw ServerError(operation: name, errno: r, file: file, line: line) }
44+
return r
45+
}
46+
47+
init(port: UInt16) throws {
48+
listenSocket = try attempt("socket", valid: isNotNegative, socket(AF_INET, Int32(SOCK_STREAM.rawValue), Int32(IPPROTO_TCP)))
49+
var on: Int = 1
50+
_ = try attempt("setsockopt", valid: isZero, setsockopt(listenSocket, SOL_SOCKET, SO_REUSEADDR, &on, socklen_t(MemoryLayout<Int>.size)))
51+
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))
52+
socketAddress.initialize(to: sa)
53+
try socketAddress.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size, {
54+
let addr = UnsafePointer<sockaddr>($0)
55+
_ = try attempt("bind", valid: isZero, bind(listenSocket, addr, socklen_t(MemoryLayout<sockaddr>.size)))
56+
})
57+
}
58+
59+
func acceptConnection(notify: DispatchSemaphore) throws {
60+
_ = try attempt("listen", valid: isZero, listen(listenSocket, SOMAXCONN))
61+
try socketAddress.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size, {
62+
let addr = UnsafeMutablePointer<sockaddr>($0)
63+
var sockLen = socklen_t(MemoryLayout<sockaddr>.size)
64+
notify.signal()
65+
connectionSocket = try attempt("accept", valid: isNotNegative, accept(listenSocket, addr, &sockLen))
66+
})
67+
}
68+
69+
func readData() throws -> String {
70+
var buffer = [UInt8](repeating: 0, count: 4096)
71+
_ = try attempt("read", valid: isNotNegative, CInt(read(connectionSocket, &buffer, 4096)))
72+
return String(cString: &buffer)
73+
}
74+
75+
func writeData(data: String) throws {
76+
var bytes = Array(data.utf8)
77+
_ = try attempt("write", valid: isNotNegative, CInt(write(connectionSocket, &bytes, data.utf8.count)))
78+
}
79+
80+
func shutdown() {
81+
close(connectionSocket)
82+
close(listenSocket)
83+
}
84+
}
85+
86+
class _HTTPServer {
87+
88+
let socket: _TCPSocket
89+
90+
init(port: UInt16) throws {
91+
socket = try _TCPSocket(port: port)
92+
}
93+
94+
public class func create(port: UInt16) throws -> _HTTPServer {
95+
return try _HTTPServer(port: port)
96+
}
97+
98+
public func listen(notify: DispatchSemaphore) throws {
99+
try socket.acceptConnection(notify: notify)
100+
}
101+
102+
public func stop() {
103+
socket.shutdown()
104+
}
105+
106+
public func request() throws -> _HTTPRequest {
107+
return _HTTPRequest(request: try socket.readData())
108+
}
109+
110+
public func respond(with response: _HTTPResponse) throws {
111+
try socket.writeData(data: response.description)
112+
}
113+
}
114+
115+
struct _HTTPRequest {
116+
enum Method : String {
117+
case GET
118+
case POST
119+
case PUT
120+
}
121+
let method: Method
122+
let uri: String
123+
let body: String
124+
let headers: [String]
125+
126+
public init(request: String) {
127+
let lines = request.components(separatedBy: _HTTPUtils.CRLF2)[0].components(separatedBy: _HTTPUtils.CRLF)
128+
headers = Array(lines[0...lines.count-2])
129+
method = Method(rawValue: headers[0].components(separatedBy: " ")[0])!
130+
uri = headers[0].components(separatedBy: " ")[1]
131+
body = lines.last!
132+
}
133+
134+
}
135+
136+
struct _HTTPResponse {
137+
enum Response : Int {
138+
case OK = 200
139+
}
140+
private let responseCode: Response
141+
private let headers: String
142+
private let body: String
143+
144+
public init(response: Response, headers: String = _HTTPUtils.EMPTY, body: String) {
145+
self.responseCode = response
146+
self.headers = headers
147+
self.body = body
148+
}
149+
150+
public var description: String {
151+
let statusLine = _HTTPUtils.VERSION + _HTTPUtils.SPACE + "\(responseCode.rawValue)" + _HTTPUtils.SPACE + "\(responseCode)"
152+
return statusLine + (headers != _HTTPUtils.EMPTY ? _HTTPUtils.CRLF + headers : _HTTPUtils.EMPTY) + _HTTPUtils.CRLF2 + body
153+
}
154+
}
155+
156+
public class TestURLSessionServer {
157+
let capitals: [String:String] = ["Nepal":"Kathmandu",
158+
"Peru":"Lima",
159+
"Italy":"Rome",
160+
"USA":"Washington, D.C.",
161+
"country.txt": "A country is a region that is identified as a distinct national entity in political geography"]
162+
let httpServer: _HTTPServer
163+
164+
public init (port: UInt16) throws {
165+
httpServer = try _HTTPServer.create(port: port)
166+
}
167+
public func start(started: DispatchSemaphore) throws {
168+
started.signal()
169+
try httpServer.listen(notify: started)
170+
}
171+
172+
public func readAndRespond() throws {
173+
try httpServer.respond(with: process(request: httpServer.request()))
174+
}
175+
176+
func process(request: _HTTPRequest) -> _HTTPResponse {
177+
if request.method == .GET {
178+
return getResponse(uri: request.uri)
179+
} else {
180+
fatalError("Unsupported method!")
181+
}
182+
}
183+
184+
func getResponse(uri: String) -> _HTTPResponse {
185+
if uri == "/country.txt" {
186+
let text = capitals[String(uri.characters.dropFirst())]!
187+
return _HTTPResponse(response: .OK, headers: "Content-Length: \(text.characters.count)", body: text)
188+
}
189+
return _HTTPResponse(response: .OK, body: capitals[String(uri.characters.dropFirst())]!)
190+
}
191+
192+
func stop() {
193+
httpServer.stop()
194+
}
195+
}
196+
197+
struct ServerError : Error {
198+
let operation: String
199+
let errno: CInt
200+
let file: String
201+
let line: UInt
202+
var _code: Int { return Int(errno) }
203+
var _domain: String { return NSPOSIXErrorDomain }
204+
}
205+
206+
207+
extension ServerError : CustomStringConvertible {
208+
var description: String {
209+
let s = String(validatingUTF8: strerror(errno)) ?? ""
210+
return "\(operation) failed: \(s) (\(_code))"
211+
}
212+
}

0 commit comments

Comments
 (0)