I have an XCTest which works with UI components. I tried to open a server socket within the xctext function using SwiftNIO.
I took the echo server example from here. and I simplified, removed the args with hardcoded values for the sake of a dirty test.
import XCTest
import NIOCore
import NIOPosix
private final class EchoHandler: ChannelInboundHandler {
public typealias InboundIn = ByteBuffer
public typealias OutboundOut = ByteBuffer
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
// As we are not really interested getting notified on success or failure we just pass nil as promise to
// reduce allocations.
context.write(data, promise: nil)
}
// Flush it out. This can make use of gathering writes if multiple buffers are pending
public func channelReadComplete(context: ChannelHandlerContext) {
context.flush()
}
public func errorCaught(context: ChannelHandlerContext, error: Error) {
print("error: ", error)
// As we are not really interested getting notified on success or failure we just pass nil as promise to
// reduce allocations.
context.close(promise: nil)
}
}
class MyXCTests: XCTestCase {
var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
// Catch system alerts such as "allow connecting to Wi-fi network"
addUIInterruptionMonitor(withDescription: "System Dialog") { (alert) -> Bool in
alert.buttons["Join"].tap()
return true
}
}
override func tearDownWithError() throws {
}
func testXYZ() throws {
app.launch()
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
let bootstrap = ServerBootstrap(group: group)
// Specify backlog and enable SO_REUSEADDR for the server itself
.serverChannelOption(ChannelOptions.backlog, value: 256)
.serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
// Set the handlers that are appled to the accepted Channels
.childChannelInitializer { channel in
// Ensure we don't read faster than we can write by adding the BackPressureHandler into the pipeline.
channel.pipeline.addHandler(BackPressureHandler()).flatMap { v in
channel.pipeline.addHandler(EchoHandler())
}
}
// Enable SO_REUSEADDR for the accepted Channels
.childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.childChannelOption(ChannelOptions.maxMessagesPerRead, value: 16)
.childChannelOption(ChannelOptions.recvAllocator, value: AdaptiveRecvByteBufferAllocator())
defer {
try! group.syncShutdownGracefully()
}
let channel = try { () -> Channel in
return try bootstrap.bind(host: "0.0.0.0", port: 1234).wait()
}()
print("============= Server started and listening on \(channel.localAddress!)")
// then some XCTest code which works here was cut from this snippet
}
The test runs correctly, it also prints
============= Server started and listening on [IPv4]0.0.0.0/0.0.0.0:1234
But in reality EchoClient from here doesn't work
swift run NIOEchoClient localhost 1234 1785
[0/0] Build complete!
Please enter line to send to the server
dfsdfd
Swift/ErrorType.swift:200: Fatal error: Error raised at top level: NIOPosix.NIOConnectionError(host: "localhost", port: 1234, dnsAError: nil, dnsAAAAError: nil, connectionErrors: [NIOPosix.SingleConnectionFailure(target: [IPv6]localhost/::1:1234, error: connection reset (error set): Connection refused (errno: 61)), NIOPosix.SingleConnectionFailure(target: [IPv4]localhost/127.0.0.1:1234, error: connection reset (error set): Connection refused (errno: 61))])
[1] 28213 trace trap swift run NIOEchoClient localhost 1234
The listening socket also unavailable with
sudo lsof -PiTCP -sTCP:LISTEN
I was also trying UITestEntitlements to set com.apple.security.app-sandbox
to false.
Is there a way to allow server sockets from XCTest?
Originally I was trying to embed a Swift-GRPC endpoint, to allow more finer grained control from a HW in the loop controller. The intent is to start an XCTest using command line xcodebuild, which in turn is starting a long running test, but instead of the test code written in Swift, I would expose the events when to tap some buttons, right outside of the test process through a grpc endpoint.
Since the grpc endpoint didn't worked, I reduced the problem to the one above.
Anybody have a hint, how to pass through this issue, or have a hint why it will never be possible to open server socket within an XCTest app, don't hesitate to reply here.
Yes, that is possible, you can find many examples of this in the AsyncHTTPClient and SwiftNIO test suites.
The reason that yours doesn't work is because you shut down the MultiThreadedEventLoopGroup
right after binding the socket. So essentially you're starting everything up and then you shut it down again.
Also, for unit tests, I'd recommend binding to 127.0.0.1
only because you probably don't want connections from elsewhere. Another good idea is to use an ephemeral port, ie. have the system pick a free, random port automatically. You can achieve this by specifying port 0
. After you bind
the server Channel
you can then interrogate the server channel by using serverChannel.localAddress?.port!
about the port it picked.
Here's a full example with a client and a server in a test case.
import XCTest
import NIO
final class ExampleTests: XCTestCase {
// We keep the `EventLoopGroup` where all the I/O runs alive during the test
private var group: EventLoopGroup!
// Same for the server channel.
private var serverChannel: Channel!
private var serverAddress: SocketAddress {
return self.serverChannel.localAddress!
}
// We set up the server in `setUp`...
override func setUp() {
self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
XCTAssertNoThrow(self.serverChannel = try ServerBootstrap(group: self.group)
.childChannelInitializer { channel in
channel.pipeline.addHandler(UppercasingHandler())
}
.bind(host: "127.0.0.1", port: 0) // port 0 means: pick a free port.
.wait())
}
// ... and tear it down in `tearDown`.
override func tearDown() {
XCTAssertNoThrow(try self.serverChannel?.close().wait())
XCTAssertNoThrow(try self.group?.syncShutdownGracefully())
}
func testExample() throws {
// Here we just bootstrap a little client that sends "Hello world!\n"
let clientChannel = try ClientBootstrap(group: self.group)
.channelInitializer { channel in
channel.pipeline.addHandler(PrintEverythingHandler())
}
.connect(to: self.serverAddress)
.wait()
XCTAssertNoThrow(try clientChannel
.writeAndFlush(ByteBuffer(string: "Hello world!\n"))
.wait())
XCTAssertNoThrow(try clientChannel.closeFuture.wait())
}
}
// Just a handler that uses the C `toupper` function which uppercases characters.
final class UppercasingHandler: ChannelInboundHandler {
typealias InboundIn = ByteBuffer
typealias OutboundOut = ByteBuffer
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let inBuffer = self.unwrapInboundIn(data)
var outBuffer = context.channel.allocator.buffer(capacity: inBuffer.readableBytes)
// Here we just upper case each byte using the C stdlib's `toupper` function.
outBuffer.writeBytes(inBuffer.readableBytesView.map { UInt8(toupper(CInt($0))) })
context.writeAndFlush(self.wrapOutboundOut(outBuffer),
promise: nil)
}
// We want to close the connection on any error really.
func errorCaught(context: ChannelHandlerContext, error: Error) {
print("server: unexpected error \(error), closing")
context.close(promise: nil)
}
}
// This handler just prints everything using the `write` system call. And closes the connection on a newline.
final class PrintEverythingHandler: ChannelInboundHandler {
typealias InboundIn = ByteBuffer
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let inBuffer = self.unwrapInboundIn(data)
guard inBuffer.readableBytes > 0 else {
return
}
// We're using Unsafe* stuff here because we're using the `write` system call, which is a C function.
_ = inBuffer.withUnsafeReadableBytes { ptr in
write(STDOUT_FILENO, ptr.baseAddress!, ptr.count)
}
// If we see a newline, then let's actually close the connection...
if inBuffer.readableBytesView.contains(UInt8(ascii: "\n")) {
print("found newline, closing...")
context.close(promise: nil)
}
}
func errorCaught(context: ChannelHandlerContext, error: Error) {
print("client: unexpected error \(error), closing")
context.close(promise: nil)
}
}