Skip to content

Commit 3322d86

Browse files
author
Pushkar N Kulkarni
committed
Loopback tests for URLSession
1 parent aea8c0b commit 3322d86

File tree

4 files changed

+381
-51
lines changed

4 files changed

+381
-51
lines changed

Foundation.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
/* Begin PBXBuildFile section */
1010
0383A1751D2E558A0052E5D1 /* TestNSStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0383A1741D2E558A0052E5D1 /* TestNSStream.swift */; };
11+
1520469B1D8AEABE00D02E36 /* HTTPServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1520469A1D8AEABE00D02E36 /* HTTPServer.swift */; };
1112
294E3C1D1CC5E19300E4F44C /* TestNSAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294E3C1C1CC5E19300E4F44C /* TestNSAttributedString.swift */; };
1213
2EBE67A51C77BF0E006583D5 /* TestNSDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBE67A31C77BF05006583D5 /* TestNSDateFormatter.swift */; };
1314
528776141BF2629700CB0090 /* FoundationErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522C253A1BF16E1600804FC6 /* FoundationErrors.swift */; };
@@ -448,6 +449,7 @@
448449

449450
/* Begin PBXFileReference section */
450451
0383A1741D2E558A0052E5D1 /* TestNSStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSStream.swift; sourceTree = "<group>"; };
452+
1520469A1D8AEABE00D02E36 /* HTTPServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPServer.swift; sourceTree = "<group>"; };
451453
22B9C1E01C165D7A00DECFF9 /* TestNSDate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSDate.swift; sourceTree = "<group>"; };
452454
294E3C1C1CC5E19300E4F44C /* TestNSAttributedString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSAttributedString.swift; sourceTree = "<group>"; };
453455
2EBE67A31C77BF05006583D5 /* TestNSDateFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSDateFormatter.swift; sourceTree = "<group>"; };
@@ -1268,6 +1270,7 @@
12681270
EA66F6371BF1619600136161 /* TestFoundation */ = {
12691271
isa = PBXGroup;
12701272
children = (
1273+
1520469A1D8AEABE00D02E36 /* HTTPServer.swift */,
12711274
EA66F6381BF1619600136161 /* main.swift */,
12721275
EA66F65A1BF1976100136161 /* Tests */,
12731276
EA66F6391BF1619600136161 /* Resources */,
@@ -2183,6 +2186,7 @@
21832186
files = (
21842187
5FE52C951D147D1C00F7D270 /* TestNSTextCheckingResult.swift in Sources */,
21852188
5B13B3451C582D4C00651CE2 /* TestNSString.swift in Sources */,
2189+
1520469B1D8AEABE00D02E36 /* HTTPServer.swift in Sources */,
21862190
5B13B3471C582D4C00651CE2 /* TestNSThread.swift in Sources */,
21872191
5B13B32E1C582D4C00651CE2 /* TestNSFileManager.swift in Sources */,
21882192
5B13B3381C582D4C00651CE2 /* TestNSNotificationQueue.swift in Sources */,

TestFoundation/HTTPServer.swift

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

0 commit comments

Comments
 (0)