652 lines
30 KiB
Swift
652 lines
30 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the SwiftNIO open source project
|
|
//
|
|
// Copyright (c) 2017-2022 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 NIOConcurrencyHelpers
|
|
import NIOCore
|
|
|
|
/// A `Channel` with fine-grained control for testing.
|
|
///
|
|
/// ``NIOAsyncTestingChannel`` is a `Channel` implementation that does no
|
|
/// actual IO but that does have proper eventing mechanism, albeit one that users can
|
|
/// control. The prime use-case for ``NIOAsyncTestingChannel`` is in unit tests when you
|
|
/// want to feed the inbound events and check the outbound events manually.
|
|
///
|
|
/// Please remember to call ``finish()`` when you are no longer using this
|
|
/// ``NIOAsyncTestingChannel``.
|
|
///
|
|
/// To feed events through an ``NIOAsyncTestingChannel``'s `ChannelPipeline` use
|
|
/// ``NIOAsyncTestingChannel/writeInbound(_:)`` which accepts data of any type. It will then
|
|
/// forward that data through the `ChannelPipeline` and the subsequent
|
|
/// `ChannelInboundHandler` will receive it through the usual `channelRead`
|
|
/// event. The user is responsible for making sure the first
|
|
/// `ChannelInboundHandler` expects data of that type.
|
|
///
|
|
/// Unlike in a regular `ChannelPipeline`, it is expected that the test code will act
|
|
/// as the "network layer", using ``readOutbound(as:)`` to observe the data that the
|
|
/// `Channel` has "written" to the network, and using ``writeInbound(_:)`` to simulate
|
|
/// receiving data from the network. There are also facilities to make it a bit easier
|
|
/// to handle the logic for `write` and `flush` (using ``writeOutbound(_:)``), and to
|
|
/// extract data that passed the whole way along the channel in `channelRead` (using
|
|
/// ``readOutbound(as:)``. Below is a diagram showing the layout of a `ChannelPipeline`
|
|
/// inside a ``NIOAsyncTestingChannel``, including the functions that can be used to
|
|
/// inject and extract data at each end.
|
|
///
|
|
/// ```
|
|
///
|
|
/// Extract data Inject data
|
|
/// using readInbound() using writeOutbound()
|
|
/// ▲ |
|
|
/// +---------------+-----------------------------------+---------------+
|
|
/// | | ChannelPipeline | |
|
|
/// | | TAIL ▼ |
|
|
/// | +---------------------+ +-----------+----------+ |
|
|
/// | | Inbound Handler N | | Outbound Handler 1 | |
|
|
/// | +----------+----------+ +-----------+----------+ |
|
|
/// | ▲ | |
|
|
/// | | ▼ |
|
|
/// | +----------+----------+ +-----------+----------+ |
|
|
/// | | Inbound Handler N-1 | | Outbound Handler 2 | |
|
|
/// | +----------+----------+ +-----------+----------+ |
|
|
/// | ▲ . |
|
|
/// | . . |
|
|
/// | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
|
|
/// | [ method call] [method call] |
|
|
/// | . . |
|
|
/// | . ▼ |
|
|
/// | +----------+----------+ +-----------+----------+ |
|
|
/// | | Inbound Handler 2 | | Outbound Handler M-1 | |
|
|
/// | +----------+----------+ +-----------+----------+ |
|
|
/// | ▲ | |
|
|
/// | | ▼ |
|
|
/// | +----------+----------+ +-----------+----------+ |
|
|
/// | | Inbound Handler 1 | | Outbound Handler M | |
|
|
/// | +----------+----------+ +-----------+----------+ |
|
|
/// | ▲ HEAD | |
|
|
/// +---------------+-----------------------------------+---------------+
|
|
/// | ▼
|
|
/// Inject data Extract data
|
|
/// using writeInbound() using readOutbound()
|
|
/// ```
|
|
///
|
|
/// - note: ``NIOAsyncTestingChannel`` is currently only compatible with
|
|
/// ``NIOAsyncTestingEventLoop``s and cannot be used with `SelectableEventLoop`s from
|
|
/// for example `MultiThreadedEventLoopGroup`.
|
|
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
|
public final class NIOAsyncTestingChannel: Channel {
|
|
/// ``LeftOverState`` represents any left-over inbound, outbound, and pending outbound events that hit the
|
|
/// ``NIOAsyncTestingChannel`` and were not consumed when ``finish()`` was called on the ``NIOAsyncTestingChannel``.
|
|
///
|
|
/// ``NIOAsyncTestingChannel`` is most useful in testing and usually in unit tests, you want to consume all inbound and
|
|
/// outbound data to verify they are what you expect. Therefore, when you ``finish()`` a ``NIOAsyncTestingChannel`` it will
|
|
/// return if it's either ``LeftOverState/clean`` (no left overs) or that it has ``LeftOverState/leftOvers(inbound:outbound:pendingOutbound:)``.
|
|
public enum LeftOverState {
|
|
/// The ``NIOAsyncTestingChannel`` is clean, ie. no inbound, outbound, or pending outbound data left on ``NIOAsyncTestingChannel/finish()``.
|
|
case clean
|
|
|
|
/// The ``NIOAsyncTestingChannel`` has inbound, outbound, or pending outbound data left on ``NIOAsyncTestingChannel/finish()``.
|
|
case leftOvers(inbound: CircularBuffer<NIOAny>, outbound: CircularBuffer<NIOAny>, pendingOutbound: [NIOAny])
|
|
|
|
/// `true` if the ``NIOAsyncTestingChannel`` was `clean` on ``NIOAsyncTestingChannel/finish()``, ie. there is no unconsumed inbound, outbound, or
|
|
/// pending outbound data left on the `Channel`.
|
|
public var isClean: Bool {
|
|
if case .clean = self {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// `true` if the ``NIOAsyncTestingChannel`` if there was unconsumed inbound, outbound, or pending outbound data left
|
|
/// on the `Channel` when it was `finish`ed.
|
|
public var hasLeftOvers: Bool {
|
|
return !self.isClean
|
|
}
|
|
}
|
|
|
|
/// ``BufferState`` represents the state of either the inbound, or the outbound ``NIOAsyncTestingChannel`` buffer.
|
|
///
|
|
/// These buffers contain data that travelled the `ChannelPipeline` all the way through..
|
|
///
|
|
/// If the last `ChannelHandler` explicitly (by calling `fireChannelRead`) or implicitly (by not implementing
|
|
/// `channelRead`) sends inbound data into the end of the ``NIOAsyncTestingChannel``, it will be held in the
|
|
/// ``NIOAsyncTestingChannel``'s inbound buffer. Similarly for `write` on the outbound side. The state of the respective
|
|
/// buffer will be returned from ``writeInbound(_:)``/``writeOutbound(_:)`` as a ``BufferState``.
|
|
public enum BufferState {
|
|
/// The buffer is empty.
|
|
case empty
|
|
|
|
/// The buffer is non-empty.
|
|
case full(CircularBuffer<NIOAny>)
|
|
|
|
/// Returns `true` is the buffer was empty.
|
|
public var isEmpty: Bool {
|
|
if case .empty = self {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if the buffer was non-empty.
|
|
public var isFull: Bool {
|
|
return !self.isEmpty
|
|
}
|
|
}
|
|
|
|
/// ``WrongTypeError`` is thrown if you use ``readInbound(as:)`` or ``readOutbound(as:)`` and request a certain type but the first
|
|
/// item in the respective buffer is of a different type.
|
|
public struct WrongTypeError: Error, Equatable {
|
|
/// The type you expected.
|
|
public let expected: Any.Type
|
|
|
|
/// The type of the actual first element.
|
|
public let actual: Any.Type
|
|
|
|
public init(expected: Any.Type, actual: Any.Type) {
|
|
self.expected = expected
|
|
self.actual = actual
|
|
}
|
|
|
|
public static func == (lhs: WrongTypeError, rhs: WrongTypeError) -> Bool {
|
|
return lhs.expected == rhs.expected && lhs.actual == rhs.actual
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if the ``NIOAsyncTestingChannel`` is 'active'.
|
|
///
|
|
/// An active ``NIOAsyncTestingChannel`` can be closed by calling `close` or ``finish()`` on the ``NIOAsyncTestingChannel``.
|
|
///
|
|
/// - note: An ``NIOAsyncTestingChannel`` starts _inactive_ and can be activated, for example by calling `connect`.
|
|
public var isActive: Bool { return channelcore.isActive }
|
|
|
|
/// - see: `ChannelOptions.Types.AllowRemoteHalfClosureOption`
|
|
public var allowRemoteHalfClosure: Bool {
|
|
get {
|
|
return channelcore.allowRemoteHalfClosure
|
|
}
|
|
set {
|
|
channelcore.allowRemoteHalfClosure = newValue
|
|
}
|
|
}
|
|
|
|
/// - see: `Channel.closeFuture`
|
|
public var closeFuture: EventLoopFuture<Void> { return channelcore.closePromise.futureResult }
|
|
|
|
/// - see: `Channel.allocator`
|
|
public let allocator: ByteBufferAllocator = ByteBufferAllocator()
|
|
|
|
/// - see: `Channel.eventLoop`
|
|
public var eventLoop: EventLoop {
|
|
return self.testingEventLoop
|
|
}
|
|
|
|
/// Returns the ``NIOAsyncTestingEventLoop`` that this ``NIOAsyncTestingChannel`` uses. This will return the same instance as
|
|
/// ``NIOAsyncTestingChannel/eventLoop`` but as the concrete ``NIOAsyncTestingEventLoop`` rather than as `EventLoop` existential.
|
|
public let testingEventLoop: NIOAsyncTestingEventLoop
|
|
|
|
/// `nil` because ``NIOAsyncTestingChannel``s don't have parents.
|
|
public let parent: Channel? = nil
|
|
|
|
// This is only written once, from a single thread, and never written again, so it's _technically_ thread-safe. Most methods cannot safely
|
|
// be used from multiple threads, but `isActive`, `isOpen`, `eventLoop`, and `closeFuture` can all safely be used from any thread. Just.
|
|
@usableFromInline
|
|
/*private but usableFromInline */ var channelcore: EmbeddedChannelCore!
|
|
|
|
/// Guards any of the getters/setters that can be accessed from any thread.
|
|
private let stateLock: NIOLock = NIOLock()
|
|
|
|
// Guarded by `stateLock`
|
|
private var _isWritable: Bool = true
|
|
|
|
// Guarded by `stateLock`
|
|
private var _localAddress: SocketAddress? = nil
|
|
|
|
// Guarded by `stateLock`
|
|
private var _remoteAddress: SocketAddress? = nil
|
|
|
|
private var _pipeline: ChannelPipeline!
|
|
|
|
/// - see: `Channel._channelCore`
|
|
public var _channelCore: ChannelCore {
|
|
return channelcore
|
|
}
|
|
|
|
/// - see: `Channel.pipeline`
|
|
public var pipeline: ChannelPipeline {
|
|
return _pipeline
|
|
}
|
|
|
|
/// - see: `Channel.isWritable`
|
|
public var isWritable: Bool {
|
|
get {
|
|
return self.stateLock.withLock { self._isWritable }
|
|
}
|
|
set {
|
|
self.stateLock.withLock { () -> Void in
|
|
self._isWritable = newValue
|
|
}
|
|
}
|
|
}
|
|
|
|
/// - see: `Channel.localAddress`
|
|
public var localAddress: SocketAddress? {
|
|
get {
|
|
return self.stateLock.withLock { self._localAddress }
|
|
}
|
|
set {
|
|
self.stateLock.withLock { () -> Void in
|
|
self._localAddress = newValue
|
|
}
|
|
}
|
|
}
|
|
|
|
/// - see: `Channel.remoteAddress`
|
|
public var remoteAddress: SocketAddress? {
|
|
get {
|
|
return self.stateLock.withLock { self._remoteAddress }
|
|
}
|
|
set {
|
|
self.stateLock.withLock { () -> Void in
|
|
self._remoteAddress = newValue
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create a new instance.
|
|
///
|
|
/// During creation it will automatically also register itself on the ``NIOAsyncTestingEventLoop``.
|
|
///
|
|
/// - parameters:
|
|
/// - loop: The ``NIOAsyncTestingEventLoop`` to use.
|
|
public init(loop: NIOAsyncTestingEventLoop = NIOAsyncTestingEventLoop()) {
|
|
self.testingEventLoop = loop
|
|
self._pipeline = ChannelPipeline(channel: self)
|
|
self.channelcore = EmbeddedChannelCore(pipeline: self._pipeline, eventLoop: self.eventLoop)
|
|
}
|
|
|
|
/// Create a new instance.
|
|
///
|
|
/// During creation it will automatically also register itself on the ``NIOAsyncTestingEventLoop``.
|
|
///
|
|
/// - parameters:
|
|
/// - handler: The `ChannelHandler` to add to the `ChannelPipeline` before register.
|
|
/// - loop: The ``NIOAsyncTestingEventLoop`` to use.
|
|
public convenience init(handler: ChannelHandler, loop: NIOAsyncTestingEventLoop = NIOAsyncTestingEventLoop()) async {
|
|
await self.init(handlers: [handler], loop: loop)
|
|
}
|
|
|
|
/// Create a new instance.
|
|
///
|
|
/// During creation it will automatically also register itself on the ``NIOAsyncTestingEventLoop``.
|
|
///
|
|
/// - parameters:
|
|
/// - handlers: The `ChannelHandler`s to add to the `ChannelPipeline` before register.
|
|
/// - loop: The ``NIOAsyncTestingEventLoop`` to use.
|
|
public convenience init(handlers: [ChannelHandler], loop: NIOAsyncTestingEventLoop = NIOAsyncTestingEventLoop()) async {
|
|
self.init(loop: loop)
|
|
|
|
try! await self._pipeline.addHandlers(handlers)
|
|
|
|
// This will never throw...
|
|
try! await self.register()
|
|
}
|
|
|
|
/// Asynchronously closes the ``NIOAsyncTestingChannel``.
|
|
///
|
|
/// Errors in the ``NIOAsyncTestingChannel`` can be consumed using ``throwIfErrorCaught()``.
|
|
///
|
|
/// - parameters:
|
|
/// - acceptAlreadyClosed: Whether ``finish()`` should throw if the ``NIOAsyncTestingChannel`` has been previously `close`d.
|
|
/// - returns: The ``LeftOverState`` of the ``NIOAsyncTestingChannel``. If all the inbound and outbound events have been
|
|
/// consumed (using ``readInbound(as:)`` / ``readOutbound(as:)``) and there are no pending outbound events (unflushed
|
|
/// writes) this will be ``LeftOverState/clean``. If there are any unconsumed inbound, outbound, or pending outbound
|
|
/// events, the ``NIOAsyncTestingChannel`` will returns those as ``LeftOverState/leftOvers(inbound:outbound:pendingOutbound:)``.
|
|
public func finish(acceptAlreadyClosed: Bool) async throws -> LeftOverState {
|
|
do {
|
|
try await self.close().get()
|
|
} catch let error as ChannelError {
|
|
guard error == .alreadyClosed && acceptAlreadyClosed else {
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// This can never actually throw.
|
|
try! await self.testingEventLoop.executeInContext {
|
|
self.testingEventLoop.drainScheduledTasksByRunningAllCurrentlyScheduledTasks()
|
|
}
|
|
await self.testingEventLoop.run()
|
|
try await throwIfErrorCaught()
|
|
|
|
// This can never actually throw.
|
|
return try! await self.testingEventLoop.executeInContext {
|
|
let c = self.channelcore!
|
|
if c.outboundBuffer.isEmpty && c.inboundBuffer.isEmpty && c.pendingOutboundBuffer.isEmpty {
|
|
return .clean
|
|
} else {
|
|
return .leftOvers(inbound: c.inboundBuffer,
|
|
outbound: c.outboundBuffer,
|
|
pendingOutbound: c.pendingOutboundBuffer.map { $0.0 })
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Asynchronously closes the ``NIOAsyncTestingChannel``.
|
|
///
|
|
/// This method will throw if the `Channel` hit any unconsumed errors or if the `close` fails. Errors in the
|
|
/// ``NIOAsyncTestingChannel`` can be consumed using ``throwIfErrorCaught()``.
|
|
///
|
|
/// - returns: The ``LeftOverState`` of the ``NIOAsyncTestingChannel``. If all the inbound and outbound events have been
|
|
/// consumed (using ``readInbound(as:)`` / ``readOutbound(as:)``) and there are no pending outbound events (unflushed
|
|
/// writes) this will be ``LeftOverState/clean``. If there are any unconsumed inbound, outbound, or pending outbound
|
|
/// events, the ``NIOAsyncTestingChannel`` will returns those as ``LeftOverState/leftOvers(inbound:outbound:pendingOutbound:)``.
|
|
public func finish() async throws -> LeftOverState {
|
|
return try await self.finish(acceptAlreadyClosed: false)
|
|
}
|
|
|
|
/// If available, this method reads one element of type `T` out of the ``NIOAsyncTestingChannel``'s outbound buffer. If the
|
|
/// first element was of a different type than requested, ``WrongTypeError`` will be thrown, if there
|
|
/// are no elements in the outbound buffer, `nil` will be returned.
|
|
///
|
|
/// Data hits the ``NIOAsyncTestingChannel``'s outbound buffer when data was written using `write`, then `flush`ed, and
|
|
/// then travelled the `ChannelPipeline` all the way to the front. For data to hit the outbound buffer, the very
|
|
/// first `ChannelHandler` must have written and flushed it either explicitly (by calling
|
|
/// `ChannelHandlerContext.write` and `flush`) or implicitly by not implementing `write`/`flush`.
|
|
///
|
|
/// - note: Outbound events travel the `ChannelPipeline` _back to front_.
|
|
/// - note: ``NIOAsyncTestingChannel/writeOutbound(_:)`` will `write` data through the `ChannelPipeline`, starting with last
|
|
/// `ChannelHandler`.
|
|
@inlinable
|
|
public func readOutbound<T: Sendable>(as type: T.Type = T.self) async throws -> T? {
|
|
try await self.testingEventLoop.executeInContext {
|
|
try self._readFromBuffer(buffer: &self.channelcore.outboundBuffer)
|
|
}
|
|
}
|
|
|
|
/// This method is similar to ``NIOAsyncTestingChannel/readOutbound(as:)`` but will wait if the outbound buffer is empty.
|
|
/// If available, this method reads one element of type `T` out of the ``NIOAsyncTestingChannel``'s outbound buffer. If the
|
|
/// first element was of a different type than requested, ``WrongTypeError`` will be thrown, if there
|
|
/// are no elements in the outbound buffer, `nil` will be returned.
|
|
///
|
|
/// Data hits the ``NIOAsyncTestingChannel``'s outbound buffer when data was written using `write`, then `flush`ed, and
|
|
/// then travelled the `ChannelPipeline` all the way to the front. For data to hit the outbound buffer, the very
|
|
/// first `ChannelHandler` must have written and flushed it either explicitly (by calling
|
|
/// `ChannelHandlerContext.write` and `flush`) or implicitly by not implementing `write`/`flush`.
|
|
///
|
|
/// - note: Outbound events travel the `ChannelPipeline` _back to front_.
|
|
/// - note: ``NIOAsyncTestingChannel/writeOutbound(_:)`` will `write` data through the `ChannelPipeline`, starting with last
|
|
/// `ChannelHandler`.
|
|
public func waitForOutboundWrite<T: Sendable>(as type: T.Type = T.self) async throws -> T {
|
|
try await withCheckedThrowingContinuation { continuation in
|
|
self.testingEventLoop.execute {
|
|
do {
|
|
if let element: T = try self._readFromBuffer(buffer: &self.channelcore.outboundBuffer) {
|
|
continuation.resume(returning: element)
|
|
return
|
|
}
|
|
self.channelcore.outboundBufferConsumer.append { element in
|
|
continuation.resume(with: Result {
|
|
try self._cast(element)
|
|
})
|
|
}
|
|
} catch {
|
|
continuation.resume(throwing: error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// If available, this method reads one element of type `T` out of the ``NIOAsyncTestingChannel``'s inbound buffer. If the
|
|
/// first element was of a different type than requested, ``WrongTypeError`` will be thrown, if there
|
|
/// are no elements in the outbound buffer, `nil` will be returned.
|
|
///
|
|
/// Data hits the ``NIOAsyncTestingChannel``'s inbound buffer when data was send through the pipeline using `fireChannelRead`
|
|
/// and then travelled the `ChannelPipeline` all the way to the back. For data to hit the inbound buffer, the
|
|
/// last `ChannelHandler` must have send the event either explicitly (by calling
|
|
/// `ChannelHandlerContext.fireChannelRead`) or implicitly by not implementing `channelRead`.
|
|
///
|
|
/// - note: ``NIOAsyncTestingChannel/writeInbound(_:)`` will fire data through the `ChannelPipeline` using `fireChannelRead`.
|
|
@inlinable
|
|
public func readInbound<T: Sendable>(as type: T.Type = T.self) async throws -> T? {
|
|
try await self.testingEventLoop.executeInContext {
|
|
try self._readFromBuffer(buffer: &self.channelcore.inboundBuffer)
|
|
}
|
|
}
|
|
|
|
/// This method is similar to ``NIOAsyncTestingChannel/readInbound(as:)`` but will wait if the inbound buffer is empty.
|
|
/// If available, this method reads one element of type `T` out of the ``NIOAsyncTestingChannel``'s inbound buffer. If the
|
|
/// first element was of a different type than requested, ``WrongTypeError`` will be thrown, if there
|
|
/// are no elements in the outbound buffer, this method will wait until an element is in the inbound buffer.
|
|
///
|
|
/// Data hits the ``NIOAsyncTestingChannel``'s inbound buffer when data was send through the pipeline using `fireChannelRead`
|
|
/// and then travelled the `ChannelPipeline` all the way to the back. For data to hit the inbound buffer, the
|
|
/// last `ChannelHandler` must have send the event either explicitly (by calling
|
|
/// `ChannelHandlerContext.fireChannelRead`) or implicitly by not implementing `channelRead`.
|
|
///
|
|
/// - note: ``NIOAsyncTestingChannel/writeInbound(_:)`` will fire data through the `ChannelPipeline` using `fireChannelRead`.
|
|
public func waitForInboundWrite<T: Sendable>(as type: T.Type = T.self) async throws -> T {
|
|
try await withCheckedThrowingContinuation { continuation in
|
|
self.testingEventLoop.execute {
|
|
do {
|
|
if let element: T = try self._readFromBuffer(buffer: &self.channelcore.inboundBuffer) {
|
|
continuation.resume(returning: element)
|
|
return
|
|
}
|
|
self.channelcore.inboundBufferConsumer.append { element in
|
|
continuation.resume(with: Result {
|
|
try self._cast(element)
|
|
})
|
|
}
|
|
} catch {
|
|
continuation.resume(throwing: error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sends an inbound `channelRead` event followed by a `channelReadComplete` event through the `ChannelPipeline`.
|
|
///
|
|
/// The immediate effect being that the first `ChannelInboundHandler` will get its `channelRead` method called
|
|
/// with the data you provide.
|
|
///
|
|
/// - parameters:
|
|
/// - data: The data to fire through the pipeline.
|
|
/// - returns: The state of the inbound buffer which contains all the events that travelled the `ChannelPipeline`
|
|
// all the way.
|
|
@inlinable
|
|
@discardableResult public func writeInbound<T: Sendable>(_ data: T) async throws -> BufferState {
|
|
try await self.testingEventLoop.executeInContext {
|
|
self.pipeline.fireChannelRead(NIOAny(data))
|
|
self.pipeline.fireChannelReadComplete()
|
|
try self._throwIfErrorCaught()
|
|
return self.channelcore.inboundBuffer.isEmpty ? .empty : .full(self.channelcore.inboundBuffer)
|
|
}
|
|
}
|
|
|
|
/// Sends an outbound `writeAndFlush` event through the `ChannelPipeline`.
|
|
///
|
|
/// The immediate effect being that the first `ChannelOutboundHandler` will get its `write` method called
|
|
/// with the data you provide. Note that the first `ChannelOutboundHandler` in the pipeline is the _last_ handler
|
|
/// because outbound events travel the pipeline from back to front.
|
|
///
|
|
/// - parameters:
|
|
/// - data: The data to fire through the pipeline.
|
|
/// - returns: The state of the outbound buffer which contains all the events that travelled the `ChannelPipeline`
|
|
// all the way.
|
|
@inlinable
|
|
@discardableResult public func writeOutbound<T: Sendable>(_ data: T) async throws -> BufferState {
|
|
try await self.writeAndFlush(NIOAny(data))
|
|
|
|
return try await self.testingEventLoop.executeInContext {
|
|
return self.channelcore.outboundBuffer.isEmpty ? .empty : .full(self.channelcore.outboundBuffer)
|
|
}
|
|
}
|
|
|
|
/// This method will throw the error that is stored in the ``NIOAsyncTestingChannel`` if any.
|
|
///
|
|
/// The ``NIOAsyncTestingChannel`` will store an error if some error travels the `ChannelPipeline` all the way past its end.
|
|
public func throwIfErrorCaught() async throws {
|
|
try await self.testingEventLoop.executeInContext {
|
|
try self._throwIfErrorCaught()
|
|
}
|
|
}
|
|
|
|
@usableFromInline
|
|
func _throwIfErrorCaught() throws {
|
|
self.testingEventLoop.preconditionInEventLoop()
|
|
if let error = self.channelcore.error {
|
|
self.channelcore.error = nil
|
|
throw error
|
|
}
|
|
}
|
|
|
|
|
|
@inlinable
|
|
func _readFromBuffer<T>(buffer: inout CircularBuffer<NIOAny>) throws -> T? {
|
|
self.testingEventLoop.preconditionInEventLoop()
|
|
|
|
if buffer.isEmpty {
|
|
return nil
|
|
}
|
|
return try self._cast(buffer.removeFirst(), to: T.self)
|
|
}
|
|
|
|
@inlinable
|
|
func _cast<T>(_ element: NIOAny, to: T.Type = T.self) throws -> T {
|
|
guard let t = self._channelCore.tryUnwrapData(element, as: T.self) else {
|
|
throw WrongTypeError(expected: T.self, actual: type(of: self._channelCore.tryUnwrapData(element, as: Any.self)!))
|
|
}
|
|
return t
|
|
}
|
|
|
|
/// - see: `Channel.setOption`
|
|
@inlinable
|
|
public func setOption<Option: ChannelOption>(_ option: Option, value: Option.Value) -> EventLoopFuture<Void> {
|
|
if self.eventLoop.inEventLoop {
|
|
self.setOptionSync(option, value: value)
|
|
return self.eventLoop.makeSucceededVoidFuture()
|
|
} else {
|
|
return self.eventLoop.submit { self.setOptionSync(option, value: value) }
|
|
}
|
|
}
|
|
|
|
@inlinable
|
|
internal func setOptionSync<Option: ChannelOption>(_ option: Option, value: Option.Value) {
|
|
if option is ChannelOptions.Types.AllowRemoteHalfClosureOption {
|
|
self.allowRemoteHalfClosure = value as! Bool
|
|
return
|
|
}
|
|
// No other options supported
|
|
fatalError("option not supported")
|
|
}
|
|
|
|
/// - see: `Channel.getOption`
|
|
@inlinable
|
|
public func getOption<Option: ChannelOption>(_ option: Option) -> EventLoopFuture<Option.Value> {
|
|
if self.eventLoop.inEventLoop {
|
|
return self.eventLoop.makeSucceededFuture(self.getOptionSync(option))
|
|
} else {
|
|
return self.eventLoop.submit { self.getOptionSync(option) }
|
|
}
|
|
}
|
|
|
|
@inlinable
|
|
internal func getOptionSync<Option: ChannelOption>(_ option: Option) -> Option.Value {
|
|
if option is ChannelOptions.Types.AutoReadOption {
|
|
return true as! Option.Value
|
|
}
|
|
if option is ChannelOptions.Types.AllowRemoteHalfClosureOption {
|
|
return self.allowRemoteHalfClosure as! Option.Value
|
|
}
|
|
fatalError("option \(option) not supported")
|
|
}
|
|
|
|
/// Fires the (outbound) `bind` event through the `ChannelPipeline`. If the event hits the ``NIOAsyncTestingChannel`` which
|
|
/// happens when it travels the `ChannelPipeline` all the way to the front, this will also set the
|
|
/// ``NIOAsyncTestingChannel``'s ``localAddress``.
|
|
///
|
|
/// - parameters:
|
|
/// - address: The address to fake-bind to.
|
|
/// - promise: The `EventLoopPromise` which will be fulfilled when the fake-bind operation has been done.
|
|
public func bind(to address: SocketAddress, promise: EventLoopPromise<Void>?) {
|
|
promise?.futureResult.whenSuccess {
|
|
self.localAddress = address
|
|
}
|
|
if self.eventLoop.inEventLoop {
|
|
self.pipeline.bind(to: address, promise: promise)
|
|
} else {
|
|
self.eventLoop.execute {
|
|
self.pipeline.bind(to: address, promise: promise)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Fires the (outbound) `connect` event through the `ChannelPipeline`. If the event hits the ``NIOAsyncTestingChannel``
|
|
/// which happens when it travels the `ChannelPipeline` all the way to the front, this will also set the
|
|
/// ``NIOAsyncTestingChannel``'s ``remoteAddress``.
|
|
///
|
|
/// - parameters:
|
|
/// - address: The address to fake-bind to.
|
|
/// - promise: The `EventLoopPromise` which will be fulfilled when the fake-bind operation has been done.
|
|
public func connect(to address: SocketAddress, promise: EventLoopPromise<Void>?) {
|
|
promise?.futureResult.whenSuccess {
|
|
self.remoteAddress = address
|
|
}
|
|
if self.eventLoop.inEventLoop {
|
|
self.pipeline.connect(to: address, promise: promise)
|
|
} else {
|
|
self.eventLoop.execute {
|
|
self.pipeline.connect(to: address, promise: promise)
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct SynchronousOptions: NIOSynchronousChannelOptions {
|
|
@usableFromInline
|
|
internal let channel: NIOAsyncTestingChannel
|
|
|
|
fileprivate init(channel: NIOAsyncTestingChannel) {
|
|
self.channel = channel
|
|
}
|
|
|
|
@inlinable
|
|
public func setOption<Option: ChannelOption>(_ option: Option, value: Option.Value) throws {
|
|
self.channel.eventLoop.preconditionInEventLoop()
|
|
self.channel.setOptionSync(option, value: value)
|
|
}
|
|
|
|
@inlinable
|
|
public func getOption<Option: ChannelOption>(_ option: Option) throws -> Option.Value {
|
|
self.channel.eventLoop.preconditionInEventLoop()
|
|
return self.channel.getOptionSync(option)
|
|
}
|
|
}
|
|
|
|
public final var syncOptions: NIOSynchronousChannelOptions? {
|
|
return SynchronousOptions(channel: self)
|
|
}
|
|
}
|
|
|
|
// MARK: Unchecked sendable
|
|
//
|
|
// Both of these types are unchecked Sendable because strictly, they aren't. This is
|
|
// because they contain NIOAny, a non-Sendable type. In this instance, we tolerate the moving
|
|
// of this object across threads because in the overwhelming majority of cases the data types
|
|
// in a channel pipeline _are_ `Sendable`, and because these objects only carry NIOAnys in cases
|
|
// where the `Channel` itself no longer holds a reference to these objects.
|
|
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
|
extension NIOAsyncTestingChannel.LeftOverState: @unchecked Sendable { }
|
|
|
|
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
|
|
extension NIOAsyncTestingChannel.BufferState: @unchecked Sendable { }
|