1379 lines
52 KiB
Swift
1379 lines
52 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the SwiftNIO open source project
|
|
//
|
|
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
|
|
// Licensed under Apache License v2.0
|
|
//
|
|
// See LICENSE.txt for license information
|
|
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import XCTest
|
|
import NIOConcurrencyHelpers
|
|
@testable import NIO
|
|
|
|
private final class IndexWritingHandler: ChannelDuplexHandler {
|
|
typealias InboundIn = ByteBuffer
|
|
typealias InboundOut = ByteBuffer
|
|
typealias OutboundIn = ByteBuffer
|
|
typealias OutboundOut = ByteBuffer
|
|
|
|
private let index: Int
|
|
|
|
init(_ index: Int) {
|
|
self.index = index
|
|
}
|
|
|
|
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
|
var buf = self.unwrapInboundIn(data)
|
|
buf.writeInteger(UInt8(self.index))
|
|
context.fireChannelRead(self.wrapInboundOut(buf))
|
|
}
|
|
|
|
func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
|
|
var buf = self.unwrapOutboundIn(data)
|
|
buf.writeInteger(UInt8(self.index))
|
|
context.write(self.wrapOutboundOut(buf), promise: promise)
|
|
}
|
|
}
|
|
|
|
private extension EmbeddedChannel {
|
|
func assertReadIndexOrder(_ order: [UInt8]) {
|
|
XCTAssertTrue(try self.writeInbound(self.allocator.buffer(capacity: 32)).isFull)
|
|
do {
|
|
var outBuffer: ByteBuffer = try self.readInbound()!
|
|
XCTAssertEqual(outBuffer.readBytes(length: outBuffer.readableBytes)!, order)
|
|
} catch {
|
|
XCTFail("unexpected error: \(error)")
|
|
}
|
|
}
|
|
|
|
func assertWriteIndexOrder(_ order: [UInt8]) {
|
|
XCTAssertTrue(try self.writeOutbound(self.allocator.buffer(capacity: 32)).isFull)
|
|
do {
|
|
guard var outBuffer2 = try self.readOutbound(as: ByteBuffer.self) else {
|
|
XCTFail("Could not read byte buffer")
|
|
return
|
|
}
|
|
|
|
XCTAssertEqual(outBuffer2.readBytes(length: outBuffer2.readableBytes)!, order)
|
|
} catch {
|
|
XCTFail("unexpected error: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
class ChannelPipelineTest: XCTestCase {
|
|
|
|
class SimpleTypedHandler1: ChannelInboundHandler {
|
|
typealias InboundIn = NIOAny
|
|
}
|
|
class SimpleTypedHandler2: ChannelInboundHandler {
|
|
typealias InboundIn = NIOAny
|
|
}
|
|
class SimpleTypedHandler3: ChannelInboundHandler {
|
|
typealias InboundIn = NIOAny
|
|
}
|
|
|
|
func testGetHandler() throws {
|
|
let handler1 = SimpleTypedHandler1()
|
|
let handler2 = SimpleTypedHandler2()
|
|
let handler3 = SimpleTypedHandler3()
|
|
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
XCTAssertNoThrow(try channel.finish())
|
|
}
|
|
try channel.pipeline.addHandlers([
|
|
handler1,
|
|
handler2,
|
|
handler3
|
|
]).wait()
|
|
|
|
let result1 = try channel.pipeline.handler(type: SimpleTypedHandler1.self).wait()
|
|
XCTAssertTrue(result1 === handler1)
|
|
|
|
let result2 = try channel.pipeline.handler(type: SimpleTypedHandler2.self).wait()
|
|
XCTAssertTrue(result2 === handler2)
|
|
|
|
let result3 = try channel.pipeline.handler(type: SimpleTypedHandler3.self).wait()
|
|
XCTAssertTrue(result3 === handler3)
|
|
}
|
|
|
|
func testGetFirstHandler() throws {
|
|
let sameTypeHandler1 = SimpleTypedHandler1()
|
|
let sameTypeHandler2 = SimpleTypedHandler1()
|
|
let otherHandler = SimpleTypedHandler2()
|
|
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
XCTAssertNoThrow(try channel.finish())
|
|
}
|
|
try channel.pipeline.addHandlers([
|
|
sameTypeHandler1,
|
|
sameTypeHandler2,
|
|
otherHandler
|
|
]).wait()
|
|
|
|
let result = try channel.pipeline.handler(type: SimpleTypedHandler1.self).wait()
|
|
XCTAssertTrue(result === sameTypeHandler1)
|
|
}
|
|
|
|
func testGetNotAddedHandler() throws {
|
|
let handler1 = SimpleTypedHandler1()
|
|
let handler2 = SimpleTypedHandler2()
|
|
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
XCTAssertNoThrow(try channel.finish())
|
|
}
|
|
try channel.pipeline.addHandlers([
|
|
handler1,
|
|
handler2,
|
|
]).wait()
|
|
|
|
XCTAssertThrowsError(try channel.pipeline.handler(type: SimpleTypedHandler3.self).wait()) { XCTAssertTrue($0 is ChannelPipelineError )}
|
|
}
|
|
|
|
func testAddAfterClose() throws {
|
|
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
XCTAssertNoThrow(try channel.finish(acceptAlreadyClosed: true))
|
|
}
|
|
XCTAssertNoThrow(try channel.close().wait())
|
|
|
|
channel.pipeline.removeHandlers()
|
|
|
|
let handler = DummyHandler()
|
|
defer {
|
|
XCTAssertFalse(handler.handlerAddedCalled.load())
|
|
XCTAssertFalse(handler.handlerRemovedCalled.load())
|
|
}
|
|
do {
|
|
try channel.pipeline.addHandler(handler).wait()
|
|
XCTFail()
|
|
} catch let err as ChannelError {
|
|
XCTAssertEqual(err, .ioOnClosedChannel)
|
|
}
|
|
}
|
|
|
|
private final class DummyHandler: ChannelHandler {
|
|
let handlerAddedCalled = NIOAtomic<Bool>.makeAtomic(value: false)
|
|
let handlerRemovedCalled = NIOAtomic<Bool>.makeAtomic(value: false)
|
|
|
|
public func handlerAdded(context: ChannelHandlerContext) {
|
|
handlerAddedCalled.store(true)
|
|
}
|
|
|
|
public func handlerRemoved(context: ChannelHandlerContext) {
|
|
handlerRemovedCalled.store(true)
|
|
}
|
|
}
|
|
|
|
func testOutboundOrdering() throws {
|
|
|
|
let channel = EmbeddedChannel()
|
|
|
|
var buf = channel.allocator.buffer(capacity: 1024)
|
|
buf.writeString("hello")
|
|
|
|
_ = try channel.pipeline.addHandler(TestChannelOutboundHandler<Int, ByteBuffer> { data in
|
|
XCTAssertEqual(1, data)
|
|
return buf
|
|
}).wait()
|
|
|
|
_ = try channel.pipeline.addHandler(TestChannelOutboundHandler<String, Int> { data in
|
|
XCTAssertEqual("msg", data)
|
|
return 1
|
|
}).wait()
|
|
|
|
XCTAssertNoThrow(try channel.writeAndFlush(NIOAny("msg")).wait() as Void)
|
|
if let data = try channel.readOutbound(as: ByteBuffer.self) {
|
|
XCTAssertEqual(buf, data)
|
|
} else {
|
|
XCTFail("couldn't read from channel")
|
|
}
|
|
XCTAssertNoThrow(XCTAssertNil(try channel.readOutbound()))
|
|
|
|
XCTAssertTrue(try channel.finish().isClean)
|
|
}
|
|
|
|
func testConnectingDoesntCallBind() throws {
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
XCTAssertTrue(try channel.finish().isClean)
|
|
}
|
|
var ipv4SocketAddress = sockaddr_in()
|
|
ipv4SocketAddress.sin_port = (12345 as in_port_t).bigEndian
|
|
let sa = SocketAddress(ipv4SocketAddress, host: "foobar.com")
|
|
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(NoBindAllowed()).wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(TestChannelOutboundHandler<ByteBuffer, ByteBuffer> { data in
|
|
data
|
|
}).wait())
|
|
|
|
XCTAssertNoThrow(try channel.connect(to: sa).wait())
|
|
}
|
|
|
|
private final class TestChannelOutboundHandler<In, Out>: ChannelOutboundHandler {
|
|
typealias OutboundIn = In
|
|
typealias OutboundOut = Out
|
|
|
|
private let body: (OutboundIn) throws -> OutboundOut
|
|
|
|
init(_ body: @escaping (OutboundIn) throws -> OutboundOut) {
|
|
self.body = body
|
|
}
|
|
|
|
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
|
|
do {
|
|
context.write(self.wrapOutboundOut(try body(self.unwrapOutboundIn(data))), promise: promise)
|
|
} catch let err {
|
|
promise!.fail(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class NoBindAllowed: ChannelOutboundHandler {
|
|
typealias OutboundIn = ByteBuffer
|
|
typealias OutboundOut = ByteBuffer
|
|
|
|
enum TestFailureError: Error {
|
|
case CalledBind
|
|
}
|
|
|
|
public func bind(context: ChannelHandlerContext, to address: SocketAddress, promise: EventLoopPromise<Void>?) {
|
|
promise!.fail(TestFailureError.CalledBind)
|
|
}
|
|
}
|
|
|
|
private final class FireChannelReadOnRemoveHandler: ChannelInboundHandler, RemovableChannelHandler {
|
|
typealias InboundIn = Never
|
|
typealias InboundOut = Int
|
|
|
|
public func handlerRemoved(context: ChannelHandlerContext) {
|
|
context.fireChannelRead(self.wrapInboundOut(1))
|
|
}
|
|
}
|
|
|
|
func testFiringChannelReadsInHandlerRemovedWorks() throws {
|
|
let channel = EmbeddedChannel()
|
|
|
|
let h = FireChannelReadOnRemoveHandler()
|
|
_ = try channel.pipeline.addHandler(h).flatMap {
|
|
channel.pipeline.removeHandler(h)
|
|
}.wait()
|
|
|
|
XCTAssertNoThrow(XCTAssertEqual(Optional<Int>.some(1), try channel.readInbound()))
|
|
XCTAssertTrue(try channel.finish().isClean)
|
|
}
|
|
|
|
func testEmptyPipelineWorks() throws {
|
|
let channel = EmbeddedChannel()
|
|
XCTAssertTrue(try assertNoThrowWithValue(channel.writeInbound(2)).isFull)
|
|
XCTAssertNoThrow(XCTAssertEqual(Optional<Int>.some(2), try channel.readInbound()))
|
|
XCTAssertTrue(try channel.finish().isClean)
|
|
}
|
|
|
|
func testWriteAfterClose() throws {
|
|
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
XCTAssertNoThrow(XCTAssertTrue(try channel.finish(acceptAlreadyClosed: true).isClean))
|
|
}
|
|
XCTAssertNoThrow(try channel.close().wait())
|
|
let loop = channel.eventLoop as! EmbeddedEventLoop
|
|
loop.run()
|
|
|
|
XCTAssertTrue(loop.inEventLoop)
|
|
do {
|
|
let handle = NIOFileHandle(descriptor: -1)
|
|
let fr = FileRegion(fileHandle: handle, readerIndex: 0, endIndex: 0)
|
|
defer {
|
|
// fake descriptor, so shouldn't be closed.
|
|
XCTAssertNoThrow(try handle.takeDescriptorOwnership())
|
|
}
|
|
try channel.writeOutbound(fr)
|
|
loop.run()
|
|
XCTFail("we ran but an error should have been thrown")
|
|
} catch let err as ChannelError {
|
|
XCTAssertEqual(err, .ioOnClosedChannel)
|
|
}
|
|
}
|
|
|
|
func testOutboundNextForInboundOnlyIsCorrect() throws {
|
|
/// This handler always add its number to the inbound `[Int]` array
|
|
final class MarkingInboundHandler: ChannelInboundHandler {
|
|
typealias InboundIn = [Int]
|
|
typealias InboundOut = [Int]
|
|
|
|
private let no: Int
|
|
|
|
init(number: Int) {
|
|
self.no = number
|
|
}
|
|
|
|
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
|
let data = self.unwrapInboundIn(data)
|
|
context.fireChannelRead(self.wrapInboundOut(data + [self.no]))
|
|
}
|
|
}
|
|
|
|
/// This handler always add its number to the outbound `[Int]` array
|
|
final class MarkingOutboundHandler: ChannelOutboundHandler {
|
|
typealias OutboundIn = [Int]
|
|
typealias OutboundOut = [Int]
|
|
|
|
private let no: Int
|
|
|
|
init(number: Int) {
|
|
self.no = number
|
|
}
|
|
|
|
func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
|
|
let data = self.unwrapOutboundIn(data)
|
|
context.write(self.wrapOutboundOut(data + [self.no]), promise: promise)
|
|
}
|
|
}
|
|
|
|
/// This handler multiplies the inbound `[Int]` it receives by `-1` and writes it to the next outbound handler.
|
|
final class WriteOnReadHandler: ChannelInboundHandler {
|
|
typealias InboundIn = [Int]
|
|
typealias InboundOut = [Int]
|
|
typealias OutboundOut = [Int]
|
|
|
|
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
|
let data = self.unwrapInboundIn(data)
|
|
context.writeAndFlush(self.wrapOutboundOut(data.map { $0 * -1 }), promise: nil)
|
|
context.fireChannelRead(self.wrapInboundOut(data))
|
|
}
|
|
}
|
|
|
|
/// This handler just prints out the outbound received `[Int]` as a `ByteBuffer`.
|
|
final class PrintOutboundAsByteBufferHandler: ChannelOutboundHandler {
|
|
typealias OutboundIn = [Int]
|
|
typealias OutboundOut = ByteBuffer
|
|
|
|
func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
|
|
let data = self.unwrapOutboundIn(data)
|
|
var buf = context.channel.allocator.buffer(capacity: 123)
|
|
buf.writeString(String(describing: data))
|
|
context.write(self.wrapOutboundOut(buf), promise: promise)
|
|
}
|
|
}
|
|
|
|
let channel = EmbeddedChannel()
|
|
let loop = channel.eventLoop as! EmbeddedEventLoop
|
|
loop.run()
|
|
|
|
try channel.pipeline.addHandler(PrintOutboundAsByteBufferHandler()).wait()
|
|
try channel.pipeline.addHandler(MarkingInboundHandler(number: 2)).wait()
|
|
try channel.pipeline.addHandler(WriteOnReadHandler()).wait()
|
|
try channel.pipeline.addHandler(MarkingOutboundHandler(number: 4)).wait()
|
|
try channel.pipeline.addHandler(WriteOnReadHandler()).wait()
|
|
try channel.pipeline.addHandler(MarkingInboundHandler(number: 6)).wait()
|
|
try channel.pipeline.addHandler(WriteOnReadHandler()).wait()
|
|
|
|
try channel.writeInbound([])
|
|
loop.run()
|
|
XCTAssertNoThrow(XCTAssertEqual([2, 6], try channel.readInbound()!))
|
|
|
|
/* the first thing, we should receive is `[-2]` as it shouldn't hit any `MarkingOutboundHandler`s (`4`) */
|
|
var outbound = try channel.readOutbound(as: ByteBuffer.self)
|
|
if var buf = outbound {
|
|
XCTAssertEqual("[-2]", buf.readString(length: buf.readableBytes))
|
|
} else {
|
|
XCTFail("wrong contents: \(outbound.debugDescription)")
|
|
}
|
|
|
|
/* the next thing we should receive is `[-2, 4]` as the first `WriteOnReadHandler` (receiving `[2]`) is behind the `MarkingOutboundHandler` (`4`) */
|
|
outbound = try channel.readOutbound()
|
|
if var buf = outbound {
|
|
XCTAssertEqual("[-2, 4]", buf.readString(length: buf.readableBytes))
|
|
} else {
|
|
XCTFail("wrong contents: \(outbound.debugDescription)")
|
|
}
|
|
|
|
/* and finally, we're waiting for `[-2, -6, 4]` as the second `WriteOnReadHandler`s (receiving `[2, 4]`) is behind the `MarkingOutboundHandler` (`4`) */
|
|
outbound = try channel.readOutbound()
|
|
if var buf = outbound {
|
|
XCTAssertEqual("[-2, -6, 4]", buf.readString(length: buf.readableBytes))
|
|
} else {
|
|
XCTFail("wrong contents: \(outbound.debugDescription)")
|
|
}
|
|
|
|
XCTAssertNoThrow(XCTAssertNil(try channel.readInbound()))
|
|
XCTAssertNoThrow(XCTAssertNil(try channel.readOutbound()))
|
|
|
|
XCTAssertTrue(try channel.finish().isClean)
|
|
}
|
|
|
|
func testChannelInfrastructureIsNotLeaked() throws {
|
|
class SomeHandler: ChannelInboundHandler, RemovableChannelHandler {
|
|
typealias InboundIn = Never
|
|
|
|
let body: (ChannelHandlerContext) -> Void
|
|
|
|
init(_ body: @escaping (ChannelHandlerContext) -> Void) {
|
|
self.body = body
|
|
}
|
|
|
|
func handlerAdded(context: ChannelHandlerContext) {
|
|
self.body(context)
|
|
}
|
|
}
|
|
try {
|
|
let channel = EmbeddedChannel()
|
|
let loop = channel.eventLoop as! EmbeddedEventLoop
|
|
|
|
weak var weakHandler1: RemovableChannelHandler?
|
|
weak var weakHandler2: ChannelHandler?
|
|
weak var weakHandlerContext1: ChannelHandlerContext?
|
|
weak var weakHandlerContext2: ChannelHandlerContext?
|
|
|
|
() /* needed because Swift's grammar is so ambiguous that you can't remove this :\ */
|
|
|
|
try {
|
|
let handler1 = SomeHandler { context in
|
|
weakHandlerContext1 = context
|
|
}
|
|
weakHandler1 = handler1
|
|
let handler2 = SomeHandler { context in
|
|
weakHandlerContext2 = context
|
|
}
|
|
weakHandler2 = handler2
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(handler1).flatMap {
|
|
channel.pipeline.addHandler(handler2)
|
|
}.wait())
|
|
}()
|
|
|
|
XCTAssertNotNil(weakHandler1)
|
|
XCTAssertNotNil(weakHandler2)
|
|
XCTAssertNotNil(weakHandlerContext1)
|
|
XCTAssertNotNil(weakHandlerContext2)
|
|
|
|
XCTAssertNoThrow(try channel.pipeline.removeHandler(weakHandler1!).wait())
|
|
|
|
XCTAssertNil(weakHandler1)
|
|
XCTAssertNotNil(weakHandler2)
|
|
XCTAssertNil(weakHandlerContext1)
|
|
XCTAssertNotNil(weakHandlerContext2)
|
|
|
|
XCTAssertTrue(try channel.finish().isClean)
|
|
|
|
XCTAssertNil(weakHandler1)
|
|
XCTAssertNil(weakHandler2)
|
|
XCTAssertNil(weakHandlerContext1)
|
|
XCTAssertNil(weakHandlerContext2)
|
|
|
|
XCTAssertNoThrow(try loop.syncShutdownGracefully())
|
|
}()
|
|
}
|
|
|
|
func testAddingHandlersFirstWorks() throws {
|
|
final class ReceiveIntHandler: ChannelInboundHandler {
|
|
typealias InboundIn = Int
|
|
|
|
var intReadCount = 0
|
|
|
|
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
|
if data.tryAs(type: Int.self) != nil {
|
|
self.intReadCount += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
final class TransformStringToIntHandler: ChannelInboundHandler {
|
|
typealias InboundIn = String
|
|
typealias InboundOut = Int
|
|
|
|
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
|
if let dataString = data.tryAs(type: String.self) {
|
|
context.fireChannelRead(self.wrapInboundOut(dataString.count))
|
|
}
|
|
}
|
|
}
|
|
|
|
final class TransformByteBufferToStringHandler: ChannelInboundHandler {
|
|
typealias InboundIn = ByteBuffer
|
|
typealias InboundOut = String
|
|
|
|
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
|
if var buffer = data.tryAs(type: ByteBuffer.self) {
|
|
context.fireChannelRead(self.wrapInboundOut(buffer.readString(length: buffer.readableBytes)!))
|
|
}
|
|
}
|
|
}
|
|
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
XCTAssertNoThrow(try channel.finish())
|
|
}
|
|
|
|
let countHandler = ReceiveIntHandler()
|
|
var buffer = channel.allocator.buffer(capacity: 12)
|
|
buffer.writeStaticString("hello, world")
|
|
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(countHandler).wait())
|
|
XCTAssertTrue(try channel.writeInbound(buffer).isEmpty)
|
|
XCTAssertEqual(countHandler.intReadCount, 0)
|
|
|
|
try channel.pipeline.addHandlers(TransformByteBufferToStringHandler(),
|
|
TransformStringToIntHandler(),
|
|
position: .first).wait()
|
|
XCTAssertTrue(try channel.writeInbound(buffer).isEmpty)
|
|
XCTAssertEqual(countHandler.intReadCount, 1)
|
|
}
|
|
|
|
func testAddAfter() {
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
XCTAssertNoThrow(try channel.finish())
|
|
}
|
|
|
|
let firstHandler = IndexWritingHandler(1)
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(firstHandler).wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(IndexWritingHandler(2)).wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(IndexWritingHandler(3),
|
|
position: .after(firstHandler)).wait())
|
|
|
|
channel.assertReadIndexOrder([1, 3, 2])
|
|
channel.assertWriteIndexOrder([2, 3, 1])
|
|
}
|
|
|
|
func testAddBefore() {
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
XCTAssertNoThrow(try channel.finish())
|
|
}
|
|
|
|
let secondHandler = IndexWritingHandler(2)
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(IndexWritingHandler(1)).wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(secondHandler).wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(IndexWritingHandler(3),
|
|
position: .before(secondHandler)).wait())
|
|
|
|
channel.assertReadIndexOrder([1, 3, 2])
|
|
channel.assertWriteIndexOrder([2, 3, 1])
|
|
}
|
|
|
|
func testAddAfterLast() {
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
XCTAssertNoThrow(try channel.finish())
|
|
}
|
|
|
|
let secondHandler = IndexWritingHandler(2)
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(IndexWritingHandler(1)).wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(secondHandler).wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(IndexWritingHandler(3),
|
|
position: .after(secondHandler)).wait())
|
|
|
|
channel.assertReadIndexOrder([1, 2, 3])
|
|
channel.assertWriteIndexOrder([3, 2, 1])
|
|
}
|
|
|
|
func testAddBeforeFirst() {
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
XCTAssertNoThrow(try channel.finish())
|
|
}
|
|
|
|
let firstHandler = IndexWritingHandler(1)
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(firstHandler).wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(IndexWritingHandler(2)).wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(IndexWritingHandler(3),
|
|
position: .before(firstHandler)).wait())
|
|
|
|
channel.assertReadIndexOrder([3, 1, 2])
|
|
channel.assertWriteIndexOrder([2, 1, 3])
|
|
}
|
|
|
|
func testAddAfterWhileClosed() {
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
do {
|
|
_ = try channel.finish()
|
|
XCTFail("Did not throw")
|
|
} catch ChannelError.alreadyClosed {
|
|
// Ok
|
|
} catch {
|
|
XCTFail("unexpected error \(error)")
|
|
}
|
|
}
|
|
|
|
let handler = IndexWritingHandler(1)
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait())
|
|
XCTAssertNoThrow(try channel.close().wait())
|
|
channel.embeddedEventLoop.run()
|
|
|
|
do {
|
|
try channel.pipeline.addHandler(IndexWritingHandler(2), position: .after(handler)).wait()
|
|
XCTFail("Did not throw")
|
|
} catch ChannelError.ioOnClosedChannel {
|
|
// all good
|
|
} catch {
|
|
XCTFail("Got incorrect error: \(error)")
|
|
}
|
|
}
|
|
|
|
func testAddBeforeWhileClosed() {
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
XCTAssertThrowsError(try channel.finish()) { error in
|
|
XCTAssertEqual(.alreadyClosed, error as? ChannelError)
|
|
}
|
|
}
|
|
|
|
let handler = IndexWritingHandler(1)
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait())
|
|
XCTAssertNoThrow(try channel.close().wait())
|
|
channel.embeddedEventLoop.run()
|
|
|
|
do {
|
|
try channel.pipeline.addHandler(IndexWritingHandler(2), position: .before(handler)).wait()
|
|
XCTFail("Did not throw")
|
|
} catch ChannelError.ioOnClosedChannel {
|
|
// all good
|
|
} catch {
|
|
XCTFail("Got incorrect error: \(error)")
|
|
}
|
|
}
|
|
|
|
func testFindHandlerByType() {
|
|
class TypeAHandler: ChannelInboundHandler {
|
|
typealias InboundIn = Any
|
|
typealias InboundOut = Any
|
|
}
|
|
|
|
class TypeBHandler: ChannelInboundHandler {
|
|
typealias InboundIn = Any
|
|
typealias InboundOut = Any
|
|
}
|
|
|
|
class TypeCHandler: ChannelInboundHandler {
|
|
typealias InboundIn = Any
|
|
typealias InboundOut = Any
|
|
}
|
|
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
XCTAssertNoThrow(try channel.finish())
|
|
}
|
|
|
|
let h1 = TypeAHandler()
|
|
let h2 = TypeBHandler()
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(h1).wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(h2).wait())
|
|
|
|
XCTAssertTrue(try h1 === channel.pipeline.context(handlerType: TypeAHandler.self).wait().handler)
|
|
XCTAssertTrue(try h2 === channel.pipeline.context(handlerType: TypeBHandler.self).wait().handler)
|
|
|
|
do {
|
|
_ = try channel.pipeline.context(handlerType: TypeCHandler.self).wait()
|
|
XCTFail("Did not throw")
|
|
} catch ChannelPipelineError.notFound {
|
|
// ok
|
|
} catch {
|
|
XCTFail("Unexpected error \(error)")
|
|
}
|
|
}
|
|
|
|
func testFindHandlerByTypeReturnsTheFirstOfItsType() {
|
|
class TestHandler: ChannelInboundHandler {
|
|
typealias InboundIn = Any
|
|
typealias InboundOut = Any
|
|
}
|
|
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
XCTAssertNoThrow(try channel.finish())
|
|
}
|
|
|
|
let h1 = TestHandler()
|
|
let h2 = TestHandler()
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(h1).wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(h2).wait())
|
|
|
|
XCTAssertTrue(try h1 === channel.pipeline.context(handlerType: TestHandler.self).wait().handler)
|
|
XCTAssertFalse(try h2 === channel.pipeline.context(handlerType: TestHandler.self).wait().handler)
|
|
}
|
|
|
|
func testContextForHeadOrTail() throws {
|
|
let channel = EmbeddedChannel()
|
|
|
|
defer {
|
|
XCTAssertTrue(try channel.finish().isClean)
|
|
}
|
|
|
|
do {
|
|
_ = try channel.pipeline.context(name: HeadChannelHandler.name).wait()
|
|
XCTFail()
|
|
} catch let err as ChannelPipelineError where err == .notFound {
|
|
/// expected
|
|
}
|
|
|
|
do {
|
|
_ = try channel.pipeline.context(handlerType: HeadChannelHandler.self).wait()
|
|
XCTFail()
|
|
} catch let err as ChannelPipelineError where err == .notFound {
|
|
/// expected
|
|
}
|
|
|
|
do {
|
|
_ = try channel.pipeline.context(name: TailChannelHandler.name).wait()
|
|
XCTFail()
|
|
} catch let err as ChannelPipelineError where err == .notFound {
|
|
/// expected
|
|
}
|
|
|
|
do {
|
|
_ = try channel.pipeline.context(handlerType: TailChannelHandler.self).wait()
|
|
XCTFail()
|
|
} catch let err as ChannelPipelineError where err == .notFound {
|
|
/// expected
|
|
}
|
|
}
|
|
|
|
func testRemoveHeadOrTail() throws {
|
|
let channel = EmbeddedChannel()
|
|
|
|
defer {
|
|
XCTAssertTrue(try channel.finish().isClean)
|
|
}
|
|
|
|
do {
|
|
_ = try channel.pipeline.removeHandler(name: HeadChannelHandler.name).wait()
|
|
XCTFail()
|
|
} catch let err as ChannelPipelineError where err == .notFound {
|
|
/// expected
|
|
}
|
|
|
|
do {
|
|
_ = try channel.pipeline.removeHandler(name: TailChannelHandler.name).wait()
|
|
XCTFail()
|
|
} catch let err as ChannelPipelineError where err == .notFound {
|
|
/// expected
|
|
}
|
|
}
|
|
|
|
func testRemovingByContextWithPromiseStillInChannel() throws {
|
|
class NoOpHandler: ChannelInboundHandler, RemovableChannelHandler {
|
|
typealias InboundIn = Never
|
|
}
|
|
class DummyError: Error { }
|
|
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
// This will definitely throw.
|
|
_ = try? channel.finish()
|
|
}
|
|
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(NoOpHandler()).wait())
|
|
|
|
let context = try assertNoThrowWithValue(channel.pipeline.context(handlerType: NoOpHandler.self).wait())
|
|
|
|
var buffer = channel.allocator.buffer(capacity: 1024)
|
|
buffer.writeStaticString("Hello, world!")
|
|
|
|
let removalPromise = channel.eventLoop.makePromise(of: Void.self)
|
|
removalPromise.futureResult.whenSuccess {
|
|
context.writeAndFlush(NIOAny(buffer), promise: nil)
|
|
context.fireErrorCaught(DummyError())
|
|
}
|
|
|
|
XCTAssertNoThrow(XCTAssertNil(try channel.readOutbound()))
|
|
XCTAssertNoThrow(try channel.throwIfErrorCaught())
|
|
channel.pipeline.removeHandler(context: context, promise: removalPromise)
|
|
|
|
XCTAssertNoThrow(try removalPromise.futureResult.wait())
|
|
guard case .some(.byteBuffer(let receivedBuffer)) = try channel.readOutbound(as: IOData.self) else {
|
|
XCTFail("No buffer")
|
|
return
|
|
}
|
|
XCTAssertEqual(receivedBuffer, buffer)
|
|
|
|
do {
|
|
try channel.throwIfErrorCaught()
|
|
XCTFail("Did not throw")
|
|
} catch is DummyError {
|
|
// expected
|
|
} catch {
|
|
XCTFail("Unexpected error: \(error)")
|
|
}
|
|
}
|
|
|
|
func testRemovingByContextWithFutureNotInChannel() throws {
|
|
class NoOpHandler: ChannelInboundHandler {
|
|
typealias InboundIn = Never
|
|
}
|
|
class DummyError: Error { }
|
|
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
// This will definitely throw.
|
|
XCTAssertTrue(try channel.finish().isClean)
|
|
}
|
|
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(NoOpHandler()).wait())
|
|
|
|
let context = try assertNoThrowWithValue(channel.pipeline.context(handlerType: NoOpHandler.self).wait())
|
|
|
|
var buffer = channel.allocator.buffer(capacity: 1024)
|
|
buffer.writeStaticString("Hello, world!")
|
|
|
|
XCTAssertNoThrow(XCTAssertNil(try channel.readOutbound()))
|
|
XCTAssertNoThrow(try channel.throwIfErrorCaught())
|
|
channel.pipeline.removeHandler(context: context).whenSuccess {
|
|
context.writeAndFlush(NIOAny(buffer), promise: nil)
|
|
context.fireErrorCaught(DummyError())
|
|
}
|
|
XCTAssertNoThrow(XCTAssertNil(try channel.readOutbound()))
|
|
XCTAssertNoThrow(try channel.throwIfErrorCaught())
|
|
}
|
|
|
|
func testRemovingByNameWithPromiseStillInChannel() throws {
|
|
class NoOpHandler: ChannelInboundHandler, RemovableChannelHandler {
|
|
typealias InboundIn = Never
|
|
}
|
|
class DummyError: Error { }
|
|
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
// This will definitely throw.
|
|
_ = try? channel.finish()
|
|
}
|
|
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(NoOpHandler(), name: "TestHandler").wait())
|
|
|
|
let context = try assertNoThrowWithValue(channel.pipeline.context(handlerType: NoOpHandler.self).wait())
|
|
|
|
var buffer = channel.allocator.buffer(capacity: 1024)
|
|
buffer.writeStaticString("Hello, world!")
|
|
|
|
let removalPromise = channel.eventLoop.makePromise(of: Void.self)
|
|
removalPromise.futureResult.map {
|
|
context.writeAndFlush(NIOAny(buffer), promise: nil)
|
|
context.fireErrorCaught(DummyError())
|
|
}.whenFailure {
|
|
XCTFail("unexpected error: \($0)")
|
|
}
|
|
|
|
XCTAssertNoThrow(XCTAssertNil(try channel.readOutbound()))
|
|
XCTAssertNoThrow(try channel.throwIfErrorCaught())
|
|
channel.pipeline.removeHandler(name: "TestHandler", promise: removalPromise)
|
|
|
|
XCTAssertNoThrow(XCTAssertEqual(try channel.readOutbound(), buffer))
|
|
|
|
do {
|
|
try channel.throwIfErrorCaught()
|
|
XCTFail("Did not throw")
|
|
} catch is DummyError {
|
|
// expected
|
|
} catch {
|
|
XCTFail("Unexpected error: \(error)")
|
|
}
|
|
}
|
|
|
|
func testRemovingByNameWithFutureNotInChannel() throws {
|
|
class NoOpHandler: ChannelInboundHandler {
|
|
typealias InboundIn = Never
|
|
}
|
|
class DummyError: Error { }
|
|
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
// This will definitely throw.
|
|
XCTAssertTrue(try channel.finish().isClean)
|
|
}
|
|
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(NoOpHandler(), name: "TestHandler").wait())
|
|
|
|
let context = try assertNoThrowWithValue(channel.pipeline.context(handlerType: NoOpHandler.self).wait())
|
|
|
|
var buffer = channel.allocator.buffer(capacity: 1024)
|
|
buffer.writeStaticString("Hello, world!")
|
|
|
|
XCTAssertNoThrow(XCTAssertNil(try channel.readOutbound()))
|
|
XCTAssertNoThrow(try channel.throwIfErrorCaught())
|
|
channel.pipeline.removeHandler(name: "TestHandler").whenSuccess {
|
|
context.writeAndFlush(NIOAny(buffer), promise: nil)
|
|
context.fireErrorCaught(DummyError())
|
|
}
|
|
XCTAssertNoThrow(XCTAssertNil(try channel.readOutbound()))
|
|
XCTAssertNoThrow(try channel.throwIfErrorCaught())
|
|
}
|
|
|
|
func testRemovingByReferenceWithPromiseStillInChannel() throws {
|
|
class NoOpHandler: ChannelInboundHandler, RemovableChannelHandler {
|
|
typealias InboundIn = Never
|
|
}
|
|
class DummyError: Error { }
|
|
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
// This will definitely throw.
|
|
_ = try? channel.finish()
|
|
}
|
|
|
|
let handler = NoOpHandler()
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait())
|
|
|
|
let context = try assertNoThrowWithValue(channel.pipeline.context(handlerType: NoOpHandler.self).wait())
|
|
|
|
var buffer = channel.allocator.buffer(capacity: 1024)
|
|
buffer.writeStaticString("Hello, world!")
|
|
|
|
let removalPromise = channel.eventLoop.makePromise(of: Void.self)
|
|
removalPromise.futureResult.whenSuccess {
|
|
context.writeAndFlush(NIOAny(buffer), promise: nil)
|
|
context.fireErrorCaught(DummyError())
|
|
}
|
|
|
|
XCTAssertNoThrow(XCTAssertNil(try channel.readOutbound()))
|
|
XCTAssertNoThrow(try channel.throwIfErrorCaught())
|
|
channel.pipeline.removeHandler(handler, promise: removalPromise)
|
|
|
|
XCTAssertNoThrow(XCTAssertEqual(try channel.readOutbound(), buffer))
|
|
|
|
do {
|
|
try channel.throwIfErrorCaught()
|
|
XCTFail("Did not throw")
|
|
} catch is DummyError {
|
|
// expected
|
|
} catch {
|
|
XCTFail("Unexpected error: \(error)")
|
|
}
|
|
}
|
|
|
|
func testRemovingByReferenceWithFutureNotInChannel() throws {
|
|
class NoOpHandler: ChannelInboundHandler, RemovableChannelHandler {
|
|
typealias InboundIn = Never
|
|
}
|
|
class DummyError: Error { }
|
|
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
// This will definitely throw.
|
|
XCTAssertTrue(try channel.finish().isClean)
|
|
}
|
|
|
|
let handler = NoOpHandler()
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait())
|
|
|
|
let context = try assertNoThrowWithValue(channel.pipeline.context(handlerType: NoOpHandler.self).wait())
|
|
|
|
var buffer = channel.allocator.buffer(capacity: 1024)
|
|
buffer.writeStaticString("Hello, world!")
|
|
|
|
XCTAssertNoThrow(XCTAssertNil(try channel.readOutbound()))
|
|
XCTAssertNoThrow(try channel.throwIfErrorCaught())
|
|
channel.pipeline.removeHandler(handler).whenSuccess {
|
|
context.writeAndFlush(NIOAny(buffer), promise: nil)
|
|
context.fireErrorCaught(DummyError())
|
|
}
|
|
XCTAssertNoThrow(XCTAssertNil(try channel.readOutbound()))
|
|
XCTAssertNoThrow(try channel.throwIfErrorCaught())
|
|
}
|
|
|
|
func testFireChannelReadInInactiveChannelDoesNotCrash() throws {
|
|
class FireWhenInactiveHandler: ChannelInboundHandler {
|
|
typealias InboundIn = ()
|
|
typealias InboundOut = ()
|
|
|
|
func channelInactive(context: ChannelHandlerContext) {
|
|
context.fireChannelRead(self.wrapInboundOut(()))
|
|
}
|
|
}
|
|
let handler = FireWhenInactiveHandler()
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try group.syncShutdownGracefully())
|
|
}
|
|
let server = try assertNoThrowWithValue(ServerBootstrap(group: group).bind(host: "127.0.0.1", port: 0).wait())
|
|
defer {
|
|
XCTAssertNoThrow(try server.close().wait())
|
|
}
|
|
let client = try assertNoThrowWithValue(ClientBootstrap(group: group)
|
|
.channelInitializer { channel in
|
|
channel.pipeline.addHandler(handler)
|
|
}
|
|
.connect(to: server.localAddress!)
|
|
.wait())
|
|
XCTAssertNoThrow(try client.close().wait())
|
|
}
|
|
|
|
func testTeardownDuringFormalRemovalProcess() {
|
|
class NeverCompleteRemovalHandler: ChannelInboundHandler, RemovableChannelHandler {
|
|
typealias InboundIn = Never
|
|
|
|
private let removalTokenPromise: EventLoopPromise<ChannelHandlerContext.RemovalToken>
|
|
private let handlerRemovedPromise: EventLoopPromise<Void>
|
|
|
|
init(removalTokenPromise: EventLoopPromise<ChannelHandlerContext.RemovalToken>,
|
|
handlerRemovedPromise: EventLoopPromise<Void>) {
|
|
self.removalTokenPromise = removalTokenPromise
|
|
self.handlerRemovedPromise = handlerRemovedPromise
|
|
}
|
|
|
|
func handlerRemoved(context: ChannelHandlerContext) {
|
|
self.handlerRemovedPromise.succeed(())
|
|
}
|
|
|
|
func removeHandler(context: ChannelHandlerContext, removalToken: ChannelHandlerContext.RemovalToken) {
|
|
self.removalTokenPromise.succeed(removalToken)
|
|
}
|
|
}
|
|
|
|
let eventLoop = EmbeddedEventLoop()
|
|
let removalTokenPromise = eventLoop.makePromise(of: ChannelHandlerContext.RemovalToken.self)
|
|
let handlerRemovedPromise = eventLoop.makePromise(of: Void.self)
|
|
|
|
let channel = EmbeddedChannel(handler: NeverCompleteRemovalHandler(removalTokenPromise: removalTokenPromise,
|
|
handlerRemovedPromise: handlerRemovedPromise),
|
|
loop: eventLoop)
|
|
|
|
// pretend we're real and connect
|
|
XCTAssertNoThrow(try channel.connect(to: .init(ipAddress: "1.2.3.4", port: 5)).wait())
|
|
|
|
// let's trigger the removal process
|
|
XCTAssertNoThrow(try channel.pipeline.context(handlerType: NeverCompleteRemovalHandler.self).map { handler in
|
|
channel.pipeline.removeHandler(context: handler, promise: nil)
|
|
}.wait())
|
|
|
|
XCTAssertNoThrow(try removalTokenPromise.futureResult.map { removalToken in
|
|
// we know that the removal process has been started, so let's tear down the pipeline
|
|
func workaroundSR9815withAUselessFunction() {
|
|
XCTAssertNoThrow(XCTAssertTrue(try channel.finish().isClean))
|
|
}
|
|
workaroundSR9815withAUselessFunction()
|
|
}.wait())
|
|
|
|
// verify that the handler has now been removed, despite the fact it should be mid-removal
|
|
XCTAssertNoThrow(try handlerRemovedPromise.futureResult.wait())
|
|
}
|
|
|
|
func testVariousChannelRemovalAPIsGoThroughRemovableChannelHandler() {
|
|
class Handler: ChannelInboundHandler, RemovableChannelHandler {
|
|
typealias InboundIn = Never
|
|
|
|
var removeHandlerCalled = false
|
|
var withinRemoveHandler = false
|
|
|
|
func removeHandler(context: ChannelHandlerContext, removalToken: ChannelHandlerContext.RemovalToken) {
|
|
self.removeHandlerCalled = true
|
|
self.withinRemoveHandler = true
|
|
defer {
|
|
self.withinRemoveHandler = false
|
|
}
|
|
context.leavePipeline(removalToken: removalToken)
|
|
}
|
|
|
|
func handlerRemoved(context: ChannelHandlerContext) {
|
|
XCTAssertTrue(self.removeHandlerCalled)
|
|
XCTAssertTrue(self.withinRemoveHandler)
|
|
}
|
|
}
|
|
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
XCTAssertNoThrow(XCTAssertTrue(try channel.finish().isClean))
|
|
}
|
|
let allHandlers = [Handler(), Handler(), Handler()]
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(allHandlers[0], name: "the first one to remove").wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(allHandlers[1]).wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(allHandlers[2], name: "the last one to remove").wait())
|
|
|
|
let lastContext = try! channel.pipeline.context(name: "the last one to remove").wait()
|
|
|
|
XCTAssertNoThrow(try channel.pipeline.removeHandler(name: "the first one to remove").wait())
|
|
XCTAssertNoThrow(try channel.pipeline.removeHandler(allHandlers[1]).wait())
|
|
XCTAssertNoThrow(try channel.pipeline.removeHandler(context: lastContext).wait())
|
|
|
|
allHandlers.forEach {
|
|
XCTAssertTrue($0.removeHandlerCalled)
|
|
XCTAssertFalse($0.withinRemoveHandler)
|
|
}
|
|
}
|
|
|
|
func testNonRemovableChannelHandlerIsNotRemovable() {
|
|
class NonRemovableHandler: ChannelInboundHandler {
|
|
typealias InboundIn = Never
|
|
}
|
|
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
XCTAssertNoThrow(XCTAssertTrue(try channel.finish().isClean))
|
|
}
|
|
|
|
let allHandlers = [NonRemovableHandler(), NonRemovableHandler(), NonRemovableHandler()]
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(allHandlers[0], name: "1").wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(allHandlers[1], name: "2").wait())
|
|
|
|
let lastContext = try! channel.pipeline.context(name: "1").wait()
|
|
|
|
XCTAssertThrowsError(try channel.pipeline.removeHandler(name: "2").wait()) { error in
|
|
if let error = error as? ChannelError {
|
|
XCTAssertEqual(ChannelError.unremovableHandler, error)
|
|
} else {
|
|
XCTFail("unexpected error: \(error)")
|
|
}
|
|
}
|
|
XCTAssertThrowsError(try channel.pipeline.removeHandler(context: lastContext).wait()) { error in
|
|
if let error = error as? ChannelError {
|
|
XCTAssertEqual(ChannelError.unremovableHandler, error)
|
|
} else {
|
|
XCTFail("unexpected error: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func testAddMultipleHandlers() {
|
|
typealias Handler = TestAddMultipleHandlersHandlerWorkingAroundSR9956
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
XCTAssertNoThrow(XCTAssertTrue(try channel.finish().isClean))
|
|
}
|
|
let a = Handler()
|
|
let b = Handler()
|
|
let c = Handler()
|
|
let d = Handler()
|
|
let e = Handler()
|
|
let f = Handler()
|
|
let g = Handler()
|
|
let h = Handler()
|
|
let i = Handler()
|
|
let j = Handler()
|
|
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(c).wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(h).wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandlers([d, e], position: .after(c)).wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandlers([f, g], position: .before(h)).wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandlers([a, b], position: .first).wait())
|
|
XCTAssertNoThrow(try channel.pipeline.addHandlers([i, j], position: .last).wait())
|
|
|
|
Handler.allHandlers = []
|
|
channel.pipeline.fireUserInboundEventTriggered(())
|
|
|
|
XCTAssertEqual([a, b, c, d, e, f, g, h, i, j], Handler.allHandlers)
|
|
}
|
|
|
|
func testPipelineDebugDescription() {
|
|
final class HTTPRequestParser: ChannelInboundHandler {
|
|
typealias InboundIn = Never
|
|
}
|
|
final class HTTPResponseSerializer: ChannelOutboundHandler {
|
|
typealias OutboundIn = Never
|
|
}
|
|
final class HTTPHandler: ChannelDuplexHandler {
|
|
typealias InboundIn = Never
|
|
typealias OutboundIn = Never
|
|
}
|
|
let channel = EmbeddedChannel()
|
|
defer {
|
|
XCTAssertNoThrow(XCTAssertTrue(try channel.finish().isClean))
|
|
}
|
|
let parser = HTTPRequestParser()
|
|
let serializer = HTTPResponseSerializer()
|
|
let handler = HTTPHandler()
|
|
XCTAssertEqual(channel.pipeline.debugDescription, """
|
|
ChannelPipeline[\(ObjectIdentifier(channel.pipeline))]:
|
|
<no handlers>
|
|
""")
|
|
XCTAssertNoThrow(try channel.pipeline.addHandlers([parser, serializer, handler]).wait())
|
|
XCTAssertEqual(channel.pipeline.debugDescription, """
|
|
ChannelPipeline[\(ObjectIdentifier(channel.pipeline))]:
|
|
[I] ↓↑ [O]
|
|
HTTPRequestParser ↓↑ [handler0]
|
|
↓↑ HTTPResponseSerializer [handler1]
|
|
HTTPHandler ↓↑ HTTPHandler [handler2]
|
|
""")
|
|
}
|
|
|
|
func testWeDontCallHandlerRemovedTwiceIfAHandlerCompletesRemovalOnlyAfterChannelTeardown() {
|
|
enum State: Int {
|
|
// When we start the test,
|
|
case testStarted = 0
|
|
// we send a trigger event,
|
|
case triggerEventRead = 1
|
|
// when receiving the trigger event, we start the manual removal (which won't complete).
|
|
case manualRemovalStarted = 2
|
|
// Instead, we now close the channel to force a pipeline teardown,
|
|
case pipelineTeardown = 3
|
|
// which will make `handlerRemoved` called, from where
|
|
case handlerRemovedCalled = 4
|
|
// we also complete the manual removal.
|
|
case manualRemovalCompleted = 5
|
|
// And hopefully we never reach the error state.
|
|
case error = 999
|
|
|
|
mutating func next() {
|
|
if let newState = State(rawValue: self.rawValue + 1) {
|
|
self = newState
|
|
} else {
|
|
XCTFail("there's no next state starting from \(self)")
|
|
self = .error
|
|
}
|
|
}
|
|
}
|
|
class Handler: ChannelInboundHandler, RemovableChannelHandler {
|
|
typealias InboundIn = State
|
|
typealias InboundOut = State
|
|
|
|
private(set) var state: State = .testStarted
|
|
private var removalToken: ChannelHandlerContext.RemovalToken? = nil
|
|
private let allDonePromise: EventLoopPromise<Void>
|
|
|
|
init(allDonePromise: EventLoopPromise<Void>) {
|
|
self.allDonePromise = allDonePromise
|
|
}
|
|
|
|
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
|
let step = self.unwrapInboundIn(data)
|
|
self.state.next()
|
|
XCTAssertEqual(self.state, step)
|
|
|
|
// just to communicate to the outside where we are in our state machine
|
|
context.fireChannelRead(self.wrapInboundOut(self.state))
|
|
|
|
switch step {
|
|
case .triggerEventRead:
|
|
// Step 1: Okay, let's kick off the manual removal (it won't complete)
|
|
context.pipeline.removeHandler(self).map {
|
|
// When the manual removal completes, we advance the state.
|
|
self.state.next()
|
|
}.cascade(to: self.allDonePromise)
|
|
default:
|
|
XCTFail("channelRead called in state \(self.state)")
|
|
}
|
|
}
|
|
|
|
func removeHandler(context: ChannelHandlerContext, removalToken: ChannelHandlerContext.RemovalToken) {
|
|
self.state.next()
|
|
XCTAssertEqual(.manualRemovalStarted, self.state)
|
|
|
|
// Step 2: Save the removal token that we got from kicking off the manual removal (in step 1)
|
|
self.removalToken = removalToken
|
|
}
|
|
|
|
func handlerRemoved(context: ChannelHandlerContext) {
|
|
self.state.next()
|
|
XCTAssertEqual(.pipelineTeardown, self.state)
|
|
|
|
// Step 3: We'll call our own channelRead which will advance the state.
|
|
self.completeTheManualRemoval(context: context)
|
|
}
|
|
|
|
func completeTheManualRemoval(context: ChannelHandlerContext) {
|
|
self.state.next()
|
|
XCTAssertEqual(.handlerRemovedCalled, self.state)
|
|
|
|
// just to communicate to the outside where we are in our state machine
|
|
context.fireChannelRead(self.wrapInboundOut(self.state))
|
|
|
|
// Step 4: This happens when the pipeline is being torn down, so let's now also finish the manual
|
|
// removal process.
|
|
self.removalToken.map(context.leavePipeline(removalToken:))
|
|
}
|
|
}
|
|
|
|
let eventLoop = EmbeddedEventLoop()
|
|
let allDonePromise = eventLoop.makePromise(of: Void.self)
|
|
let handler = Handler(allDonePromise: allDonePromise)
|
|
let channel = EmbeddedChannel(handler: handler, loop: eventLoop)
|
|
XCTAssertNoThrow(try channel.connect(to: .init(ipAddress: "1.2.3.4", port: 5)).wait())
|
|
|
|
XCTAssertEqual(.testStarted, handler.state)
|
|
XCTAssertNoThrow(try channel.writeInbound(State.triggerEventRead))
|
|
XCTAssertNoThrow(XCTAssertEqual(State.triggerEventRead, try channel.readInbound()))
|
|
XCTAssertNoThrow(XCTAssertNil(try channel.readInbound()))
|
|
|
|
XCTAssertNoThrow(try {
|
|
// we'll get a left-over event on close which triggers the pipeline teardown and therefore continues the
|
|
// process.
|
|
switch try channel.finish() {
|
|
case .clean:
|
|
XCTFail("expected output")
|
|
case .leftOvers(inbound: let inbound, outbound: let outbound, pendingOutbound: let pendingOutbound):
|
|
XCTAssertEqual(0, outbound.count)
|
|
XCTAssertEqual(0, pendingOutbound.count)
|
|
XCTAssertEqual(1, inbound.count)
|
|
XCTAssertEqual(.handlerRemovedCalled, inbound.first?.tryAs(type: State.self))
|
|
}
|
|
}())
|
|
|
|
XCTAssertEqual(.manualRemovalCompleted, handler.state)
|
|
|
|
XCTAssertNoThrow(try allDonePromise.futureResult.wait())
|
|
}
|
|
|
|
func testWeFailTheSecondRemoval() {
|
|
final class Handler: ChannelInboundHandler, RemovableChannelHandler {
|
|
typealias InboundIn = Never
|
|
|
|
private let removalTriggeredPromise: EventLoopPromise<Void>
|
|
private let continueRemovalFuture: EventLoopFuture<Void>
|
|
private var removeHandlerCalls = 0
|
|
|
|
init(removalTriggeredPromise: EventLoopPromise<Void>,
|
|
continueRemovalFuture: EventLoopFuture<Void>) {
|
|
self.removalTriggeredPromise = removalTriggeredPromise
|
|
self.continueRemovalFuture = continueRemovalFuture
|
|
}
|
|
|
|
func removeHandler(context: ChannelHandlerContext, removalToken: ChannelHandlerContext.RemovalToken) {
|
|
self.removeHandlerCalls += 1
|
|
XCTAssertEqual(1, self.removeHandlerCalls)
|
|
self.removalTriggeredPromise.succeed(())
|
|
self.continueRemovalFuture.whenSuccess {
|
|
context.leavePipeline(removalToken: removalToken)
|
|
}
|
|
}
|
|
}
|
|
|
|
let channel = EmbeddedChannel()
|
|
let removalTriggeredPromise: EventLoopPromise<Void> = channel.eventLoop.makePromise()
|
|
let continueRemovalPromise: EventLoopPromise<Void> = channel.eventLoop.makePromise()
|
|
|
|
let handler = Handler(removalTriggeredPromise: removalTriggeredPromise,
|
|
continueRemovalFuture: continueRemovalPromise.futureResult)
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait())
|
|
let removal1Future = channel.pipeline.removeHandler(handler)
|
|
XCTAssertThrowsError(try channel.pipeline.removeHandler(handler).wait()) { error in
|
|
XCTAssert(error is NIOAttemptedToRemoveHandlerMultipleTimesError,
|
|
"unexpected error: \(error)")
|
|
}
|
|
continueRemovalPromise.succeed(())
|
|
XCTAssertThrowsError(try channel.pipeline.removeHandler(handler).wait()) { error in
|
|
XCTAssertEqual(.notFound, error as? ChannelPipelineError, "unexpected error: \(error)")
|
|
}
|
|
XCTAssertNoThrow(try removal1Future.wait())
|
|
XCTAssertNoThrow(XCTAssertTrue(try channel.finish().isClean))
|
|
}
|
|
}
|
|
|
|
// this should be within `testAddMultipleHandlers` but https://bugs.swift.org/browse/SR-9956
|
|
final class TestAddMultipleHandlersHandlerWorkingAroundSR9956: ChannelDuplexHandler, Equatable {
|
|
typealias InboundIn = Never
|
|
typealias OutboundIn = Never
|
|
|
|
static var allHandlers: [TestAddMultipleHandlersHandlerWorkingAroundSR9956] = []
|
|
|
|
init() {}
|
|
|
|
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
|
|
TestAddMultipleHandlersHandlerWorkingAroundSR9956.allHandlers.append(self)
|
|
context.fireUserInboundEventTriggered(event)
|
|
}
|
|
|
|
public static func == (lhs: TestAddMultipleHandlersHandlerWorkingAroundSR9956,
|
|
rhs: TestAddMultipleHandlersHandlerWorkingAroundSR9956) -> Bool {
|
|
return lhs === rhs
|
|
}
|
|
}
|