From 5144b2282bbcd5adf361d28e7dbd8963a75fb6d6 Mon Sep 17 00:00:00 2001 From: Joe Heck Date: Tue, 31 Dec 2024 10:53:13 -0800 Subject: [PATCH] working commands to be a single string invoked remotely --- Sources/Formic/Commands/CopyFrom.swift | 2 +- Sources/Formic/Commands/ShellCommand.swift | 47 ++++++------------- Sources/Formic/Commands/VerifyAccess.swift | 6 +-- .../DependencyProxies/CommandInvoker.swift | 40 ++++++++-------- .../Formic/ResourceTypes/Swarm+Parsers.swift | 2 +- Tests/formicTests/CommandInvokerTests.swift | 24 ++++++++-- .../Commands/ShellCommandTests.swift | 4 +- Tests/formicTests/EngineTests.swift | 22 ++++----- .../Resources/OperatingSystemTests.swift | 6 +-- .../Resources/SwarmInitTests.swift | 17 ++++--- Tests/formicTests/TestDependencies.swift | 22 ++++----- 11 files changed, 97 insertions(+), 95 deletions(-) diff --git a/Sources/Formic/Commands/CopyFrom.swift b/Sources/Formic/Commands/CopyFrom.swift index 168fd21..c97eca5 100644 --- a/Sources/Formic/Commands/CopyFrom.swift +++ b/Sources/Formic/Commands/CopyFrom.swift @@ -69,7 +69,7 @@ public struct CopyFrom: Command { remotePath: destinationPath) } else { return try await invoker.localShell( - cmd: ["cp", tempFile.path, destinationPath], stdIn: nil, env: nil, chdir: nil, debugPrint: false) + cmd: "cp \(tempFile.path) \(destinationPath)", stdIn: nil, env: nil, chdir: nil, debugPrint: false) } } } diff --git a/Sources/Formic/Commands/ShellCommand.swift b/Sources/Formic/Commands/ShellCommand.swift index 891888a..c9522e3 100644 --- a/Sources/Formic/Commands/ShellCommand.swift +++ b/Sources/Formic/Commands/ShellCommand.swift @@ -7,7 +7,7 @@ import Foundation /// Do not use shell control or redirect operators in the command string. public struct ShellCommand: Command { /// The command and arguments to run. - public let args: [String] + public let commandString: String /// An optional dictionary of environment variables the system sets when it runs the command. public let env: [String: String]? /// An optional directory to change to before running the command. @@ -20,51 +20,33 @@ public struct ShellCommand: Command { public let executionTimeout: Duration /// The ID of the command. public let id: UUID + /// A Boolean flag that enables additional debug output. + public let debug: Bool /// Creates a new command declaration that the engine runs as a shell command. /// - Parameters: - /// - arguments: the command and arguments to run, each argument as a separate string. + /// - argString: the command and arguments to run as a single string separated by spaces. /// - env: An optional dictionary of environment variables the system sets when it runs the command. /// - chdir: An optional directory to change to before running the command. /// - ignoreFailure: A Boolean value that indicates whether a failing command should fail a playbook. /// - retry: The retry settings for the command. /// - executionTimeout: The maximum duration to allow for the command. + /// - debug: An optional Boolean value the presents additional debug output on execution. public init( - arguments: [String], env: [String: String]? = nil, chdir: String? = nil, - ignoreFailure: Bool = false, retry: Backoff = .never, - executionTimeout: Duration = .seconds(30) + _ argString: String, env: [String: String]? = nil, chdir: String? = nil, + ignoreFailure: Bool = false, + retry: Backoff = .never, executionTimeout: Duration = .seconds(30), debug: Bool = false ) { - self.args = arguments + self.commandString = argString self.env = env self.retry = retry self.ignoreFailure = ignoreFailure self.executionTimeout = executionTimeout self.chdir = chdir + self.debug = debug id = UUID() } - /// Creates a new command declaration that the engine runs as a shell command. - /// - Parameters: - /// - argString: the command and arguments to run as a single string separated by spaces. - /// - env: An optional dictionary of environment variables the system sets when it runs the command. - /// - chdir: An optional directory to change to before running the command. - /// - ignoreFailure: A Boolean value that indicates whether a failing command should fail a playbook. - /// - retry: The retry settings for the command. - /// - executionTimeout: The maximum duration to allow for the command. - /// - /// This initializer is useful when you have a space-separated string of arguments, and splits all arguments by whitespace. - /// If a command, or argument, requires a whitespace within it, use ``init(arguments:env:chdir:ignoreFailure:retry:executionTimeout:)`` instead. - public init( - _ argString: String, env: [String: String]? = nil, chdir: String? = nil, - ignoreFailure: Bool = false, - retry: Backoff = .never, executionTimeout: Duration = .seconds(30) - ) { - let splitArgs: [String] = argString.split(separator: .whitespace).map(String.init) - self.init( - arguments: splitArgs, env: env, chdir: chdir, ignoreFailure: ignoreFailure, retry: retry, - executionTimeout: executionTimeout) - } - /// Runs the command on the host you provide. /// - Parameter host: The host on which to run the command. /// - Returns: The command output. @@ -81,12 +63,13 @@ public struct ShellCommand: Command { port: host.sshPort, strictHostKeyChecking: false, chdir: chdir, - cmd: args, + cmd: commandString, env: env, - debugPrint: false + debugPrint: debug ) } else { - return try await invoker.localShell(cmd: args, stdIn: nil, env: env, chdir: chdir, debugPrint: false) + return try await invoker.localShell( + cmd: commandString, stdIn: nil, env: env, chdir: chdir, debugPrint: debug) } } } @@ -94,6 +77,6 @@ public struct ShellCommand: Command { extension ShellCommand: CustomStringConvertible { /// A textual representation of the command. public var description: String { - return args.joined(separator: " ") + return commandString } } diff --git a/Sources/Formic/Commands/VerifyAccess.swift b/Sources/Formic/Commands/VerifyAccess.swift index 7ae959a..52bd9b5 100644 --- a/Sources/Formic/Commands/VerifyAccess.swift +++ b/Sources/Formic/Commands/VerifyAccess.swift @@ -36,7 +36,7 @@ public struct VerifyAccess: Command { @discardableResult public func run(host: Host) async throws -> CommandOutput { @Dependency(\.commandInvoker) var invoker: any CommandInvoker - let cmdArgs = ["echo", "'hello'"] + let command = "echo 'hello'" let answer: CommandOutput if host.remote { @@ -49,11 +49,11 @@ public struct VerifyAccess: Command { port: host.sshPort, strictHostKeyChecking: false, chdir: nil, - cmd: cmdArgs, + cmd: command, env: nil, debugPrint: false) } else { - answer = try await invoker.localShell(cmd: cmdArgs, stdIn: nil, env: nil, chdir: nil, debugPrint: false) + answer = try await invoker.localShell(cmd: command, stdIn: nil, env: nil, chdir: nil, debugPrint: false) } if answer.stdoutString != "hello" { return CommandOutput(returnCode: -1, stdOut: nil, stdErr: "Unable to verify access.".data(using: .utf8)) diff --git a/Sources/Formic/DependencyProxies/CommandInvoker.swift b/Sources/Formic/DependencyProxies/CommandInvoker.swift index 8c18ab1..4a77127 100644 --- a/Sources/Formic/DependencyProxies/CommandInvoker.swift +++ b/Sources/Formic/DependencyProxies/CommandInvoker.swift @@ -22,7 +22,7 @@ import Foundation // // - https://github.com/Zollerboy1/SwiftCommand // I like the structure of SwiftCommand, but it has a few swift6 concurrency warnings about fiddling -// with mutable buffers that are _just_ slightly concerning to me. There also doesn't appear to +// with mutable buffers that are slightly concerning to me. There also doesn't appear to // be a convenient way to capture STDERR separately (it's mixed together). // Dependency injection docs: @@ -36,7 +36,7 @@ protocol CommandInvoker: Sendable { port: Int?, strictHostKeyChecking: Bool, chdir: String?, - cmd: [String], + cmd: String, env: [String: String]?, debugPrint: Bool ) async throws -> CommandOutput @@ -54,7 +54,7 @@ protocol CommandInvoker: Sendable { func getDataAtURL(url: URL) async throws -> Data func localShell( - cmd: [String], + cmd: String, stdIn: Pipe?, env: [String: String]?, chdir: String?, @@ -94,7 +94,7 @@ struct ProcessCommandInvoker: CommandInvoker { /// followed by attempting to read the Pipe() outputs (fileHandleForReading.readToEnd()). /// The types of errors thrown from those locations aren't undocumented. func localShell( - cmd: [String], stdIn: Pipe? = nil, env: [String: String]? = nil, chdir: String? = nil, debugPrint: Bool = false + cmd: String, stdIn: Pipe? = nil, env: [String: String]? = nil, chdir: String? = nil, debugPrint: Bool = false ) async throws -> CommandOutput { let task = Process() task.executableURL = URL(fileURLWithPath: "/usr/bin/env") @@ -112,9 +112,9 @@ struct ProcessCommandInvoker: CommandInvoker { } if let chdir = chdir { - task.arguments = ["sh", "-c", "cd \(chdir); \(cmd.joined(separator: " "))"] + task.arguments = ["sh", "-c", "cd \(chdir); \(cmd)"] } else { - task.arguments = ["sh", "-c", "\(cmd.joined(separator: " "))"] + task.arguments = ["sh", "-c", "\(cmd)"] } if debugPrint { print(task.arguments?.joined(separator: " ") ?? "nil") @@ -186,9 +186,10 @@ struct ProcessCommandInvoker: CommandInvoker { args.append(localPath) args.append("\(user)@\(host):\(remotePath)") + let commandString: String = args.joined(separator: " ") // loose form: // scp -o StrictHostKeyChecking=no get-docker.sh "docker-user@${IP_ADDRESS}:get-docker.sh" - let rcAndPipe = try await localShell(cmd: args) + let rcAndPipe = try await localShell(cmd: commandString) return rcAndPipe } @@ -213,31 +214,30 @@ struct ProcessCommandInvoker: CommandInvoker { port: Int? = nil, strictHostKeyChecking: Bool = false, chdir: String?, - cmd: [String], + cmd: String, env: [String: String]? = nil, debugPrint: Bool = false ) async throws -> CommandOutput { - var args: [String] = ["ssh"] + var args: String = "ssh" if strictHostKeyChecking { - args.append("-o") - args.append("StrictHostKeyChecking=no") + args.append(" -o") + args.append(" StrictHostKeyChecking=no") } if let identityFile { - args.append("-i") - args.append(identityFile) + args.append(" -i") + args.append(" \(identityFile)") } if let port { - args.append("-p") - args.append("\(port)") + args.append(" -p") + args.append(" \(port)") } - args.append("-t") // request a TTY at the remote host - args.append("\(user)@\(host)") + args.append(" -t") // request a TTY at the remote host + args.append(" \(user)@\(host)") - let joinedCmd = cmd.joined(separator: " ") if let chdir = chdir { - args.append("cd \(chdir);\(joinedCmd)") + args.append(" cd \(chdir);\(cmd)") } else { - args.append("\(joinedCmd)") + args.append(" \(cmd)") } // NOTE(heckj): Ansible's SSH capability diff --git a/Sources/Formic/ResourceTypes/Swarm+Parsers.swift b/Sources/Formic/ResourceTypes/Swarm+Parsers.swift index 42eec1c..fc1a375 100644 --- a/Sources/Formic/ResourceTypes/Swarm+Parsers.swift +++ b/Sources/Formic/ResourceTypes/Swarm+Parsers.swift @@ -8,7 +8,7 @@ import Parsing public struct SwarmJoinCommand: Parser { func convertToShellCommand(_ argTuple: (String, String, String, String, String, String)) -> ShellCommand { - ShellCommand(arguments: [argTuple.0, argTuple.1, argTuple.2, argTuple.3, argTuple.4, argTuple.5]) + ShellCommand("\(argTuple.0) \(argTuple.1) \(argTuple.2) \(argTuple.3) \(argTuple.4) \(argTuple.5)") } public var body: some Parser { diff --git a/Tests/formicTests/CommandInvokerTests.swift b/Tests/formicTests/CommandInvokerTests.swift index 3e8c137..acd21ed 100644 --- a/Tests/formicTests/CommandInvokerTests.swift +++ b/Tests/formicTests/CommandInvokerTests.swift @@ -9,7 +9,7 @@ import Testing .timeLimit(.minutes(1)), .tags(.functionalTest)) func invokeBasicCommandLocally() async throws { - let shellResult = try await ProcessCommandInvoker().localShell(cmd: ["uname"], stdIn: nil, env: nil) + let shellResult = try await ProcessCommandInvoker().localShell(cmd: "uname", stdIn: nil, env: nil) // print("rc: \(shellResult.returnCode)") // print("out: \(shellResult.stdoutString ?? "nil")") @@ -27,7 +27,7 @@ func invokeBasicCommandLocally() async throws { .timeLimit(.minutes(1)), .tags(.integrationTest)) func invokeBasicCommandLocallyWithChdir() async throws { - let shellResult = try await ProcessCommandInvoker().localShell(cmd: ["pwd"], stdIn: nil, env: nil, chdir: "..") + let shellResult = try await ProcessCommandInvoker().localShell(cmd: "pwd", stdIn: nil, env: nil, chdir: "..") print("rc: \(shellResult.returnCode)") print("out: \(shellResult.stdoutString ?? "nil")") @@ -42,7 +42,7 @@ func invokeBasicCommandLocallyWithChdir() async throws { func invokeRemoteCommand() async throws { let shellResult = try await ProcessCommandInvoker().remoteShell( host: "127.0.0.1", user: "heckj", identityFile: "~/.orbstack/ssh/id_ed25519", port: 32222, chdir: nil, - cmd: ["ls", "-al"], env: nil) + cmd: "ls -al", env: nil) print("rc: \(shellResult.returnCode)") print("out: \(shellResult.stdoutString ?? "nil")") print("err: \(shellResult.stderrString ?? "nil")") @@ -56,7 +56,7 @@ func invokeRemoteCommand() async throws { func invokeRemoteCommandWithEnv() async throws { let shellResult = try await ProcessCommandInvoker().remoteShell( host: "127.0.0.1", user: "heckj", identityFile: "~/.orbstack/ssh/id_ed25519", port: 32222, chdir: nil, - cmd: ["echo", "${FIDDLY}"], env: ["FIDDLY": "FADDLY"]) + cmd: "echo ${FIDDLY}", env: ["FIDDLY": "FADDLY"]) print("rc: \(shellResult.returnCode)") print("out: \(shellResult.stdoutString ?? "nil")") print("err: \(shellResult.stderrString ?? "nil")") @@ -70,7 +70,21 @@ func invokeRemoteCommandWithEnv() async throws { func invokeRemoteCommandWithChdir() async throws { let shellResult = try await ProcessCommandInvoker().remoteShell( host: "127.0.0.1", user: "heckj", identityFile: "~/.orbstack/ssh/id_ed25519", port: 32222, chdir: "..", - cmd: ["ls", "-al"], env: nil) + cmd: "ls -al", env: nil) + print("rc: \(shellResult.returnCode)") + print("out: \(shellResult.stdoutString ?? "nil")") + print("err: \(shellResult.stderrString ?? "nil")") +} + +@Test( + "invoking a remote command w/ tilde", + .enabled(if: ProcessInfo.processInfo.environment.keys.contains("INTEGRATION_ENABLED")), + .timeLimit(.minutes(1)), + .tags(.integrationTest)) +func invokeRemoteCommandWithTilde() async throws { + let shellResult = try await ProcessCommandInvoker().remoteShell( + host: "127.0.0.1", user: "heckj", identityFile: "~/.orbstack/ssh/id_ed25519", port: 32222, chdir: "..", + cmd: "mkdir ~/.ssh", env: nil) print("rc: \(shellResult.returnCode)") print("out: \(shellResult.stdoutString ?? "nil")") print("err: \(shellResult.stderrString ?? "nil")") diff --git a/Tests/formicTests/Commands/ShellCommandTests.swift b/Tests/formicTests/Commands/ShellCommandTests.swift index 2395461..fec579b 100644 --- a/Tests/formicTests/Commands/ShellCommandTests.swift +++ b/Tests/formicTests/Commands/ShellCommandTests.swift @@ -8,7 +8,7 @@ import Testing func shellCommandDeclarationTest() async throws { let command = ShellCommand("uname") #expect(command.retry == .never) - #expect(command.args == ["uname"]) + #expect(command.commandString == "uname") #expect(command.env == nil) #expect(command.id != nil) @@ -27,7 +27,7 @@ func shellCommandFullDeclarationTest() async throws { let command = ShellCommand( "ls", env: ["PATH": "/usr/bin"], retry: Backoff(maxRetries: 200, strategy: .exponential(maxDelay: .seconds(60)))) - #expect(command.args == ["ls"]) + #expect(command.commandString == "ls") #expect(command.env == ["PATH": "/usr/bin"]) #expect(command.retry == Backoff(maxRetries: 200, strategy: .exponential(maxDelay: .seconds(60)))) #expect(command.description == "ls") diff --git a/Tests/formicTests/EngineTests.swift b/Tests/formicTests/EngineTests.swift index 3d17b3e..8093801 100644 --- a/Tests/formicTests/EngineTests.swift +++ b/Tests/formicTests/EngineTests.swift @@ -16,7 +16,7 @@ func testEngineRun() async throws { let cmdExecOut = try await withDependencies { dependencyValues in dependencyValues.localSystemAccess = TestFileSystemAccess() dependencyValues.commandInvoker = TestCommandInvoker() - .addSuccess(command: ["uname"], presentOutput: "Darwin\n") + .addSuccess(command: "uname", presentOutput: "Darwin\n") } operation: { try await engine.run(host: .localhost, command: cmd) } @@ -40,8 +40,8 @@ func testEngineRunList() async throws { let cmdExecOut = try await withDependencies { dependencyValues in dependencyValues.localSystemAccess = TestFileSystemAccess() dependencyValues.commandInvoker = TestCommandInvoker() - .addSuccess(command: ["uname"], presentOutput: "Darwin\n") - .addSuccess(command: ["whoami"], presentOutput: "docker-user") + .addSuccess(command: "uname", presentOutput: "Darwin\n") + .addSuccess(command: "whoami", presentOutput: "docker-user") } operation: { try await engine.run(host: .localhost, displayProgress: false, commands: [cmd1, cmd2]) } @@ -75,8 +75,8 @@ func testEngineRunPlaybook() async throws { let collectedResults = try await withDependencies { dependencyValues in dependencyValues.localSystemAccess = TestFileSystemAccess() dependencyValues.commandInvoker = TestCommandInvoker() - .addSuccess(command: ["uname"], presentOutput: "Darwin\n") - .addSuccess(command: ["whoami"], presentOutput: "docker-user") + .addSuccess(command: "uname", presentOutput: "Darwin\n") + .addSuccess(command: "whoami", presentOutput: "docker-user") } operation: { try await engine.run(hosts: [.localhost], displayProgress: false, commands: [cmd1, cmd2]) } @@ -106,8 +106,8 @@ func testEngineRunPlaybookWithFailure() async throws { let collectedResults = try await withDependencies { dependencyValues in dependencyValues.localSystemAccess = TestFileSystemAccess() dependencyValues.commandInvoker = TestCommandInvoker() - .addSuccess(command: ["uname"], presentOutput: "Darwin\n") - .addFailure(command: ["whoami"], presentOutput: "not tellin!") + .addSuccess(command: "uname", presentOutput: "Darwin\n") + .addFailure(command: "whoami", presentOutput: "not tellin!") } operation: { try await engine.run(hosts: [.localhost], displayProgress: false, commands: [cmd1, cmd2]) } @@ -140,9 +140,9 @@ func testEngineRunPlaybookWithException() async throws { let _ = try await withDependencies { dependencyValues in dependencyValues.localSystemAccess = TestFileSystemAccess() dependencyValues.commandInvoker = TestCommandInvoker() - .addSuccess(command: ["uname"], presentOutput: "Darwin\n") + .addSuccess(command: "uname", presentOutput: "Darwin\n") .addException( - command: ["whoami"], errorToThrow: TestError.unknown(msg: "Process failed in something")) + command: "whoami", errorToThrow: TestError.unknown(msg: "Process failed in something")) } operation: { try await engine.run(hosts: [.localhost], displayProgress: false, commands: [cmd1, cmd2]) } @@ -163,7 +163,7 @@ func testCommandTimeout() async throws { } let mockCmdInvoker = TestCommandInvoker() - .addSuccess(command: ["uname"], presentOutput: "Linux\n", delay: .seconds(2)) + .addSuccess(command: "uname", presentOutput: "Linux\n", delay: .seconds(2)) await #expect( throws: CommandError.self, "Slow command should invoke timeout", @@ -191,7 +191,7 @@ func testCommandRetry() async throws { } let mockCmdInvoker = TestCommandInvoker() - .addFailure(command: ["uname"], presentOutput: "not tellin!") + .addFailure(command: "uname", presentOutput: "not tellin!") let result = try await withDependencies { dependencyValues in dependencyValues.localSystemAccess = TestFileSystemAccess() diff --git a/Tests/formicTests/Resources/OperatingSystemTests.swift b/Tests/formicTests/Resources/OperatingSystemTests.swift index a99e314..83a833e 100644 --- a/Tests/formicTests/Resources/OperatingSystemTests.swift +++ b/Tests/formicTests/Resources/OperatingSystemTests.swift @@ -31,7 +31,7 @@ func testOSStringInitializer() async throws { func testOperatingSystemSingularInquiry() async throws { let shellResult: CommandOutput = try await withDependencies { $0.commandInvoker = TestCommandInvoker() - .addSuccess(command: ["uname"], presentOutput: "Linux\n") + .addSuccess(command: "uname", presentOutput: "Linux\n") } operation: { try await OperatingSystem.inquiry.run(host: .localhost) } @@ -53,7 +53,7 @@ func testOperatingSystemQuery() async throws { let (parsedOS, _) = try await withDependencies { $0.commandInvoker = TestCommandInvoker() - .addSuccess(command: ["uname"], presentOutput: "Linux\n") + .addSuccess(command: "uname", presentOutput: "Linux\n") $0.date.now = Date(timeIntervalSince1970: 1_234_567_890) } operation: { @@ -70,7 +70,7 @@ func testOperatingSystemInstanceQuery() async throws { let (parsedOS, _) = try await withDependencies { $0.commandInvoker = TestCommandInvoker() - .addSuccess(command: ["uname"], presentOutput: "Linux\n") + .addSuccess(command: "uname", presentOutput: "Linux\n") $0.date.now = Date(timeIntervalSince1970: 1_234_567_890) } operation: { try await instance.query(from: .localhost) diff --git a/Tests/formicTests/Resources/SwarmInitTests.swift b/Tests/formicTests/Resources/SwarmInitTests.swift index 5277510..fe986de 100644 --- a/Tests/formicTests/Resources/SwarmInitTests.swift +++ b/Tests/formicTests/Resources/SwarmInitTests.swift @@ -17,9 +17,9 @@ func verifyParsingSwarmInitIntoWorkerCommand() async throws { """ let cmd = try SwarmJoinCommand().parse(sample) - #expect(cmd.args.count == 6) - #expect(cmd.args[4] == "SWMTKN-1-4co3ccnbcdrww0iq7f9te0478286pd168bhzfx9oyc1wyws0vi-1p35bj1i57s9h9dpf57mmeqq0") - #expect(cmd.args[5] == "198.19.249.61:2377") + #expect( + cmd.commandString.contains( + "SWMTKN-1-4co3ccnbcdrww0iq7f9te0478286pd168bhzfx9oyc1wyws0vi-1p35bj1i57s9h9dpf57mmeqq0")) } @Test("docker swarm join-token worker parsing") @@ -31,9 +31,14 @@ func verifyParsingSwarmJoinTokenWorkerCommand() async throws { """ let cmd = try SwarmJoinCommand().parse(sample) - #expect(cmd.args.count == 6) - #expect(cmd.args[4] == "SWMTKN-1-4co3ccnbcdrww0iq7f9te0478286pd168bhzfx9oyc1wyws0vi-1p35bj1i57s9h9dpf57mmeqq0") - #expect(cmd.args[5] == "198.19.249.61:2377") + // #expect(cmd.args.count == 6) + // #expect(cmd.args[4] == "SWMTKN-1-4co3ccnbcdrww0iq7f9te0478286pd168bhzfx9oyc1wyws0vi-1p35bj1i57s9h9dpf57mmeqq0") + // #expect(cmd.args[5] == "198.19.249.61:2377") + #expect( + cmd.commandString.contains( + "SWMTKN-1-4co3ccnbcdrww0iq7f9te0478286pd168bhzfx9oyc1wyws0vi-1p35bj1i57s9h9dpf57mmeqq0")) + #expect(cmd.commandString.contains("198.19.249.61:2377")) + } @Test("checking slicing") diff --git a/Tests/formicTests/TestDependencies.swift b/Tests/formicTests/TestDependencies.swift index b6133f1..25f2335 100644 --- a/Tests/formicTests/TestDependencies.swift +++ b/Tests/formicTests/TestDependencies.swift @@ -18,18 +18,18 @@ struct TestCommandInvoker: CommandInvoker { } // proxyResults is keyed by arguments, returns a tuple of seconds delay to apply, then the result - var proxyResults: [[String]: (Duration, CommandOutput)] - var proxyErrors: [[String]: (any Error)] + var proxyResults: [String: (Duration, CommandOutput)] + var proxyErrors: [String: (any Error)] var proxyData: [URL: Data] func remoteCopy( host: String, user: String, identityFile: String?, port: Int?, strictHostKeyChecking: Bool, localPath: String, remotePath: String ) async throws -> Formic.CommandOutput { - if let errorToThrow = proxyErrors[[localPath, remotePath]] { + if let errorToThrow = proxyErrors["\(localPath) \(remotePath)"] { throw errorToThrow } - if let (delay, storedResponse) = proxyResults[[localPath, remotePath]] { + if let (delay, storedResponse) = proxyResults["\(localPath) \(remotePath)"] { try await Task.sleep(for: delay) return storedResponse } @@ -38,7 +38,7 @@ struct TestCommandInvoker: CommandInvoker { func remoteShell( host: String, user: String, identityFile: String?, port: Int?, strictHostKeyChecking: Bool, chdir: String?, - cmd: [String], env: [String: String]?, debugPrint: Bool + cmd: String, env: [String: String]?, debugPrint: Bool ) async throws -> Formic.CommandOutput { if let errorToThrow = proxyErrors[cmd] { throw errorToThrow @@ -51,7 +51,7 @@ struct TestCommandInvoker: CommandInvoker { return CommandOutput(returnCode: 0, stdOut: "".data(using: .utf8), stdErr: nil) } - func localShell(cmd: [String], stdIn: Pipe?, env: [String: String]?, chdir: String?, debugPrint: Bool) async throws + func localShell(cmd: String, stdIn: Pipe?, env: [String: String]?, chdir: String?, debugPrint: Bool) async throws -> Formic.CommandOutput { if let errorToThrow = proxyErrors[cmd] { @@ -66,8 +66,8 @@ struct TestCommandInvoker: CommandInvoker { } init( - _ outputs: [[String]: (Duration, CommandOutput)], - _ errors: [[String]: (any Error)], + _ outputs: [String: (Duration, CommandOutput)], + _ errors: [String: (any Error)], _ data: [URL: Data] ) { proxyResults = outputs @@ -81,7 +81,7 @@ struct TestCommandInvoker: CommandInvoker { proxyData = [:] } - func addSuccess(command: [String], presentOutput: String, delay: Duration = .zero) -> Self { + func addSuccess(command: String, presentOutput: String, delay: Duration = .zero) -> Self { var existingResult = proxyResults existingResult[command] = ( delay, @@ -93,7 +93,7 @@ struct TestCommandInvoker: CommandInvoker { return TestCommandInvoker(existingResult, proxyErrors, proxyData) } - func addFailure(command: [String], presentOutput: String, delay: Duration = .zero, returnCode: Int32 = -1) -> Self { + func addFailure(command: String, presentOutput: String, delay: Duration = .zero, returnCode: Int32 = -1) -> Self { var existingResult = proxyResults existingResult[command] = ( delay, @@ -106,7 +106,7 @@ struct TestCommandInvoker: CommandInvoker { return TestCommandInvoker(existingResult, proxyErrors, proxyData) } - func addException(command: [String], errorToThrow: (any Error)) -> Self { + func addException(command: String, errorToThrow: (any Error)) -> Self { var existingErrors = proxyErrors existingErrors[command] = errorToThrow return TestCommandInvoker(proxyResults, existingErrors, proxyData)