Skip to content

Commit

Permalink
Create IOSSHCommandTests
Browse files Browse the repository at this point in the history
  • Loading branch information
gaetanzanella committed Oct 16, 2023
1 parent efb94a3 commit cef527e
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 105 deletions.
24 changes: 13 additions & 11 deletions Sources/SSHClient/Internal/Command/SSHCommandSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ private class SSHCommandHandler: ChannelDuplexHandler {
private let invocation: SSHCommandInvocation
private let promise: Promise<Void>

private var isSuccess = false

// MARK: - Life Cycle

init(invocation: SSHCommandInvocation,
Expand All @@ -53,34 +55,34 @@ private class SSHCommandHandler: ChannelDuplexHandler {
self.promise = promise
}

deinit {
promise.fail(SSHConnectionError.unknown)
}

func handlerAdded(context: ChannelHandlerContext) {
let execRequest = SSHChannelRequestEvent.ExecRequest(
command: invocation.command.command,
wantReply: true
)
context
.channel
.setOption(ChannelOptions.allowRemoteHalfClosure, value: true)
.flatMap {
// .setOption(ChannelOptions.allowRemoteHalfClosure, value: true)
.eventLoop.flatSubmit {
context.triggerUserOutboundEvent(execRequest)
}
.whenFailure { _ in
context.close(promise: nil)
context.channel.close(promise: nil)
}
}

func errorCaught(context: ChannelHandlerContext, error: Error) {
context.channel.close(promise: nil)
promise.fail(SSHConnectionError.unknown)
context.fireErrorCaught(error)
}

func handlerRemoved(context: ChannelHandlerContext) {
promise.succeed(())
func channelInactive(context: ChannelHandlerContext) {
if isSuccess {
promise.succeed(())
} else {
promise.fail(SSHShellError.unknown)
}
context.fireChannelInactive()
}

func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
Expand All @@ -93,6 +95,7 @@ private class SSHCommandHandler: ChannelDuplexHandler {
case let event as ChannelEvent:
switch event {
case .inputClosed:
isSuccess = true
context.channel.close(promise: nil)
case .outputClosed:
break
Expand All @@ -115,7 +118,6 @@ private class SSHCommandHandler: ChannelDuplexHandler {
switch channelData.type {
case .channel:
invocation.onChunk?(.init(channel: .standard, data: data))
return
case .stdErr:
invocation.onChunk?(.init(channel: .error, data: data))
default:
Expand Down
2 changes: 2 additions & 0 deletions Sources/SSHClient/Internal/Shell/IOSSHShell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,5 @@ class IOSSHShell {
}
}
}

extension IOSSHShell: SSHSession {}
6 changes: 3 additions & 3 deletions Sources/SSHClient/Internal/Shell/SSHShellHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ class StartShellHandler: ChannelInboundHandler {
_ = context
.channel
.eventLoop
// TODO (gz): Move option to bootstrapper
// https://forums.swift.org/t/unit-testing-channeloptions/51797
// .setOption(ChannelOptions.allowRemoteHalfClosure, value: true)
// TODO: (gz): Move option to bootstrapper
// https://forums.swift.org/t/unit-testing-channeloptions/51797
// .setOption(ChannelOptions.allowRemoteHalfClosure, value: true)
.flatSubmit {
let promise = context.channel.eventLoop.makePromise(of: Void.self)
let request = SSHChannelRequestEvent.ShellRequest(wantReply: true)
Expand Down
10 changes: 5 additions & 5 deletions Sources/SSHClient/SSHCommand.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@

import Foundation

public struct SSHCommandStatus: Sendable {
public struct SSHCommandStatus: Sendable, Hashable {
public let exitStatus: Int
}

public enum SSHCommandResponseChunk: Sendable {
public enum SSHCommandResponseChunk: Sendable, Hashable {
case chunk(SSHCommandChunk)
case status(SSHCommandStatus)
}

public struct SSHCommandChunk: Sendable {
public enum Channel: Sendable {
public struct SSHCommandChunk: Sendable, Hashable {
public enum Channel: Sendable, Hashable {
case standard
case error
}
Expand All @@ -20,7 +20,7 @@ public struct SSHCommandChunk: Sendable {
public let data: Data
}

public struct SSHCommand: Sendable {
public struct SSHCommand: Sendable, Hashable {
public let command: String

public init(_ command: String) {
Expand Down
113 changes: 113 additions & 0 deletions Tests/SSHClientTests/Unit/IOSSHCommandTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@

import Foundation
import NIOCore
import NIOEmbedded
import NIOSSH
@testable import SSHClient
import XCTest

class IOSSHCommandTests: XCTestCase {
func testSimpleInvocation() throws {
let context = IOSSHCommandTestsContext(command: SSHCommand("echo"))
let future = try context.assertStart()
try context.serverEnd()
XCTAssertNoThrow(try future.wait())
}

func testRegularInvocation() throws {
let context = IOSSHCommandTestsContext(command: SSHCommand("echo main"))
let future = try context.assertStart()
var isCompleted = false
future.whenComplete { _ in isCompleted = true }
let data = context.harness.channel.triggerInboundChannelString("main")
XCTAssertEqual(
[SSHCommandChunk(channel: .standard, data: data)],
context.chunks
)
let error = context.harness.channel.triggerInboundSTDErrString("error")
XCTAssertEqual(
[
SSHCommandChunk(channel: .standard, data: data),
SSHCommandChunk(channel: .error, data: error),
],
context.chunks
)
let exitStatus = SSHChannelRequestEvent.ExitStatus(exitStatus: 1)
context.harness.channel.triggerInbound(exitStatus)
XCTAssertEqual(
[SSHCommandStatus(exitStatus: exitStatus.exitStatus)],
context.status
)
XCTAssertFalse(isCompleted)
try context.serverEnd()
XCTAssertNoThrow(try future.wait())
}

func testChannelClosingOnInputClosed() throws {
let context = IOSSHCommandTestsContext(command: SSHCommand("echo"))
let future = try context.assertStart()
try context.harness.channel.close().wait()
context.harness.run()
XCTAssertThrowsError(try future.wait())
}

func testChannelClosingOnError() throws {
let context = IOSSHCommandTestsContext(command: SSHCommand("echo"))
let future = try context.assertStart()
context.harness.channel.fireErrorCaught()
context.harness.run()
XCTAssertThrowsError(try future.wait())
}

func testChannelClosingOnOutboundFailure() throws {
let context = IOSSHCommandTestsContext(command: SSHCommand("echo"))
context.harness.channel.shouldFailOnOutboundEvent = true
try context.harness.channel.connect().wait()
let futur = try context.harness.start(context.session)
context.harness.run()
XCTAssertThrowsError(try futur.wait())
}
}

private class IOSSHCommandTestsContext {
private(set) var invocation: SSHCommandInvocation!
private(set) var session: SSHCommandSession!
let harness = SSHSessionHarness()

private(set) var chunks: [SSHCommandChunk] = []
private(set) var status: [SSHCommandStatus] = []

private let channel = EmbeddedSSHChannel()

init(command: SSHCommand) {
invocation = SSHCommandInvocation(
command: command,
onChunk: { self.chunks.append($0) },
onStatus: { self.status.append($0) }
)
session = SSHCommandSession(invocation: invocation)
}

func assertStart() throws -> Future<Void> {
try channel.connect().wait()
let promise = try harness.start(session)
harness.channel.run()
XCTAssertTrue(harness.channel.isActive)
XCTAssertEqual(
harness.channel.outboundEvents,
[SSHChannelRequestEvent.ExecRequest(
command: invocation.command.command,
wantReply: true
)]
)
XCTAssertEqual(chunks, [])
XCTAssertEqual(status, [])
return promise
}

func serverEnd() throws {
let closing = ChannelEvent.inputClosed
harness.channel.triggerInbound(closing)
harness.run()
}
}
Loading

0 comments on commit cef527e

Please sign in to comment.