From 3322d86f67b28e6986f50f8d0c04de88845912cb Mon Sep 17 00:00:00 2001 From: Pushkar N Kulkarni Date: Wed, 31 Aug 2016 17:43:08 +0530 Subject: [PATCH] Loopback tests for URLSession --- Foundation.xcodeproj/project.pbxproj | 4 + TestFoundation/HTTPServer.swift | 245 ++++++++++++++++++++++++++ TestFoundation/TestNSURLSession.swift | 180 +++++++++++++------ build.py | 3 + 4 files changed, 381 insertions(+), 51 deletions(-) create mode 100644 TestFoundation/HTTPServer.swift diff --git a/Foundation.xcodeproj/project.pbxproj b/Foundation.xcodeproj/project.pbxproj index 66715d5c1e..3e30c84521 100755 --- a/Foundation.xcodeproj/project.pbxproj +++ b/Foundation.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 0383A1751D2E558A0052E5D1 /* TestNSStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0383A1741D2E558A0052E5D1 /* TestNSStream.swift */; }; + 1520469B1D8AEABE00D02E36 /* HTTPServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1520469A1D8AEABE00D02E36 /* HTTPServer.swift */; }; 294E3C1D1CC5E19300E4F44C /* TestNSAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294E3C1C1CC5E19300E4F44C /* TestNSAttributedString.swift */; }; 2EBE67A51C77BF0E006583D5 /* TestNSDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBE67A31C77BF05006583D5 /* TestNSDateFormatter.swift */; }; 528776141BF2629700CB0090 /* FoundationErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522C253A1BF16E1600804FC6 /* FoundationErrors.swift */; }; @@ -448,6 +449,7 @@ /* Begin PBXFileReference section */ 0383A1741D2E558A0052E5D1 /* TestNSStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSStream.swift; sourceTree = ""; }; + 1520469A1D8AEABE00D02E36 /* HTTPServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPServer.swift; sourceTree = ""; }; 22B9C1E01C165D7A00DECFF9 /* TestNSDate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSDate.swift; sourceTree = ""; }; 294E3C1C1CC5E19300E4F44C /* TestNSAttributedString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSAttributedString.swift; sourceTree = ""; }; 2EBE67A31C77BF05006583D5 /* TestNSDateFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSDateFormatter.swift; sourceTree = ""; }; @@ -1268,6 +1270,7 @@ EA66F6371BF1619600136161 /* TestFoundation */ = { isa = PBXGroup; children = ( + 1520469A1D8AEABE00D02E36 /* HTTPServer.swift */, EA66F6381BF1619600136161 /* main.swift */, EA66F65A1BF1976100136161 /* Tests */, EA66F6391BF1619600136161 /* Resources */, @@ -2183,6 +2186,7 @@ files = ( 5FE52C951D147D1C00F7D270 /* TestNSTextCheckingResult.swift in Sources */, 5B13B3451C582D4C00651CE2 /* TestNSString.swift in Sources */, + 1520469B1D8AEABE00D02E36 /* HTTPServer.swift in Sources */, 5B13B3471C582D4C00651CE2 /* TestNSThread.swift in Sources */, 5B13B32E1C582D4C00651CE2 /* TestNSFileManager.swift in Sources */, 5B13B3381C582D4C00651CE2 /* TestNSNotificationQueue.swift in Sources */, diff --git a/TestFoundation/HTTPServer.swift b/TestFoundation/HTTPServer.swift new file mode 100644 index 0000000000..5b032a247c --- /dev/null +++ b/TestFoundation/HTTPServer.swift @@ -0,0 +1,245 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// + + +//This is a very rudimentary HTTP server written plainly for testing URLSession. +//It is not concurrent. It listens on a port, reads once and writes back only once. +//We can make it better everytime we need more functionality to test different aspects of URLSession. + +import Dispatch + +#if DEPLOYMENT_RUNTIME_OBJC || os(Linux) + import Foundation + import Glibc +#else + import CoreFoundation + import SwiftFoundation + import Darwin +#endif + +public let globalDispatchQueue = DispatchQueue.global() + +struct _HTTPUtils { + static let CRLF = "\r\n" + static let VERSION = "HTTP/1.1" + static let SPACE = " " + static let CRLF2 = CRLF + CRLF + static let EMPTY = "" +} + +class _TCPSocket { + + private var listenSocket: Int32! + private var socketAddress = UnsafeMutablePointer.allocate(capacity: 1) + private var connectionSocket: Int32! + + private func isNotNegative(r: CInt) -> Bool { + return r != -1 + } + + private func isZero(r: CInt) -> Bool { + return r == 0 + } + + private func attempt(_ name: String, file: String = #file, line: UInt = #line, valid: (CInt) -> Bool, _ b: @autoclosure () -> CInt) throws -> CInt { + let r = b() + guard valid(r) else { throw ServerError(operation: name, errno: r, file: file, line: line) } + return r + } + + init(port: UInt16) throws { + #if os(Linux) + let SOCKSTREAM = Int32(SOCK_STREAM.rawValue) + #else + let SOCKSTREAM = SOCK_STREAM + #endif + listenSocket = try attempt("socket", valid: isNotNegative, socket(AF_INET, SOCKSTREAM, Int32(IPPROTO_TCP))) + var on: Int = 1 + _ = try attempt("setsockopt", valid: isZero, setsockopt(listenSocket, SOL_SOCKET, SO_REUSEADDR, &on, socklen_t(MemoryLayout.size))) + let sa = createSockaddr(port) + socketAddress.initialize(to: sa) + try socketAddress.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout.size, { + let addr = UnsafePointer($0) + _ = try attempt("bind", valid: isZero, bind(listenSocket, addr, socklen_t(MemoryLayout.size))) + }) + } + + private func createSockaddr(_ port: UInt16) -> sockaddr_in { + #if os(Linux) + 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)) + #else + 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) ) + #endif + } + func acceptConnection(notify: ServerSemaphore) throws { + _ = try attempt("listen", valid: isZero, listen(listenSocket, SOMAXCONN)) + try socketAddress.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout.size, { + let addr = UnsafeMutablePointer($0) + var sockLen = socklen_t(MemoryLayout.size) + notify.signal() + connectionSocket = try attempt("accept", valid: isNotNegative, accept(listenSocket, addr, &sockLen)) + }) + } + + func readData() throws -> String { + var buffer = [UInt8](repeating: 0, count: 4096) + _ = try attempt("read", valid: isNotNegative, CInt(read(connectionSocket, &buffer, 4096))) + return String(cString: &buffer) + } + + func writeData(data: String) throws { + var bytes = Array(data.utf8) + _ = try attempt("write", valid: isNotNegative, CInt(write(connectionSocket, &bytes, data.utf8.count))) + } + + func shutdown() { + close(connectionSocket) + close(listenSocket) + } +} + +class _HTTPServer { + + let socket: _TCPSocket + + init(port: UInt16) throws { + socket = try _TCPSocket(port: port) + } + + public class func create(port: UInt16) throws -> _HTTPServer { + return try _HTTPServer(port: port) + } + + public func listen(notify: ServerSemaphore) throws { + try socket.acceptConnection(notify: notify) + } + + public func stop() { + socket.shutdown() + } + + public func request() throws -> _HTTPRequest { + return _HTTPRequest(request: try socket.readData()) + } + + public func respond(with response: _HTTPResponse) throws { + try socket.writeData(data: response.description) + } +} + +struct _HTTPRequest { + enum Method : String { + case GET + case POST + case PUT + } + let method: Method + let uri: String + let body: String + let headers: [String] + + public init(request: String) { + let lines = request.components(separatedBy: _HTTPUtils.CRLF2)[0].components(separatedBy: _HTTPUtils.CRLF) + headers = Array(lines[0...lines.count-2]) + method = Method(rawValue: headers[0].components(separatedBy: " ")[0])! + uri = headers[0].components(separatedBy: " ")[1] + body = lines.last! + } + +} + +struct _HTTPResponse { + enum Response : Int { + case OK = 200 + } + private let responseCode: Response + private let headers: String + private let body: String + + public init(response: Response, headers: String = _HTTPUtils.EMPTY, body: String) { + self.responseCode = response + self.headers = headers + self.body = body + } + + public var description: String { + let statusLine = _HTTPUtils.VERSION + _HTTPUtils.SPACE + "\(responseCode.rawValue)" + _HTTPUtils.SPACE + "\(responseCode)" + return statusLine + (headers != _HTTPUtils.EMPTY ? _HTTPUtils.CRLF + headers : _HTTPUtils.EMPTY) + _HTTPUtils.CRLF2 + body + } +} + +public class TestURLSessionServer { + let capitals: [String:String] = ["Nepal":"Kathmandu", + "Peru":"Lima", + "Italy":"Rome", + "USA":"Washington, D.C.", + "country.txt": "A country is a region that is identified as a distinct national entity in political geography"] + let httpServer: _HTTPServer + + public init (port: UInt16) throws { + httpServer = try _HTTPServer.create(port: port) + } + public func start(started: ServerSemaphore) throws { + started.signal() + try httpServer.listen(notify: started) + } + + public func readAndRespond() throws { + try httpServer.respond(with: process(request: httpServer.request())) + } + + func process(request: _HTTPRequest) -> _HTTPResponse { + if request.method == .GET { + return getResponse(uri: request.uri) + } else { + fatalError("Unsupported method!") + } + } + + func getResponse(uri: String) -> _HTTPResponse { + if uri == "/country.txt" { + let text = capitals[String(uri.characters.dropFirst())]! + return _HTTPResponse(response: .OK, headers: "Content-Length: \(text.characters.count)", body: text) + } + return _HTTPResponse(response: .OK, body: capitals[String(uri.characters.dropFirst())]!) + } + + func stop() { + httpServer.stop() + } +} + +struct ServerError : Error { + let operation: String + let errno: CInt + let file: String + let line: UInt + var _code: Int { return Int(errno) } + var _domain: String { return NSPOSIXErrorDomain } +} + + +extension ServerError : CustomStringConvertible { + var description: String { + let s = String(validatingUTF8: strerror(errno)) ?? "" + return "\(operation) failed: \(s) (\(_code))" + } +} + +public class ServerSemaphore { + let dispatchSemaphore = DispatchSemaphore(value: 0) + + public func wait() { + dispatchSemaphore.wait() + } + + public func signal() { + dispatchSemaphore.signal() + } +} diff --git a/TestFoundation/TestNSURLSession.swift b/TestFoundation/TestNSURLSession.swift index a27f50d942..487389900e 100644 --- a/TestFoundation/TestNSURLSession.swift +++ b/TestFoundation/TestNSURLSession.swift @@ -7,33 +7,60 @@ // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // - #if DEPLOYMENT_RUNTIME_OBJC || os(Linux) -import Foundation -import XCTest + import Foundation + import XCTest #else -import SwiftFoundation -import SwiftXCTest + import SwiftFoundation + import SwiftXCTest #endif class TestURLSession : XCTestCase { + var serverPort: Int = -1 + static var allTests: [(String, (TestURLSession) -> () throws -> Void)] { return [ ("test_dataTaskWithURL", test_dataTaskWithURL), ("test_dataTaskWithURLRequest", test_dataTaskWithURLRequest), ("test_dataTaskWithURLCompletionHandler", test_dataTaskWithURLCompletionHandler), ("test_dataTaskWithURLRequestCompletionHandler", test_dataTaskWithURLRequestCompletionHandler), -// ("test_downloadTaskWithURL", test_downloadTaskWithURL), -// ("test_downloadTaskWithURLRequest", test_downloadTaskWithURLRequest), -// ("test_downloadTaskWithRequestAndHandler", test_downloadTaskWithRequestAndHandler), -// ("test_downloadTaskWithURLAndHandler", test_downloadTaskWithURLAndHandler), + ("test_downloadTaskWithURL", test_downloadTaskWithURL), + ("test_downloadTaskWithURLRequest", test_downloadTaskWithURLRequest), + ("test_downloadTaskWithRequestAndHandler", test_downloadTaskWithRequestAndHandler), + ("test_downloadTaskWithURLAndHandler", test_downloadTaskWithURLAndHandler), ] } + private func runServer(with condition: ServerSemaphore) throws { + let start = 21961 + for port in start...(start+100) { //we must find at least one port to bind + do { + serverPort = port + let test = try TestURLSessionServer(port: UInt16(port)) + try test.start(started: condition) + try test.readAndRespond() + test.stop() + } catch let e as ServerError { + if e.operation != "bind" { continue } + throw e + } + } + } + func test_dataTaskWithURL() { - let urlString = "https://restcountries.eu/rest/v1/name/Nepal?fullText=true" + let serverReady = ServerSemaphore() + globalDispatchQueue.async { + do { + try self.runServer(with: serverReady) + } catch { + XCTAssertTrue(true) + return + } + } + serverReady.wait() + let urlString = "http://127.0.0.1:\(serverPort)/Nepal" let url = URL(string: urlString)! let d = DataTask(with: expectation(description: "data task")) d.run(with: url) @@ -44,7 +71,17 @@ class TestURLSession : XCTestCase { } func test_dataTaskWithURLCompletionHandler() { - let urlString = "https://restcountries.eu/rest/v1/name/USA?fullText=true" + let serverReady = ServerSemaphore() + globalDispatchQueue.async { + do { + try self.runServer(with: serverReady) + } catch { + XCTAssertTrue(true) + return + } + } + serverReady.wait() + let urlString = "http://127.0.0.1:\(serverPort)/USA" let url = URL(string: urlString)! let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 8 @@ -60,13 +97,7 @@ class TestURLSession : XCTestCase { let httpResponse = response as! HTTPURLResponse? XCTAssertEqual(200, httpResponse!.statusCode, "HTTP response code is not 200") - do { - let json = try JSONSerialization.jsonObject(with: data!, options: []) - let arr = json as? Array - let first = arr![0] - let result = first as? [String : Any] - expectedResult = result!["capital"] as! String - } catch { } + expectedResult = String(data: data!, encoding: String.Encoding.utf8)! XCTAssertEqual("Washington, D.C.", expectedResult, "Did not receive expected value") expect.fulfill() } @@ -75,7 +106,17 @@ class TestURLSession : XCTestCase { } func test_dataTaskWithURLRequest() { - let urlString = "https://restcountries.eu/rest/v1/name/Peru?fullText=true" + let serverReady = ServerSemaphore() + globalDispatchQueue.async { + do { + try self.runServer(with: serverReady) + } catch { + XCTAssertTrue(true) + return + } + } + serverReady.wait() + let urlString = "http://127.0.0.1:\(serverPort)/Peru" let urlRequest = URLRequest(url: URL(string: urlString)!) let d = DataTask(with: expectation(description: "data task")) d.run(with: urlRequest) @@ -86,7 +127,17 @@ class TestURLSession : XCTestCase { } func test_dataTaskWithURLRequestCompletionHandler() { - let urlString = "https://restcountries.eu/rest/v1/name/Italy?fullText=true" + let serverReady = ServerSemaphore() + globalDispatchQueue.async { + do { + try self.runServer(with: serverReady) + } catch { + XCTAssertTrue(true) + return + } + } + serverReady.wait() + let urlString = "http://127.0.0.1:\(serverPort)/Italy" let urlRequest = URLRequest(url: URL(string: urlString)!) let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 8 @@ -101,13 +152,7 @@ class TestURLSession : XCTestCase { } let httpResponse = response as! HTTPURLResponse? XCTAssertEqual(200, httpResponse!.statusCode, "HTTP response code is not 200") - do { - let json = try JSONSerialization.jsonObject(with: data!, options: []) - let arr = json as? Array - let first = arr![0] - let result = first as? [String : Any] - expectedResult = result!["capital"] as! String - } catch { } + expectedResult = String(data: data!, encoding: String.Encoding.utf8)! XCTAssertEqual("Rome", expectedResult, "Did not receive expected value") expect.fulfill() } @@ -116,7 +161,17 @@ class TestURLSession : XCTestCase { } func test_downloadTaskWithURL() { - let urlString = "https://swift.org/LICENSE.txt" + let serverReady = ServerSemaphore() + globalDispatchQueue.async { + do { + try self.runServer(with: serverReady) + } catch { + XCTAssertTrue(true) + return + } + } + serverReady.wait() + let urlString = "http://127.0.0.1:\(serverPort)/country.txt" let url = URL(string: urlString)! let d = DownloadTask(with: expectation(description: "download task with delegate")) d.run(with: url) @@ -124,19 +179,39 @@ class TestURLSession : XCTestCase { } func test_downloadTaskWithURLRequest() { - let urlString = "https://swift.org/LICENSE.txt" - let urlRequest = URLRequest(url: URL(string: urlString)!) - let d = DownloadTask(with: expectation(description: "download task with delegate")) - d.run(with: urlRequest) - waitForExpectations(timeout: 12) + let serverReady = ServerSemaphore() + globalDispatchQueue.async { + do { + try self.runServer(with: serverReady) + } catch { + XCTAssertTrue(true) + return + } + } + serverReady.wait() + let urlString = "http://127.0.0.1:\(serverPort)/country.txt" + let urlRequest = URLRequest(url: URL(string: urlString)!) + let d = DownloadTask(with: expectation(description: "download task with delegate")) + d.run(with: urlRequest) + waitForExpectations(timeout: 12) } func test_downloadTaskWithRequestAndHandler() { + let serverReady = ServerSemaphore() + globalDispatchQueue.async { + do { + try self.runServer(with: serverReady) + } catch { + XCTAssertTrue(true) + return + } + } + serverReady.wait() let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 8 let session = URLSession(configuration: config, delegate: nil, delegateQueue: nil) let expect = expectation(description: "download task with handler") - let req = URLRequest(url: URL(string: "https://swift.org/LICENSE.txt")!) + let req = URLRequest(url: URL(string: "http://127.0.0.1:\(serverPort)/country.txt")!) let task = session.downloadTask(with: req) { (_, _, error) -> Void in if let e = error { XCTAssertEqual(e.code, NSURLErrorTimedOut, "Unexpected error code") @@ -148,11 +223,21 @@ class TestURLSession : XCTestCase { } func test_downloadTaskWithURLAndHandler() { + let serverReady = ServerSemaphore() + globalDispatchQueue.async { + do { + try self.runServer(with: serverReady) + } catch { + XCTAssertTrue(true) + return + } + } + serverReady.wait() let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 8 let session = URLSession(configuration: config, delegate: nil, delegateQueue: nil) let expect = expectation(description: "download task with handler") - let req = URLRequest(url: URL(string: "https://swift.org/LICENSE.txt")!) + let req = URLRequest(url: URL(string: "http://127.0.0.1:\(serverPort)/country.txt")!) let task = session.downloadTask(with: req) { (_, _, error) -> Void in if let e = error { XCTAssertEqual(e.code, NSURLErrorTimedOut, "Unexpected error code") @@ -164,7 +249,7 @@ class TestURLSession : XCTestCase { } } -class DataTask: NSObject { +class DataTask : NSObject { let dataTaskExpectation: XCTestExpectation! var capital = "unknown" var session: URLSession! = nil @@ -194,14 +279,7 @@ class DataTask: NSObject { extension DataTask : URLSessionDataDelegate { public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - do { - let json = try JSONSerialization.jsonObject(with: data, options: []) - let arr = json as? Array - let first = arr![0] - let result = first as? [String : Any] - capital = result!["capital"] as! String - } catch { } - + capital = String(data: data, encoding: String.Encoding.utf8)! dataTaskExpectation.fulfill() } } @@ -216,19 +294,19 @@ extension DataTask : URLSessionTaskDelegate { } class DownloadTask : NSObject { - var totalBytesWritten: Int64 = 0 + var totalBytesWritten: Int64 = 0 let dwdExpectation: XCTestExpectation! - var session: URLSession! = nil - var task: URLSessionDownloadTask! = nil - + var session: URLSession! = nil + var task: URLSessionDownloadTask! = nil + init(with expectation: XCTestExpectation) { - dwdExpectation = expectation + dwdExpectation = expectation } - + func run(with url: URL) { let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 8 - session = URLSession(configuration: config, delegate: self, delegateQueue: nil) + session = URLSession(configuration: config, delegate: self, delegateQueue: nil) task = session.downloadTask(with: url) task.resume() } diff --git a/build.py b/build.py index 0f22a9898a..130ade88d8 100644 --- a/build.py +++ b/build.py @@ -467,8 +467,11 @@ # TODO: Probably this should be another 'product', but for now it's simply a phase foundation_tests = SwiftExecutable('TestFoundation', [ 'TestFoundation/main.swift', + 'TestFoundation/HTTPServer.swift', ] + glob.glob('./TestFoundation/Test*.swift')) # all TestSomething.swift are considered sources to the test project in the TestFoundation directory +Configuration.current.extra_ld_flags = '-L'+Configuration.current.variables["LIBDISPATCH_BUILD_DIR"]+'/src/.libs' + foundation_tests.add_dependency(foundation_tests_resources) foundation.add_phase(foundation_tests_resources) foundation.add_phase(foundation_tests)