【问题标题】:是否可以在 XCTest 测试应用程序中打开基于 SwiftNIO 的服务器套接字?
【发布时间】:2022-01-23 13:10:24
【问题描述】:

我有一个可与 UI 组件配合使用的 XCTest。我尝试使用 SwiftNIO 在 xctext 函数中打开一个服务器套接字。

我采用了 echo 服务器示例 from here。为了进行肮脏的测试,我简化并删除了带有硬编码值的参数。

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
}

测试运行正确,它也打印

============= Server started and listening on [IPv4]0.0.0.0/0.0.0.0:1234

但实际上 EchoClient from here 不起作用

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

监听套接字也无法使用

sudo lsof -PiTCP -sTCP:LISTEN

我也在尝试 UITestEntitlements 将 com.apple.security.app-sandbox 设置为 false。

有没有办法允许来自 XCTest 的服务器套接字?

最初我试图嵌入一个 Swift-GRPC 端点,以允许循环控制器中的硬件进行更细粒度的控制。目的是使用命令行 xcodebuild 启动 XCTest,这反过来又启动了一个长时间运行的测试,但不是用 Swift 编写的测试代码,而是在测试过程之外通过一个 grpc 端点。

由于 grpc 端点不起作用,我将问题归结为上述问题。

任何人有提示,如何解决这个问题,或者提示为什么永远无法在 XCTest 应用程序中打开服务器套接字,请不要犹豫,在这里回复。

【问题讨论】:

    标签: ios swift xctest swift-nio


    【解决方案1】:

    是的,这是可能的,您可以在 AsyncHTTPClient 和 SwiftNIO 测试套件中找到很多这样的示例。

    您的不起作用的原因是因为您在绑定套接字后立即关闭了MultiThreadedEventLoopGroup。所以本质上你是在启动一切,然后又将其关闭。

    另外,对于单元测试,我建议绑定到127.0.0.1,只是因为您可能不希望从其他地方连接。另一个好主意是使用临时端口,即。让系统自动选择一个免费的随机端口。您可以通过指定端口0 来实现此目的。在您bind 服务器Channel 之后,您可以使用serverChannel.localAddress?.port! 询问服务器通道关于它选择的端口。

    这是一个完整的示例,其中包含测试用例中的客户端和服务器。

    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)
        }
    }
    

    【讨论】:

    • 谢谢你的好例子。确实,过早关闭小组在我这边是一个错误。我复制了你的代码,客户端连接可以访问服务器套接字,但是如果我想从主机连接,从控制台连接,或者只是列表,同样的情况。就我而言,我必须绑定到 0.0.0.0 并从另一台机器连接。在我看来,这个问题与沙箱有关,我可以从同一个测试应用程序连接到套接字,但不能从外部连接。您可以尝试从外部访问套接字吗?为测试添加一个大睡眠,不要这么快关闭。
    • 现在我看到这个 XCTest 代码没有在我的 Xcode 中运行,而是以某种方式注入到 iPad 中。插座在 iPad 中打开。这里碰到了这个问题stackoverflow.com/questions/21018736/…
    猜你喜欢
    • 2017-09-28
    • 2020-02-24
    • 1970-01-01
    • 2017-12-14
    • 1970-01-01
    • 1970-01-01
    • 2014-01-28
    • 2015-03-15
    • 1970-01-01
    相关资源
    最近更新 更多