diff --git a/Sources/LCLPing/HTTP/HTTPChannelHandlers.swift b/Sources/LCLPing/HTTP/HTTPChannelHandlers.swift index 4ed24a1..cb1a517 100644 --- a/Sources/LCLPing/HTTP/HTTPChannelHandlers.swift +++ b/Sources/LCLPing/HTTP/HTTPChannelHandlers.swift @@ -78,12 +78,12 @@ internal final class HTTPDuplexer: ChannelDuplexHandler { } func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { + let sequenceNumber = self.unwrapOutboundIn(data) guard self.state.isOperational else { - context.fireChannelRead(self.wrapInboundOut(.error(ChannelError.ioOnClosedChannel))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNumber, ChannelError.ioOnClosedChannel))) return } - let sequenceNumber = self.unwrapOutboundIn(data) var header = HTTPHeaders(self.httpOptions.httpHeaders.map { ($0.key, $0.value) }) if !self.httpOptions.httpHeaders.keys.contains("Host"), let host = self.url.host { header.add(name: "Host", value: host) @@ -115,17 +115,17 @@ internal final class HTTPDuplexer: ChannelDuplexHandler { self.state = .error fatalError("HTTP Handler in some error state while the status code is \(statusCode). Please report this to the developer") case 300...399: - context.fireChannelRead(self.wrapInboundOut(.error(PingError.httpRedirect))) + context.fireChannelRead(self.wrapInboundOut(.error(latencyEntry.seqNum, PingError.httpRedirect))) case 400...499: - context.fireChannelRead(self.wrapInboundOut(.error(PingError.httpClientError))) + context.fireChannelRead(self.wrapInboundOut(.error(latencyEntry.seqNum, PingError.httpClientError))) case 500...599: - context.fireChannelRead(self.wrapInboundOut(.error(PingError.httpServerError))) + context.fireChannelRead(self.wrapInboundOut(.error(latencyEntry.seqNum, PingError.httpServerError))) default: - context.fireChannelRead(self.wrapInboundOut(.error(PingError.httpUnknownStatus(statusCode)))) + context.fireChannelRead(self.wrapInboundOut(.error(latencyEntry.seqNum, PingError.httpUnknownStatus(statusCode)))) } case .waiting: self.state = .error - context.fireChannelRead(self.wrapInboundOut(.error(PingError.invalidLatencyResponseState))) + context.fireChannelRead(self.wrapInboundOut(.error(latencyEntry.seqNum, PingError.invalidLatencyResponseState))) } context.channel.close(mode: .all, promise: nil) @@ -138,7 +138,7 @@ internal final class HTTPDuplexer: ChannelDuplexHandler { return } self.state = .error - let pingResponse: PingResponse = .error(error) + let pingResponse: PingResponse = .error(nil, error) context.fireChannelRead(self.wrapInboundOut(pingResponse)) context.channel.close(mode: .all, promise: nil) } @@ -466,13 +466,13 @@ extension HTTPHandler: URLSessionTaskDelegate, URLSessionDataDelegate { self.continuation?.yield(.timeout(taskToSeqNum[taskIdentifier]!)) default: logger.debug("task \(taskIdentifier) has error: \(urlError.localizedDescription)") - self.continuation?.yield(.error(urlError)) + self.continuation?.yield(.error(UInt16(taskIdentifier), urlError)) } } else { // no error, let's check the data received guard let response = task.response else { - self.continuation?.yield(.error(PingError.httpNoResponse)) + self.continuation?.yield(.error(UInt16(taskIdentifier), PingError.httpNoResponse)) logger.debug("request #\(taskIdentifier) doesnt have response") return } @@ -488,7 +488,7 @@ extension HTTPHandler: URLSessionTaskDelegate, URLSessionDataDelegate { self.continuation?.yield(.ok(seqNum, latency, Date.currentTimestamp)) } default: - self.continuation?.yield(.error(PingError.httpRequestFailed(statusCode))) + self.continuation?.yield(.error(UInt16(taskIdentifier), PingError.httpRequestFailed(statusCode))) } // completes the async stream diff --git a/Sources/LCLPing/HTTP/HTTPPing.swift b/Sources/LCLPing/HTTP/HTTPPing.swift index b42bf8f..361331e 100644 --- a/Sources/LCLPing/HTTP/HTTPPing.swift +++ b/Sources/LCLPing/HTTP/HTTPPing.swift @@ -39,6 +39,14 @@ internal struct HTTPPing: Pingable { private var pingSummary: PingSummary? private let httpOptions: LCLPing.PingConfiguration.HTTPOptions +#if INTEGRATION_TEST + private var networkLinkConfig: TrafficControllerChannelHandler.NetworkLinkConfiguration? + internal init(httpOptions: LCLPing.PingConfiguration.HTTPOptions, networkLinkConfig: TrafficControllerChannelHandler.NetworkLinkConfiguration) { + self.httpOptions = httpOptions + self.networkLinkConfig = networkLinkConfig + } +#endif + internal init(httpOptions: LCLPing.PingConfiguration.HTTPOptions) { self.httpOptions = httpOptions } @@ -74,6 +82,9 @@ internal struct HTTPPing: Pingable { } let httpOptions = self.httpOptions +#if INTEGRATION_TEST + let networkLinkConfig = self.networkLinkConfig +#endif let enableTLS = schema == "https" port = enableTLS && port == 80 ? 443 : port let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) @@ -82,6 +93,7 @@ internal struct HTTPPing: Pingable { } let resolvedAddress = try SocketAddress.makeAddressResolvingHost(host, port: Int(port)) + logger.debug("resolved address is \(resolvedAddress)") pingStatus = .running do { @@ -94,34 +106,46 @@ internal struct HTTPPing: Pingable { group.cancelAll() return pingResponses } - + + logger.debug("added task #\(cnt)") group.addTask { - let asyncChannel = try await ClientBootstrap(group: eventLoopGroup).connect(to: resolvedAddress) { channel in - channel.eventLoop.makeCompletedFuture { - if enableTLS { - do { - let tlsConfiguration = TLSConfiguration.makeClientConfiguration() - let sslContext = try NIOSSLContext(configuration: tlsConfiguration) - let tlsHandler = try NIOSSLClientHandler(context: sslContext, serverHostname: host) - try channel.pipeline.syncOperations.addHandlers(tlsHandler) - } catch { - throw PingError.httpUnableToEstablishTLSConnection - } + var asyncChannel: NIOAsyncChannel + let channel = try await ClientBootstrap(group: eventLoopGroup).connect(to: resolvedAddress).get() + logger.debug("in event loop: \(channel.eventLoop.inEventLoop)") + asyncChannel = try await channel.eventLoop.submit { + if enableTLS { + do { + let tlsConfiguration = TLSConfiguration.makeClientConfiguration() + let sslContext = try NIOSSLContext(configuration: tlsConfiguration) + let tlsHandler = try NIOSSLClientHandler(context: sslContext, serverHostname: host) + try channel.pipeline.syncOperations.addHandlers(tlsHandler) + } catch { + throw PingError.httpUnableToEstablishTLSConnection } - - try channel.pipeline.syncOperations.addHTTPClientHandlers(position: .last) - try channel.pipeline.syncOperations.addHandlers([HTTPTracingHandler(configuration: pingConfiguration, httpOptions: httpOptions), HTTPDuplexer(url: url, httpOptions: httpOptions, configuration: pingConfiguration)], position: .last) - - return try NIOAsyncChannel(wrappingChannelSynchronously: channel) } - } + +#if INTEGRATION_TEST + try channel.pipeline.syncOperations.addHandler(TrafficControllerChannelHandler(networkLinkConfig: networkLinkConfig!)) +#endif + + try channel.pipeline.syncOperations.addHTTPClientHandlers(position: .last) + try channel.pipeline.syncOperations.addHandlers([HTTPTracingHandler(configuration: pingConfiguration, httpOptions: httpOptions), HTTPDuplexer(url: url, httpOptions: httpOptions, configuration: pingConfiguration)], position: .last) + + return try NIOAsyncChannel(wrappingChannelSynchronously: channel) + }.get() + asyncChannel.channel.pipeline.fireChannelActive() + + logger.debug("pipeline is: \(asyncChannel.channel.pipeline.debugDescription)") - // Task.sleep respects cooperative cancellation. That is, it will throw a cancellation error and finish early if its current task is cancelled. + // NOTE: Task.sleep respects cooperative cancellation. That is, it will throw a cancellation error and finish early if its current task is cancelled. try await Task.sleep(nanoseconds: UInt64(cnt) * pingConfiguration.interval.nanosecond) -// logger.trace("write packet #\(cnt)") + logger.debug("write packet #\(cnt)") let result = try await asyncChannel.executeThenClose { inbound, outbound in try await outbound.write(cnt) - + defer { + outbound.finish() + } + var asyncItr = inbound.makeAsyncIterator() guard let next = try await asyncItr.next() else { throw PingError.httpMissingResult @@ -132,14 +156,16 @@ internal struct HTTPPing: Pingable { return result } } - + logger.debug("now waiting to receive data from the channel") do { while pingStatus != .stopped, let next = try await group.next() { + logger.debug("received \(next)") pingResponses.append(next) } } catch is CancellationError { - logger.info("Task is cancelled while waiting") + logger.debug("Task is cancelled while waiting") } catch { + print("received error: \(error)") throw error } diff --git a/Sources/LCLPing/ICMP/ICMPChannelHandlers.swift b/Sources/LCLPing/ICMP/ICMPChannelHandlers.swift index 8eb84dc..3ba62b1 100644 --- a/Sources/LCLPing/ICMP/ICMPChannelHandlers.swift +++ b/Sources/LCLPing/ICMP/ICMPChannelHandlers.swift @@ -61,13 +61,13 @@ internal final class ICMPDuplexer: ChannelDuplexHandler { } func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { + let (identifier, sequenceNum) = self.unwrapOutboundIn(data) guard self.state.isOperational else { logger.error("[\(#function)]: Error: IO on closed channel") - context.fireChannelRead(self.wrapInboundOut(.error(ChannelError.ioOnClosedChannel))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum, ChannelError.ioOnClosedChannel))) return } - let (identifier, sequenceNum) = self.unwrapOutboundIn(data) var icmpRequest = ICMPHeader(idenifier: identifier, sequenceNum: sequenceNum) icmpRequest.setChecksum() @@ -105,100 +105,98 @@ internal final class ICMPDuplexer: ChannelDuplexHandler { let type = icmpResponse.type let code = icmpResponse.code - logger.debug("[\(#function)]: received icmp response with type: \(type), code: \(code)") + let sequenceNum = icmpResponse.sequenceNum + let identifier = icmpResponse.idenifier + logger.debug("[\(#function)]: received icmp response with type: \(type), code: \(code), sequence number: \(sequenceNum), identifier: \(identifier)") switch (type, code) { case (ICMPType.EchoReply.rawValue, 0): break case (3,0): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpDestinationNetworkUnreachable))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum, PingError.icmpDestinationNetworkUnreachable))) return case (3,1): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpDestinationHostUnreachable))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpDestinationHostUnreachable))) return case (3,2): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpDestinationProtocoltUnreachable))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpDestinationProtocoltUnreachable))) return case (3,3): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpDestinationPortUnreachable))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpDestinationPortUnreachable))) return case (3,4): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpFragmentationRequired))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpFragmentationRequired))) return case (3,5): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpSourceRouteFailed))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpSourceRouteFailed))) return case (3,6): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpUnknownDestinationNetwork))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpUnknownDestinationNetwork))) return case (3,7): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpUnknownDestinationHost))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpUnknownDestinationHost))) return case (3,8): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpSourceHostIsolated))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpSourceHostIsolated))) return case (3,9): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpNetworkAdministrativelyProhibited))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpNetworkAdministrativelyProhibited))) return case (3,10): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpHostAdministrativelyProhibited))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpHostAdministrativelyProhibited))) return case (3,11): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpNetworkUnreachableForToS))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpNetworkUnreachableForToS))) return case (3,12): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpHostUnreachableForToS))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpHostUnreachableForToS))) return case (3,13): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpCommunicationAdministrativelyProhibited))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpCommunicationAdministrativelyProhibited))) return case (3,14): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpHostPrecedenceViolation))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpHostPrecedenceViolation))) return case (3,15): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpPrecedenceCutoffInEffect))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpPrecedenceCutoffInEffect))) return case (5,0): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpRedirectDatagramForNetwork))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpRedirectDatagramForNetwork))) return case (5,1): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpRedirectDatagramForHost))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpRedirectDatagramForHost))) return case (5,2): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpRedirectDatagramForTosAndNetwork))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpRedirectDatagramForTosAndNetwork))) return case (5,3): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpRedirectDatagramForTosAndHost))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpRedirectDatagramForTosAndHost))) return case (9,0): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpRouterAdvertisement))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpRouterAdvertisement))) return case (10,0): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpRouterDiscoverySelectionSolicitation))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpRouterDiscoverySelectionSolicitation))) return case (11,0): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpTTLExpiredInTransit))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpTTLExpiredInTransit))) return case (11,1): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpFragmentReassemblyTimeExceeded))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpFragmentReassemblyTimeExceeded))) return case (12,0): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpPointerIndicatesError))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpPointerIndicatesError))) return case (12,1): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpMissingARequiredOption))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpMissingARequiredOption))) return case (12,2): - context.fireChannelRead(self.wrapInboundOut(.error(PingError.icmpBadLength))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.icmpBadLength))) return default: - context.fireChannelRead(self.wrapInboundOut(.error(PingError.unknownError("Received unknown ICMP type (\(type)) and ICMP code (\(code))")))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum,PingError.unknownError("Received unknown ICMP type (\(type)) and ICMP code (\(code))")))) return } - let sequenceNum = icmpResponse.sequenceNum - let identifier = icmpResponse.idenifier - logger.debug("[\(#function)]: received icmp response with sequence number: \(sequenceNum), identifier: \(identifier)") - self.timerScheduler.remove(key: sequenceNum) if self.responseSeqNumSet.contains(sequenceNum) { @@ -213,19 +211,19 @@ internal final class ICMPDuplexer: ChannelDuplexHandler { self.responseSeqNumSet.insert(sequenceNum) guard let icmpRequest = self.seqToRequest[sequenceNum] else { logger.error("[\(#function)]: Unable to find matching request with sequence number \(sequenceNum)") - context.fireChannelRead(self.wrapInboundOut(.error(PingError.invalidICMPResponse))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum, PingError.invalidICMPResponse))) closeWhenComplete(context: context) return } if icmpResponse.checkSum != icmpResponse.calcChecksum() { - context.fireChannelRead(self.wrapInboundOut(.error(PingError.invalidICMPChecksum))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum, PingError.invalidICMPChecksum))) closeWhenComplete(context: context) return } if identifier != icmpRequest.idenifier { - context.fireChannelRead(self.wrapInboundOut(.error(PingError.invalidICMPIdentifier))) + context.fireChannelRead(self.wrapInboundOut(.error(sequenceNum, PingError.invalidICMPIdentifier))) closeWhenComplete(context: context) return } @@ -277,7 +275,7 @@ internal final class ICMPDuplexer: ChannelDuplexHandler { return } self.state = .error - let pingResponse: PingResponse = .error(error) + let pingResponse: PingResponse = .error(nil, error) context.fireChannelRead(self.wrapInboundOut(pingResponse)) context.close(mode: .all, promise: nil) } diff --git a/Sources/LCLPing/ICMP/ICMPPing.swift b/Sources/LCLPing/ICMP/ICMPPing.swift index b5dfdf7..2571ae0 100644 --- a/Sources/LCLPing/ICMP/ICMPPing.swift +++ b/Sources/LCLPing/ICMP/ICMPPing.swift @@ -41,9 +41,11 @@ internal struct ICMPPing: Pingable { #if INTEGRATION_TEST private var networkLinkConfig: TrafficControllerChannelHandler.NetworkLinkConfiguration? + private var rewriteHeaders: [PartialKeyPath>:AnyObject]? - internal init(networkLinkConfig: TrafficControllerChannelHandler.NetworkLinkConfiguration) { + internal init(networkLinkConfig: TrafficControllerChannelHandler.NetworkLinkConfiguration, rewriteHeaders: [PartialKeyPath>:AnyObject]?) { self.networkLinkConfig = networkLinkConfig + self.rewriteHeaders = rewriteHeaders } #endif @@ -70,6 +72,7 @@ internal struct ICMPPing: Pingable { let resolvedAddress = try SocketAddress.makeAddressResolvingHost(host, port: 0) #if INTEGRATION_TEST let networkLinkConfig = self.networkLinkConfig! + let rewriteHeaders = rewriteHeaders #endif @@ -79,15 +82,22 @@ internal struct ICMPPing: Pingable { .connect(to: resolvedAddress) { channel in channel.eventLoop.makeCompletedFuture { #if INTEGRATION_TEST - try channel.pipeline.syncOperations.addHandler(TrafficControllerChannelHandler(networkLinkConfig: networkLinkConfig)) -#endif - try channel.pipeline.syncOperations.addHandlers( - [ - IPDecoder(), - ICMPDecoder(), - ICMPDuplexer(configuration: pingConfiguration) - ] - ) + let handlers: [ChannelHandler] = [ + TrafficControllerChannelHandler(networkLinkConfig: networkLinkConfig), + InboundHeaderRewriter(rewriteHeaders: rewriteHeaders), + IPDecoder(), + ICMPDecoder(), + ICMPDuplexer(configuration: pingConfiguration) + ] +#else // !INTEGRATION_TEST + let handlers: [ChannelHandler] = [ + IPDecoder(), + ICMPDecoder(), + ICMPDuplexer(configuration: pingConfiguration) + ] +#endif // !INTEGRATION_TEST + + try channel.pipeline.syncOperations.addHandlers(handlers) return try NIOAsyncChannel(wrappingChannelSynchronously: channel) } } diff --git a/Sources/LCLPing/Models/Errors+LCLPing.swift b/Sources/LCLPing/Models/Errors+LCLPing.swift index 1669847..cc60715 100644 --- a/Sources/LCLPing/Models/Errors+LCLPing.swift +++ b/Sources/LCLPing/Models/Errors+LCLPing.swift @@ -71,6 +71,8 @@ public enum PingError: Error { case httpMissingSchema case httpUnableToEstablishTLSConnection case httpMissingResult + + case forTestingPurposeOnly } public enum RuntimeError: Error, Equatable { diff --git a/Sources/LCLPing/Models/PingSummary.swift b/Sources/LCLPing/Models/PingSummary.swift index 8c3674f..aa9ac98 100644 --- a/Sources/LCLPing/Models/PingSummary.swift +++ b/Sources/LCLPing/Models/PingSummary.swift @@ -24,11 +24,23 @@ public struct PingSummary: Equatable, Encodable { public let totalCount: Int public let timeout: Set public let duplicates: Set + public let errors: Set public let ipAddress: String public let port: Int public let `protocol`: CInt } extension PingSummary { - static let empty: PingSummary = .init(min: .zero, max: .zero, avg: .zero, median: .zero, stdDev: .zero, jitter: .zero, details: [], totalCount: .zero, timeout: .init(), duplicates: .init(), ipAddress: "", port: 0, protocol: 0) + public struct PingErrorSummary: Hashable, Encodable { + public static func == (lhs: PingSummary.PingErrorSummary, rhs: PingSummary.PingErrorSummary) -> Bool { + return lhs.seqNum == rhs.seqNum + } + + public let seqNum: UInt16 + public let reason: String + } +} + +extension PingSummary { + static let empty: PingSummary = .init(min: .zero, max: .zero, avg: .zero, median: .zero, stdDev: .zero, jitter: .zero, details: [], totalCount: .zero, timeout: .init(), duplicates: .init(), errors: .init(), ipAddress: "", port: 0, protocol: 0) } diff --git a/Sources/LCLPing/Models/Request.swift b/Sources/LCLPing/Models/Request.swift index f39e6da..cd63c0e 100644 --- a/Sources/LCLPing/Models/Request.swift +++ b/Sources/LCLPing/Models/Request.swift @@ -105,7 +105,3 @@ internal enum ICMPType: UInt8 { /// ICMP Reply from host case EchoReply = 0 } - -internal struct HTTPRequest { - -} diff --git a/Sources/LCLPing/Models/Response.swift b/Sources/LCLPing/Models/Response.swift index 5f88c93..e4fddc2 100644 --- a/Sources/LCLPing/Models/Response.swift +++ b/Sources/LCLPing/Models/Response.swift @@ -19,10 +19,10 @@ internal enum PingResponse: Equatable { return lSequenceNum == rSequenceNum && lLatency == rLatency && lTime == rTime case (PingResponse.duplicated(let lSequenceNum), PingResponse.duplicated(let rSequenceNum)): return lSequenceNum == rSequenceNum - case (PingResponse.timeout(let lSequenceNun), PingResponse.timeout(let rSequenceNum)): - return lSequenceNun == rSequenceNum - case (PingResponse.error(.some(let lError)), PingResponse.error(.some(let rError))): - return lError.localizedDescription == rError.localizedDescription + case (PingResponse.timeout(let lSequenceNum), PingResponse.timeout(let rSequenceNum)): + return lSequenceNum == rSequenceNum + case (PingResponse.error(.some(let lSequenceNum), .some(let lError)), PingResponse.error(.some(let rSequenceNum), .some(let rError))): + return lError.localizedDescription == rError.localizedDescription && lSequenceNum == rSequenceNum default: return false } @@ -31,5 +31,5 @@ internal enum PingResponse: Equatable { case ok(UInt16, Double, TimeInterval) case duplicated(UInt16) case timeout(UInt16) - case error(Error?) + case error(UInt16?, Error?) } diff --git a/Sources/LCLPing/TestUtils/InboundHeaderRewriter.swift b/Sources/LCLPing/TestUtils/InboundHeaderRewriter.swift new file mode 100644 index 0000000..f5bc3c5 --- /dev/null +++ b/Sources/LCLPing/TestUtils/InboundHeaderRewriter.swift @@ -0,0 +1,88 @@ +// +// This source file is part of the LCLPing open source project +// +// Copyright (c) 2021-2023 Local Connectivity Lab and the project authors +// Licensed under Apache License v2.0 +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of project authors +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import NIOCore +import NIO + +class InboundHeaderRewriter: ChannelInboundHandler { + typealias InboundIn = In + typealias InboundOut = In + + private enum State { + case operational + case error + case inactive + + var isOperational: Bool { + switch self { + case .operational: + return true + case .error, .inactive: + return false + } + } + } + + private var state: State + private var rewriteHeaders: [PartialKeyPath:AnyObject]? + + init(rewriteHeaders: [PartialKeyPath:AnyObject]?) { + self.rewriteHeaders = rewriteHeaders + self.state = .inactive + } + + func channelActive(context: ChannelHandlerContext) { + switch self.state { + case .operational: + break + case .error: + logger.error("[\(#function)]: in an incorrect state: \(self.state)") + assertionFailure("[\(#function)]: in an incorrect state: \(self.state)") + case .inactive: + logger.debug("[\(#function)]: Channel active") + context.fireChannelActive() + self.state = .operational + } + } + + func channelInactive(context: ChannelHandlerContext) { + switch self.state { + case .operational: + logger.debug("[\(#function)]: Channel inactive") + context.fireChannelInactive() + self.state = .inactive + case .error: + break + case .inactive: + logger.error("[\(#function)]: received inactive signal when channel is already in inactive state.") + assertionFailure("[\(#function)]: received inactive signal when channel is already in inactive state.") + } + } + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + guard self.state.isOperational else { + logger.debug("[\(#function)]: drop data: \(data) because channel is not in operational state") + return + } + + guard let rewriteHeaders = self.rewriteHeaders else { + context.fireChannelRead(data) + return + } + + let unwrapped = self.unwrapInboundIn(data) + let newValue = unwrapped.rewrite(newValues: rewriteHeaders) + context.fireChannelRead(self.wrapInboundOut(newValue)) + } + +} diff --git a/Sources/LCLPing/TestUtils/Rewritable.swift b/Sources/LCLPing/TestUtils/Rewritable.swift new file mode 100644 index 0000000..5685f17 --- /dev/null +++ b/Sources/LCLPing/TestUtils/Rewritable.swift @@ -0,0 +1,109 @@ +// +// This source file is part of the LCLPing open source project +// +// Copyright (c) 2021-2023 Local Connectivity Lab and the project authors +// Licensed under Apache License v2.0 +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of project authors +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import NIOCore + +protocol Rewritable { +// var allKeyPaths: [PartialKeyPath] { get } + func rewrite(newValues: [PartialKeyPath : AnyObject]) -> Self +} + +extension ICMPRequestPayload: Rewritable { +// var allKeyPaths: [PartialKeyPath] { +// return [\.identifier, \.timestamp] +// } + + func rewrite(newValues: [PartialKeyPath : AnyObject]) -> ICMPRequestPayload { + return ICMPRequestPayload(timestamp: newValues[\.timestamp] as? TimeInterval ?? self.timestamp, identifier: newValues[\.identifier] as? UInt16 ?? self.identifier) + } +} + +extension IPv4Header: Rewritable { +// var allKeyPaths: [PartialKeyPath] { +// return [ +// \.versionAndHeaderLength, +// \.differentiatedServicesAndECN, +// \.totalLength, +// \.identification, +// \.flagsAndFragmentOffset, +// \.timeToLive, +// \.protocol, +// \.headerChecksum, +// \.sourceAddress, +// \.destinationAddress +// ] +// } + + func rewrite(newValues: [PartialKeyPath : AnyObject]) -> IPv4Header { + return IPv4Header( + versionAndHeaderLength: newValues[\.versionAndHeaderLength] as? UInt8 ?? self.versionAndHeaderLength, + differentiatedServicesAndECN: newValues[\.differentiatedServicesAndECN] as? UInt8 ?? self.differentiatedServicesAndECN, + totalLength: newValues[\.totalLength] as? UInt16 ?? self.totalLength, + identification: newValues[\.identification] as? UInt16 ?? self.identification, + flagsAndFragmentOffset: newValues[\.flagsAndFragmentOffset] as? UInt16 ?? self.flagsAndFragmentOffset, + timeToLive: newValues[\.timeToLive] as? UInt8 ?? self.timeToLive, + protocol: newValues[\.protocol] as? UInt8 ?? self.protocol, + headerChecksum: newValues[\.headerChecksum] as? UInt16 ?? self.headerChecksum, + sourceAddress: newValues[\.sourceAddress] as? (UInt8, UInt8, UInt8, UInt8) ?? self.sourceAddress, + destinationAddress: newValues[\.destinationAddress] as? (UInt8, UInt8, UInt8, UInt8) ?? self.destinationAddress + ) + } +} + +extension ICMPHeader: Rewritable { +// var allKeyPaths: [PartialKeyPath] { +// return [ +// \.type, +// \.code, +// \.checkSum, +// \.idenifier, +// \.sequenceNum, +// \.payload +// ] +// } + + func rewrite(newValues: [PartialKeyPath : AnyObject]) -> ICMPHeader { + var newHeader = ICMPHeader( + type: newValues[\.type] as? UInt8 ?? self.type, + code: newValues[\.code] as? UInt8 ?? self.code, + idenifier: newValues[\.idenifier] as? UInt16 ?? self.idenifier, + sequenceNum: newValues[\.sequenceNum] as? UInt16 ?? self.sequenceNum + ) + + newHeader.payload = self.payload.rewrite(newValues: newValues[\.payload] as! [PartialKeyPath : AnyObject]) + return newHeader + } +} + +extension AddressedEnvelope: Rewritable where DataType: Rewritable { +// var allKeyPaths: [PartialKeyPath>] { +// return [ +// \.remoteAddress, +// \.data +// ] +// } + + func rewrite(newValues: [PartialKeyPath> : AnyObject]) -> NIOCore.AddressedEnvelope { + return AddressedEnvelope( + remoteAddress: newValues[\.remoteAddress] as? SocketAddress ?? self.remoteAddress, + data: data.rewrite(newValues: newValues[\.data] as! [PartialKeyPath : AnyObject]) + ) + } +} + +// TODO: implement Bytebuffer +extension ByteBuffer: Rewritable { + func rewrite(newValues: [PartialKeyPath : AnyObject]) -> NIOCore.ByteBuffer { + return ByteBuffer(buffer: self) + } +} diff --git a/Sources/LCLPing/Utilities/Utilities.swift b/Sources/LCLPing/Utilities/Utilities.swift index cc9aff9..f56719d 100644 --- a/Sources/LCLPing/Utilities/Utilities.swift +++ b/Sources/LCLPing/Utilities/Utilities.swift @@ -49,6 +49,7 @@ internal func summarizePingResponse(_ pingResponses: [PingResponse], host: Socke var localMax: Double = .zero var consecutiveDiffSum: Double = .zero var errorCount: Int = 0 + var errors: Set = Set() var pingResults: [PingResult] = [] var timeout: Set = Set() var duplicates: Set = Set() @@ -66,8 +67,11 @@ internal func summarizePingResponse(_ pingResponses: [PingResponse], host: Socke duplicates.insert(sequenceNum) case .timeout(let sequenceNum): timeout.insert(sequenceNum) - case .error: + case .error(let seqNum, let error): errorCount += 1 + if let seqNum = seqNum, let error = error { + errors.insert(PingSummary.PingErrorSummary(seqNum: seqNum, reason: error.localizedDescription)) + } } } @@ -85,6 +89,7 @@ internal func summarizePingResponse(_ pingResponses: [PingResponse], host: Socke totalCount: pingResultLen + errorCount + timeout.count, timeout: timeout, duplicates: duplicates, + errors: errors, ipAddress: host.ipAddress ?? "", port: host.port ?? 0, protocol: host.protocol.rawValue) diff --git a/Tests/HTTPChannelTests/HTTPDuplexerHandlerTests.swift b/Tests/HTTPChannelTests/HTTPDuplexerHandlerTests.swift index 6ba9462..808bc04 100644 --- a/Tests/HTTPChannelTests/HTTPDuplexerHandlerTests.swift +++ b/Tests/HTTPChannelTests/HTTPDuplexerHandlerTests.swift @@ -163,7 +163,8 @@ final class HTTPDuplexerHandlerTests: XCTestCase { let inboundRead = try channel.readInbound(as: PingResponse.self) XCTAssertNotNil(inboundRead) switch inboundRead! { - case .error(let error): + case .error(let seqNum, let error): + XCTAssertEqual(seqNum, 2) XCTAssertEqual(error?.localizedDescription, expectedError.localizedDescription) default: XCTFail("Should receive PingResponse.timeout") diff --git a/Tests/ICMPChannelTests/ICMPDuplexerTests.swift b/Tests/ICMPChannelTests/ICMPDuplexerTests.swift index a72872f..234d0f1 100644 --- a/Tests/ICMPChannelTests/ICMPDuplexerTests.swift +++ b/Tests/ICMPChannelTests/ICMPDuplexerTests.swift @@ -126,7 +126,8 @@ final class ICMPDuplexerTests: XCTestCase { self.loop.run() let inboundInResult = try channel.readInbound(as: PingResponse.self)! switch inboundInResult { - case .error(.some(let error)): + case .error(.some(let seqNum), .some(let error)): + XCTAssertEqual(seqNum, 2) XCTAssertEqual(error.localizedDescription, expectedError.localizedDescription) default: XCTFail("Should receive a PingResponse.error, but received \(inboundInResult)") @@ -147,7 +148,8 @@ final class ICMPDuplexerTests: XCTestCase { let inboundInResult = try channel.readInbound(as: PingResponse.self)! let expectedError = PingError.invalidICMPResponse switch inboundInResult { - case .error(.some(let error)): + case .error(.some(let seqNum), .some(let error)): + XCTAssertEqual(seqNum, 2) XCTAssertEqual(error.localizedDescription, expectedError.localizedDescription) default: XCTFail("Should receive a PingResponse.error, but received \(inboundInResult)") @@ -165,7 +167,8 @@ final class ICMPDuplexerTests: XCTestCase { let inboundInResult = try channel.readInbound(as: PingResponse.self)! let expectedError = PingError.invalidICMPChecksum switch inboundInResult { - case .error(.some(let error)): + case .error(.some(let seqNum), .some(let error)): + XCTAssertEqual(seqNum, 2) XCTAssertEqual(error.localizedDescription, expectedError.localizedDescription) default: XCTFail("Should receive a PingResponse.error, but received \(inboundInResult)") @@ -184,7 +187,8 @@ final class ICMPDuplexerTests: XCTestCase { let inboundInResult = try channel.readInbound(as: PingResponse.self)! let expectedError = PingError.invalidICMPIdentifier switch inboundInResult { - case .error(.some(let error)): + case .error(.some(let seqNum), .some(let error)): + XCTAssertEqual(seqNum, 2) XCTAssertEqual(error.localizedDescription, expectedError.localizedDescription) default: XCTFail("Should receive a PingResponse.error, but received \(inboundInResult)") diff --git a/Tests/IntegrationTests/HTTPIntegrationTests.swift b/Tests/IntegrationTests/HTTPIntegrationTests.swift new file mode 100644 index 0000000..1e05360 --- /dev/null +++ b/Tests/IntegrationTests/HTTPIntegrationTests.swift @@ -0,0 +1,251 @@ +// +// This source file is part of the LCLPing open source project +// +// Copyright (c) 2021-2023 Local Connectivity Lab and the project authors +// Licensed under Apache License v2.0 +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of project authors +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +import NIOCore +@testable import LCLPing + +#if INTEGRATION_TEST +final class HTTPIntegrationTests: XCTestCase { + + private func runTest( + networkLinkConfig: TrafficControllerChannelHandler.NetworkLinkConfiguration = .fullyConnected, + pingConfig: LCLPing.PingConfiguration = .init(type: .http(LCLPing.PingConfiguration.HTTPOptions()), endpoint: .ipv4("http://127.0.0.1", 8080)) + ) async throws -> (PingState, PingSummary?) { + switch pingConfig.type { + case .http(let httpOptions): + var httpPing = HTTPPing(httpOptions: httpOptions, networkLinkConfig: networkLinkConfig) + try await httpPing.start(with: pingConfig) + return (httpPing.pingStatus, httpPing.summary) + default: + XCTFail("Invalid PingConfig. Need HTTP, but received \(pingConfig.type)") + } + return (PingState.error, nil) + } + + + func testfullyConnectedNetwork() async throws { + let (pingStatus, pingSummary) = try await runTest() + switch pingStatus { + case .finished: + XCTAssertEqual(pingSummary?.totalCount, 10) + XCTAssertEqual(pingSummary?.details.isEmpty, false) + XCTAssertEqual(pingSummary?.duplicates.count, 0) + XCTAssertEqual(pingSummary?.timeout.count, 0) + default: + XCTFail("HTTP Test failed with status \(pingStatus)") + } + } + + func testFullyDisconnectedNetwork() async throws { + let fullyDisconnected = TrafficControllerChannelHandler.NetworkLinkConfiguration.fullyDisconnected + let (pingStatus, pingSummary) = try await runTest(networkLinkConfig: fullyDisconnected) + switch pingStatus { + case .finished: + for i in 0..<10 { + XCTAssertEqual(pingSummary?.timeout.contains(UInt16(i)), true) + } + XCTAssertEqual(pingSummary?.totalCount, 10) + XCTAssertEqual(pingSummary?.details.isEmpty, true) + XCTAssertEqual(pingSummary?.duplicates.count, 0) + XCTAssertEqual(pingSummary?.timeout.count, 10) + default: + XCTFail("HTTP Test failed with status \(pingStatus)") + } + } + + func testInvalidIPURL() async throws { + let expectedError = PingError.invalidIPv4URL + do { + let pingConfig = LCLPing.PingConfiguration(type: .http(.init()), endpoint: .ipv4("ww.invalid-url.^&*", 8080)) + let _ = try await runTest(pingConfig: pingConfig) + XCTFail("Expect throwing PingError.invalidIPv4URL") + } catch { + XCTAssertEqual(expectedError.localizedDescription, error.localizedDescription) + } + } + + func testMissingHostInURL() async throws { + let expectedError = PingError.httpMissingHost + do { + let pingConfig: LCLPing.PingConfiguration = .init(type: .http(LCLPing.PingConfiguration.HTTPOptions()), endpoint: .ipv4("127.0.0.1", 8080)) + let _ = try await runTest(pingConfig: pingConfig) + XCTFail("Expect throwing PingError.httpMissingHost") + } catch { + XCTAssertEqual(expectedError.localizedDescription, error.localizedDescription) + } + } + + func testMissingHTTPSchemaInURL() async throws { + let expectedError = PingError.httpMissingSchema + do { + let pingConfig = LCLPing.PingConfiguration(type: .http(LCLPing.PingConfiguration.HTTPOptions()), endpoint: .ipv4("someOtherSchema://127.0.0.1", 8080)) + let _ = try await runTest(pingConfig: pingConfig) + XCTFail("Expect throwing PingError.httpMissingSchema") + } catch { + XCTAssertEqual(expectedError.localizedDescription, error.localizedDescription) + } + } + + func testUnknownHost() async throws { + let expectedError = PingError.sendPingFailed(IOError(errnoCode: 61, reason: "connection reset (error set)")) + let pingConfig = LCLPing.PingConfiguration(type: .http(LCLPing.PingConfiguration.HTTPOptions()), endpoint: .ipv4("http://127.0.0.1", 9090)) + do { + let _ = try await runTest(pingConfig: pingConfig) + XCTFail("Expect throwing ") + } catch { + print("error: \(error)") + XCTAssertEqual(expectedError.localizedDescription, error.localizedDescription) + } + } + + func testMinorInOutPacketDrop() async throws { + let networkLink = TrafficControllerChannelHandler.NetworkLinkConfiguration(inPacketLoss: 0.1, outPacketLoss: 0.1) + let (pingStatus, _) = try await runTest(networkLinkConfig: networkLink) + switch pingStatus { + case .finished: + () + default: + XCTFail("HTTP Test failed with status \(pingStatus)") + } + } + + func testCorrectStatusCode() async throws { + for param in [(statusCode: 200, ok: true), (statusCode: 201, ok: true), (statusCode: 301, ok: false), (statusCode: 404, ok: false), (statusCode: 410, ok: false), (statusCode: 500, ok: false), (statusCode: 505, ok: false)] { + var httpOptions = LCLPing.PingConfiguration.HTTPOptions() + let desiredHeaders = [ + "Status-Code": String(param.statusCode) + ] + httpOptions.httpHeaders = desiredHeaders + let pingConfig: LCLPing.PingConfiguration = .init(type: .http(httpOptions), endpoint: .ipv4("http://127.0.0.1", 8080), count: 3) + let expectedSequenceNumbers: Set = [0, 1, 2] + let (pingStatus, pingSummary) = try await runTest(pingConfig: pingConfig) + switch pingStatus { + case .finished: + XCTAssertEqual(pingSummary?.totalCount, 3) + if param.ok { + XCTAssertEqual(pingSummary?.details.count, 3) + pingSummary?.details.forEach { element in + XCTAssertEqual(expectedSequenceNumbers.contains(element.seqNum), true) + } + } else { + XCTAssertEqual(pingSummary?.errors.count, 3) + switch param.statusCode { + case 300...399: + pingSummary?.errors.forEach { element in + XCTAssertEqual(expectedSequenceNumbers.contains(element.seqNum), true) + XCTAssertEqual(element.reason, PingError.httpRedirect.localizedDescription) + } + case 400...499: + pingSummary?.errors.forEach { element in + XCTAssertEqual(expectedSequenceNumbers.contains(element.seqNum), true) + XCTAssertEqual(element.reason, PingError.httpClientError.localizedDescription) + } + case 500...599: + pingSummary?.errors.forEach { element in + XCTAssertEqual(expectedSequenceNumbers.contains(element.seqNum), true) + XCTAssertEqual(element.reason, PingError.httpServerError.localizedDescription) + } + default: + XCTFail("HTTP Test failed with unknown status code \(param.statusCode)") + } + } + + default: + XCTFail("HTTP Test failed with status \(pingStatus)") + } + } + + } + + // FIXME: re-enable the following test after https://github.com/apple/swift-nio/issues/2612 is fixed + +// func testMediumInOutPacketDrop() async throws { +// XCTSkip("temporarily skip the test because Deinited NIOAsyncWriter without calling finish()") +// let networkLink = TrafficControllerChannelHandler.NetworkLinkConfiguration(inPacketLoss: 0.4, outPacketLoss: 0.4) +// let (pingStatus, _) = try await runTest(networkLinkConfig: networkLink) +// switch pingStatus { +// case .finished: +// () +// default: +// XCTFail("HTTP Test failed with status \(pingStatus)") +// } +// } +// +// func testMinorInPacketDrop() async throws { +// let networkLink = TrafficControllerChannelHandler.NetworkLinkConfiguration(inPacketLoss: 0.2) +// let (pingStatus, _) = try await runTest(networkLinkConfig: networkLink) +// switch pingStatus { +// case .finished: +// () +// default: +// XCTFail("HTTP Test failed with status \(pingStatus)") +// } +// } +// +// func testMinorOutPacketDrop() async throws { +// let networkLink = TrafficControllerChannelHandler.NetworkLinkConfiguration(outPacketLoss: 0.2) +// let (pingStatus, _) = try await runTest(networkLinkConfig: networkLink) +// switch pingStatus { +// case .finished: +// () +// default: +// XCTFail("HTTP Test failed with status \(pingStatus)") +// } +// } +// +// func testMediumInPacketDrop() async throws { +// let networkLink = TrafficControllerChannelHandler.NetworkLinkConfiguration(inPacketLoss: 0.5) +// let (pingStatus, _) = try await runTest(networkLinkConfig: networkLink) +// switch pingStatus { +// case .finished: +// () +// default: +// XCTFail("HTTP Test failed with status \(pingStatus)") +// } +// } +// +// func testMediumOutPacketDrop() async throws { +// let networkLink = TrafficControllerChannelHandler.NetworkLinkConfiguration(outPacketLoss: 0.5) +// let (pingStatus, _) = try await runTest(networkLinkConfig: networkLink) +// switch pingStatus { +// case .finished: +// () +// default: +// XCTFail("HTTP Test failed with status \(pingStatus)") +// } +// } +// +// func testFullyDuplicatedNetwork() async throws { +// let fullyDuplicated = TrafficControllerChannelHandler.NetworkLinkConfiguration.fullyDuplicated +// let (pingStatus, pingSummary) = try await runTest(networkLinkConfig: fullyDuplicated) +// switch pingStatus { +// case .finished: +// XCTAssertEqual(pingSummary?.duplicates.count, 9) // before the last duplicate is sent, the channel is already closed. +// default: +// XCTFail("HTTP Test failed with status \(pingStatus)") +// } +// } +// +// func testDuplicatedNetwork() async throws { +// let networkLink = TrafficControllerChannelHandler.NetworkLinkConfiguration.init(inDuplicate: 0.5) +// let (pingStatus, pingSummary) = try await runTest(networkLinkConfig: networkLink) +// switch pingStatus { +// case .finished: +// XCTAssertEqual(pingSummary?.duplicates.isEmpty, false) +// default: +// XCTFail("HTTP Test failed with status \(pingStatus)") +// } +// } + +} +#endif diff --git a/Tests/IntegrationTests/ICMPIntegrationTests.swift b/Tests/IntegrationTests/ICMPIntegrationTests.swift index ac3d7e2..8b0c676 100644 --- a/Tests/IntegrationTests/ICMPIntegrationTests.swift +++ b/Tests/IntegrationTests/ICMPIntegrationTests.swift @@ -1,26 +1,34 @@ // -// ICMPIntegrationTests.swift -// +// This source file is part of the LCLPing open source project // -// Created by JOHN ZZN on 12/12/23. +// Copyright (c) 2021-2023 Local Connectivity Lab and the project authors +// Licensed under Apache License v2.0 +// +// See LICENSE for license information +// See CONTRIBUTORS for the list of project authors +// +// SPDX-License-Identifier: Apache-2.0 // import XCTest +import NIOCore @testable import LCLPing #if INTEGRATION_TEST final class ICMPIntegrationTests: XCTestCase { - private func runTest(networkLinkConfig: TrafficControllerChannelHandler.NetworkLinkConfiguration, pingConfig: LCLPing.PingConfiguration) async throws -> (PingState, PingSummary?) { - var icmpPing = ICMPPing(networkLinkConfig: networkLinkConfig) + private func runTest( + networkLinkConfig: TrafficControllerChannelHandler.NetworkLinkConfiguration = .fullyConnected, + rewriteHeader: [PartialKeyPath>:AnyObject]? = nil, + pingConfig: LCLPing.PingConfiguration = .init(type: .icmp, endpoint: .ipv4("127.0.0.1", 0)) + ) async throws -> (PingState, PingSummary?) { + var icmpPing = ICMPPing(networkLinkConfig: networkLinkConfig, rewriteHeaders: rewriteHeader) try await icmpPing.start(with: pingConfig) return (icmpPing.pingStatus, icmpPing.summary) } func testFullyConnectedNetwork() async throws { - let fullyConnectedLink = TrafficControllerChannelHandler.NetworkLinkConfiguration.fullyConnected - let pingConfig = LCLPing.PingConfiguration(type: .icmp, endpoint: .ipv4("127.0.0.1", 0), count: 10) - let (pingStatus, pingSummary) = try await runTest(networkLinkConfig: fullyConnectedLink, pingConfig: pingConfig) + let (pingStatus, pingSummary) = try await runTest() switch pingStatus { case .finished: XCTAssertEqual(pingSummary?.totalCount, 10) @@ -36,9 +44,8 @@ final class ICMPIntegrationTests: XCTestCase { } func testFullyDisconnectedNetwork() async throws { - let fullyConnectedLink = TrafficControllerChannelHandler.NetworkLinkConfiguration.fullyDisconnected - let pingConfig = LCLPing.PingConfiguration(type: .icmp, endpoint: .ipv4("127.0.0.1", 0), count: 10) - let (pingStatus, pingSummary) = try await runTest(networkLinkConfig: fullyConnectedLink, pingConfig: pingConfig) + let fullyDisconnected = TrafficControllerChannelHandler.NetworkLinkConfiguration.fullyDisconnected + let (pingStatus, pingSummary) = try await runTest(networkLinkConfig: fullyDisconnected) switch pingStatus { case .finished: for i in 0..<10 { @@ -54,9 +61,8 @@ final class ICMPIntegrationTests: XCTestCase { } func testUnknownHost() async throws { - let fullyConnectedLink = TrafficControllerChannelHandler.NetworkLinkConfiguration.fullyDisconnected let pingConfig = LCLPing.PingConfiguration(type: .icmp, endpoint: .ipv4("10.10.10.127", 0), count: 10) - let (pingStatus, pingSummary) = try await runTest(networkLinkConfig: fullyConnectedLink, pingConfig: pingConfig) + let (pingStatus, pingSummary) = try await runTest(pingConfig: pingConfig) switch pingStatus { case .finished: XCTAssertEqual(pingSummary?.timeout.count, 10) @@ -67,8 +73,7 @@ final class ICMPIntegrationTests: XCTestCase { func testMinorInOutPacketDrop() async throws { let networkLink = TrafficControllerChannelHandler.NetworkLinkConfiguration(inPacketLoss: 0.1, outPacketLoss: 0.1) - let pingConfig = LCLPing.PingConfiguration(type: .icmp, endpoint: .ipv4("127.0.0.1", 0), count: 10) - let (pingStatus, _) = try await runTest(networkLinkConfig: networkLink, pingConfig: pingConfig) + let (pingStatus, _) = try await runTest(networkLinkConfig: networkLink) switch pingStatus { case .finished: () @@ -79,8 +84,7 @@ final class ICMPIntegrationTests: XCTestCase { func testMediumInOutPacketDrop() async throws { let networkLink = TrafficControllerChannelHandler.NetworkLinkConfiguration(inPacketLoss: 0.4, outPacketLoss: 0.4) - let pingConfig = LCLPing.PingConfiguration(type: .icmp, endpoint: .ipv4("127.0.0.1", 0), count: 10) - let (pingStatus, _) = try await runTest(networkLinkConfig: networkLink, pingConfig: pingConfig) + let (pingStatus, _) = try await runTest(networkLinkConfig: networkLink) switch pingStatus { case .finished: () @@ -91,8 +95,7 @@ final class ICMPIntegrationTests: XCTestCase { func testMinorInPacketDrop() async throws { let networkLink = TrafficControllerChannelHandler.NetworkLinkConfiguration(inPacketLoss: 0.2) - let pingConfig = LCLPing.PingConfiguration(type: .icmp, endpoint: .ipv4("127.0.0.1", 0), count: 10) - let (pingStatus, _) = try await runTest(networkLinkConfig: networkLink, pingConfig: pingConfig) + let (pingStatus, _) = try await runTest(networkLinkConfig: networkLink) switch pingStatus { case .finished: () @@ -103,8 +106,7 @@ final class ICMPIntegrationTests: XCTestCase { func testMinorOutPacketDrop() async throws { let networkLink = TrafficControllerChannelHandler.NetworkLinkConfiguration(outPacketLoss: 0.2) - let pingConfig = LCLPing.PingConfiguration(type: .icmp, endpoint: .ipv4("127.0.0.1", 0), count: 10) - let (pingStatus, _) = try await runTest(networkLinkConfig: networkLink, pingConfig: pingConfig) + let (pingStatus, _) = try await runTest(networkLinkConfig: networkLink) switch pingStatus { case .finished: () @@ -115,8 +117,7 @@ final class ICMPIntegrationTests: XCTestCase { func testMediumInPacketDrop() async throws { let networkLink = TrafficControllerChannelHandler.NetworkLinkConfiguration(inPacketLoss: 0.5) - let pingConfig = LCLPing.PingConfiguration(type: .icmp, endpoint: .ipv4("127.0.0.1", 0), count: 10) - let (pingStatus, _) = try await runTest(networkLinkConfig: networkLink, pingConfig: pingConfig) + let (pingStatus, _) = try await runTest(networkLinkConfig: networkLink) switch pingStatus { case .finished: () @@ -127,8 +128,7 @@ final class ICMPIntegrationTests: XCTestCase { func testMediumOutPacketDrop() async throws { let networkLink = TrafficControllerChannelHandler.NetworkLinkConfiguration(outPacketLoss: 0.5) - let pingConfig = LCLPing.PingConfiguration(type: .icmp, endpoint: .ipv4("127.0.0.1", 0), count: 10) - let (pingStatus, _) = try await runTest(networkLinkConfig: networkLink, pingConfig: pingConfig) + let (pingStatus, _) = try await runTest(networkLinkConfig: networkLink) switch pingStatus { case .finished: () @@ -138,10 +138,8 @@ final class ICMPIntegrationTests: XCTestCase { } func testFullyDuplicatedNetwork() async throws { - logger.logLevel = .debug let fullyDuplicated = TrafficControllerChannelHandler.NetworkLinkConfiguration.fullyDuplicated - let pingConfig = LCLPing.PingConfiguration(type: .icmp, endpoint: .ipv4("127.0.0.1", 0), count: 10) - let (pingStatus, pingSummary) = try await runTest(networkLinkConfig: fullyDuplicated, pingConfig: pingConfig) + let (pingStatus, pingSummary) = try await runTest(networkLinkConfig: fullyDuplicated) switch pingStatus { case .finished: XCTAssertEqual(pingSummary?.duplicates.count, 9) // before the last duplicate is sent, the channel is already closed. @@ -149,6 +147,34 @@ final class ICMPIntegrationTests: XCTestCase { XCTFail("ICMP Test failed with status \(pingStatus)") } } + + func testDuplicatedNetwork() async throws { + let networkLink = TrafficControllerChannelHandler.NetworkLinkConfiguration.init(inDuplicate: 0.5) + let (pingStatus, pingSummary) = try await runTest(networkLinkConfig: networkLink) + switch pingStatus { + case .finished: + XCTAssertEqual(pingSummary?.duplicates.isEmpty, false) + default: + XCTFail("ICMP Test failed with status \(pingStatus)") + } + } + + // TODO: more tests with header rewrite + +// func testInvalidIpHeader() async throws { +// let ipRewriteHeaders: [PartialKeyPath : AnyObject] = [ +// \.versionAndHeaderLength: 0x55 as AnyObject, +// \.protocol: 1 as AnyObject +// ] +// let expectedError = PingError.sendPingFailed(PingError.invalidIPVersion) +// do { +// let _ = try await runTest(ipRewriteHeader: ipRewriteHeaders) +// } catch { +// XCTAssertEqual(error.localizedDescription, expectedError.localizedDescription) +// } +// } + + // TODO: more tests on cancellation } #endif diff --git a/Tests/UtilitiesTests/SummarizePingResponseTests.swift b/Tests/UtilitiesTests/SummarizePingResponseTests.swift index bf60fea..be18339 100644 --- a/Tests/UtilitiesTests/SummarizePingResponseTests.swift +++ b/Tests/UtilitiesTests/SummarizePingResponseTests.swift @@ -18,9 +18,9 @@ final class SummarizePingResponseTests: XCTestCase { private let empty: [PingResponse] = [] private let singleValueOk: [PingResponse] = [.ok(1, 1.1, 100)] - private let singleValueError: [PingResponse] = [.error(nil)] + private let singleValueError: [PingResponse] = [.error(1, PingError.forTestingPurposeOnly)] private let multipleOk: [PingResponse] = [.ok(1, 1.3, 100), .ok(2, 2.3, 101), .ok(3, 3.1, 102)] - private let multipleOkAndError: [PingResponse] = [.ok(1, 1.3, 100), .error(nil), .ok(3, 3.1, 102), .ok(4, 4.2, 103)] + private let multipleOkAndError: [PingResponse] = [.ok(1, 1.3, 100), .error(2, PingError.forTestingPurposeOnly), .ok(3, 3.1, 102), .ok(4, 4.2, 103)] private let multpleOkAndDuplicates: [PingResponse] = [ .ok(1, 1.3, 100), .duplicated(1), @@ -42,7 +42,7 @@ final class SummarizePingResponseTests: XCTestCase { .duplicated(1), .ok(2, 2.2, 102), .duplicated(2), - .error(nil), + .error(3, PingError.forTestingPurposeOnly), .ok(4, 1.4, 104), .duplicated(3), .timeout(5), @@ -78,30 +78,30 @@ final class SummarizePingResponseTests: XCTestCase { .timeout(4), ] private let allErrors: [PingResponse] = [ - .error(nil), - .error(nil), - .error(nil), - .error(nil), - .error(nil), - .error(nil) + .error(1, PingError.forTestingPurposeOnly), + .error(2, PingError.forTestingPurposeOnly), + .error(3, PingError.forTestingPurposeOnly), + .error(4, PingError.forTestingPurposeOnly), + .error(5, PingError.forTestingPurposeOnly), + .error(6, PingError.forTestingPurposeOnly) ] private let host = try! SocketAddress(ipAddress: "127.0.0.1", port: 80) func testEmpty() { let result = summarizePingResponse(empty, host: host) - let target = PingSummary(min: .greatestFiniteMagnitude, max: .zero, avg: .zero, median: .zero, stdDev: .zero, jitter: 0.0, details: [], totalCount: 0, timeout: Set(), duplicates: Set(), ipAddress: host.ipAddress!, port: 80, protocol: host.protocol.rawValue) + let target = PingSummary(min: .greatestFiniteMagnitude, max: .zero, avg: .zero, median: .zero, stdDev: .zero, jitter: 0.0, details: [], totalCount: 0, timeout: Set(), duplicates: Set(), errors: Set(), ipAddress: host.ipAddress!, port: 80, protocol: host.protocol.rawValue) XCTAssertEqual(result, target) } func testOneValue() { let resultOk = summarizePingResponse(singleValueOk, host: host) - let targetOk = PingSummary(min: 1.1, max: 1.1, avg: 1.1, median: 1.1, stdDev: 0.0, jitter: 0.0, details: [.init(seqNum: 1, latency: 1.1, timestamp: 100)], totalCount: 1, timeout: Set(), duplicates: Set(), ipAddress: host.ipAddress!, port: 80, protocol: host.protocol.rawValue) + let targetOk = PingSummary(min: 1.1, max: 1.1, avg: 1.1, median: 1.1, stdDev: 0.0, jitter: 0.0, details: [.init(seqNum: 1, latency: 1.1, timestamp: 100)], totalCount: 1, timeout: Set(), duplicates: Set(), errors: [], ipAddress: host.ipAddress!, port: 80, protocol: host.protocol.rawValue) XCTAssertEqual(resultOk, targetOk) let resultError = summarizePingResponse(singleValueError, host: host) - let targetError = PingSummary(min: .greatestFiniteMagnitude, max: .zero, avg: .zero, median: .zero, stdDev: .zero, jitter: .zero, details: [], totalCount: 1, timeout: Set(), duplicates: Set(), ipAddress: host.ipAddress!, port: 80, protocol: host.protocol.rawValue) + let targetError = PingSummary(min: .greatestFiniteMagnitude, max: .zero, avg: .zero, median: .zero, stdDev: .zero, jitter: .zero, details: [], totalCount: 1, timeout: Set(), duplicates: Set(), errors: [.init(seqNum: 1, reason: PingError.forTestingPurposeOnly.localizedDescription)], ipAddress: host.ipAddress!, port: 80, protocol: host.protocol.rawValue) XCTAssertEqual(resultError, targetError) } @@ -118,6 +118,7 @@ final class SummarizePingResponseTests: XCTestCase { totalCount: 3, timeout: Set(), duplicates: Set(), + errors: Set(), ipAddress: host.ipAddress!, port: 80, protocol: host.protocol.rawValue @@ -126,6 +127,9 @@ final class SummarizePingResponseTests: XCTestCase { let resultMultipleOkAndError = summarizePingResponse(multipleOkAndError, host: host) let multipleOkAndErrorPingResults: [PingResult] = [.init(seqNum: 1, latency: 1.3, timestamp: 100), .init(seqNum: 3, latency: 3.1, timestamp: 102), .init(seqNum: 4, latency: 4.2, timestamp: 103)] + let multipleOkAndErrorErrors: Set = [ + .init(seqNum: 2, reason: PingError.forTestingPurposeOnly.localizedDescription) + ] let targetMultipleOkAndError = PingSummary(min: 1.3, max: 4.2, avg: multipleOkAndErrorPingResults.avg, @@ -134,7 +138,9 @@ final class SummarizePingResponseTests: XCTestCase { jitter: computeJitter(multipleOkAndErrorPingResults), details: multipleOkAndErrorPingResults, totalCount: 4, - timeout: Set(), duplicates: Set(), ipAddress: host.ipAddress!, port: 80, protocol: host.protocol.rawValue) + timeout: Set(), duplicates: Set(), + errors: multipleOkAndErrorErrors, + ipAddress: host.ipAddress!, port: 80, protocol: host.protocol.rawValue) XCTAssertEqual(resultMultipleOkAndError, targetMultipleOkAndError) } @@ -151,7 +157,7 @@ final class SummarizePingResponseTests: XCTestCase { stdDev: multipleOkAndDuplicates.stdDev, jitter: computeJitter(multipleOkAndDuplicates), details: multipleOkAndDuplicates, totalCount: 2, timeout: Set(), - duplicates: Set([1]), + duplicates: Set([1]), errors: Set(), ipAddress: host.ipAddress!, port: 80, protocol: host.protocol.rawValue) XCTAssertEqual(resultMultipleOkAndDuplicates, targetMultipleOkAndDuplicates) @@ -171,7 +177,7 @@ final class SummarizePingResponseTests: XCTestCase { stdDev: multipleOkAndTimeouts.stdDev, jitter: computeJitter(multipleOkAndTimeouts), details: multipleOkAndTimeouts, totalCount: 7, timeout: Set([2,4,5,7]), - duplicates: Set(), + duplicates: Set(), errors: Set(), ipAddress: host.ipAddress!, port: 80, protocol: host.protocol.rawValue) XCTAssertEqual(resultMultipleOkAndTimeouts, targetMultipleOkAndTimeouts) @@ -189,7 +195,7 @@ final class SummarizePingResponseTests: XCTestCase { details: allTimeouts, totalCount: 8, timeout: Set([1,2,3,4,5,6,7,8]), - duplicates: Set(), + duplicates: Set(), errors: Set(), ipAddress: host.ipAddress!, port: 80, protocol: host.protocol.rawValue) XCTAssertEqual(resultAllTimeouts, targetAllTimeouts) @@ -207,7 +213,7 @@ final class SummarizePingResponseTests: XCTestCase { details: timeoutsDuplicates, totalCount: 4, timeout: Set([1,2,3,4]), - duplicates: Set(), + duplicates: Set(), errors: Set(), ipAddress: host.ipAddress!, port: 80, protocol: host.protocol.rawValue) XCTAssertEqual(resultTimeoutsDuplicates, targetTimeoutsDuplicates) @@ -216,6 +222,14 @@ final class SummarizePingResponseTests: XCTestCase { func testAllErrors() { let resultAllErrors = summarizePingResponse(allErrors, host: host) let allErrorsPingResult = [PingResult]() + let allErrorsErrorSummaries: Set = [ + .init(seqNum: 1, reason: PingError.forTestingPurposeOnly.localizedDescription), + .init(seqNum: 2, reason: PingError.forTestingPurposeOnly.localizedDescription), + .init(seqNum: 3, reason: PingError.forTestingPurposeOnly.localizedDescription), + .init(seqNum: 4, reason: PingError.forTestingPurposeOnly.localizedDescription), + .init(seqNum: 5, reason: PingError.forTestingPurposeOnly.localizedDescription), + .init(seqNum: 6, reason: PingError.forTestingPurposeOnly.localizedDescription) + ] let targetAllErrors = PingSummary(min: .greatestFiniteMagnitude, max: .zero, avg: .zero, @@ -225,7 +239,7 @@ final class SummarizePingResponseTests: XCTestCase { details: allErrorsPingResult, totalCount: 6, timeout: Set(), - duplicates: Set(), + duplicates: Set(), errors: allErrorsErrorSummaries, ipAddress: host.ipAddress!, port: 80, protocol: host.protocol.rawValue) XCTAssertEqual(resultAllErrors, targetAllErrors) @@ -240,6 +254,9 @@ final class SummarizePingResponseTests: XCTestCase { PingResult(seqNum: 7, latency: 3.6, timestamp: 107), PingResult(seqNum: 10, latency: 5.9, timestamp: 110) ] + let mixedErrorSummaries: Set = [ + .init(seqNum: 3, reason: PingError.forTestingPurposeOnly.localizedDescription) + ] let targetMixed = PingSummary(min: 1.4, max: 5.9, avg: mixedPingResults.avg, @@ -249,7 +266,7 @@ final class SummarizePingResponseTests: XCTestCase { details: mixedPingResults, totalCount: 10, timeout: Set([1,5,8,9]), - duplicates: Set([1,2,3,7]), + duplicates: Set([1,2,3,7]), errors: mixedErrorSummaries, ipAddress: host.ipAddress!, port: 80, protocol: host.protocol.rawValue) XCTAssertEqual(resultMixed, targetMixed)