diff --git a/phpmon-tests/Next/FakeShellTest.swift b/phpmon-tests/Next/FakeShellTest.swift index dedd2b1..f7f1fd8 100644 --- a/phpmon-tests/Next/FakeShellTest.swift +++ b/phpmon-tests/Next/FakeShellTest.swift @@ -9,54 +9,27 @@ import XCTest class FakeShellTest: XCTestCase { - - func test_fake_shell_output_can_be_declared() { + func test_fake_shell_output_can_be_declared() async { let greeting = BatchFakeShellOutput(items: [ - .instant("Hello world"), + .instant("Hello world\n"), .delayed(0.3, "Goodbye world") ]) - let output = greeting.outputInstantaneously() + let output = await greeting.outputInstantaneously() XCTAssertEqual("Hello world\nGoodbye world", output.out) } - /* - func test_we_can_predefine_responses_for_dummy_shell() { - let expectedPhpOutput = """ - PHP 8.1.10 (cli) (built: Sep 3 2022 12:09:27) (NTS) - Copyright (c) The PHP Group - Zend Engine v4.1.10, Copyright (c) Zend Technologies - with Zend OPcache v8.1.10, Copyright (c), by Zend Technologies - with Xdebug v3.1.4, Copyright (c) 2002-2022, by Derick Rethans - """ - - let slowVersionOutput = FakeTerminalOutput( - output: expectedPhpOutput, - duration: 1000, - isError: false - ) - - ActiveShell.useTestable([ - "php -v": expectedPhpOutput, - "php --version": slowVersionOutput + func test_fake_shell_can_output_in_realtime() async { + let greeting = BatchFakeShellOutput(items: [ + .instant("Hello world\n"), + .delayed(2, "Goodbye world") ]) - XCTAssertTrue(Shell is TestableShell) + let output = await greeting.output(didReceiveOutput: { output, _ in + print(output) + }) - XCTAssertEqual(expectedPhpOutput, Shell.sync("php -v").out) - - XCTAssertEqual(expectedPhpOutput, Shell.sync("php --version").out) + XCTAssertEqual("Hello world\nGoodbye world", output.out) } - - func test_unrecognized_commands_output_stderr() { - ActiveShell.useTestable([:]) - - let output = Shell.sync("unrecognized command") - - XCTAssertTrue(output.hasError) - XCTAssertEqual("Unexpected Command", output.err) - XCTAssertEqual("", output.out) - } - */ } diff --git a/phpmon-tests/Next/SystemShellTest.swift b/phpmon-tests/Next/SystemShellTest.swift index 28530ff..3d04221 100644 --- a/phpmon-tests/Next/SystemShellTest.swift +++ b/phpmon-tests/Next/SystemShellTest.swift @@ -33,8 +33,8 @@ class SystemShellTest: XCTestCase { let (_, shellOutput) = try! await Shell.attach( "php -r \"echo 'Hello world' . PHP_EOL; usleep(200); echo 'Goodbye world';\"", - didReceiveOutput: { incoming in - bits.append(incoming.out) + didReceiveOutput: { incoming, _ in + bits.append(incoming) }, withTimeout: 2.0 ) @@ -50,7 +50,7 @@ class SystemShellTest: XCTestCase { do { _ = try await Shell.attach( "php -r \"sleep(1);\"", - didReceiveOutput: { _ in }, + didReceiveOutput: { _, _ in }, withTimeout: 0.1 ) } catch { diff --git a/phpmon/Domain/Integrations/Composer/ComposerWindow.swift b/phpmon/Domain/Integrations/Composer/ComposerWindow.swift index 6386793..81a67a3 100644 --- a/phpmon/Domain/Integrations/Composer/ComposerWindow.swift +++ b/phpmon/Domain/Integrations/Composer/ComposerWindow.swift @@ -57,16 +57,9 @@ import Foundation let (process, _) = try await Shell.attach( command, - didReceiveOutput: { [weak self] output in + didReceiveOutput: { [weak self] (incoming, _) in guard let window = self?.window else { return } - - if output.hasError { - window.addToConsole(output.err) - } - - if !output.out.isEmpty { - window.addToConsole(output.out) - } + window.addToConsole(incoming) }, withTimeout: .minutes(5) ) diff --git a/phpmon/Next/Shellable.swift b/phpmon/Next/Shellable.swift index 40bc8d2..816fba1 100644 --- a/phpmon/Next/Shellable.swift +++ b/phpmon/Next/Shellable.swift @@ -8,6 +8,10 @@ import Foundation +enum ShellStream { + case stdOut, stdErr, stdIn +} + struct ShellOutput { var out: String var err: String @@ -56,7 +60,7 @@ protocol Shellable { */ func attach( _ command: String, - didReceiveOutput: @escaping (ShellOutput) -> Void, + didReceiveOutput: @escaping (String, ShellStream) -> Void, withTimeout timeout: TimeInterval ) async throws -> (Process, ShellOutput) } diff --git a/phpmon/Next/SystemShell.swift b/phpmon/Next/SystemShell.swift index dfab315..28f3637 100644 --- a/phpmon/Next/SystemShell.swift +++ b/phpmon/Next/SystemShell.swift @@ -116,18 +116,16 @@ class SystemShell: Shellable { func attach( _ command: String, - didReceiveOutput: @escaping (ShellOutput) -> Void, + didReceiveOutput: @escaping (String, ShellStream) -> Void, withTimeout timeout: TimeInterval = 5.0 ) async throws -> (Process, ShellOutput) { let task = getShellProcess(for: command) + var output = ShellOutput(out: "", err: "") - var allOut: String = "" - var allErr: String = "" - - task.listen { stdOut in - allOut += stdOut; didReceiveOutput(.out(stdOut)) - } didReceiveStandardErrorData: { stdErr in - allErr += stdErr; didReceiveOutput(.err(stdErr)) + task.listen { incoming in + output.out += incoming; didReceiveOutput(incoming, .stdOut) + } didReceiveStandardErrorData: { incoming in + output.err += incoming; didReceiveOutput(incoming, .stdErr) } return try await withCheckedThrowingContinuation({ continuation in @@ -138,11 +136,11 @@ class SystemShell: Shellable { timer?.invalidate() - if !allErr.isEmpty { - return continuation.resume(returning: (process, .err(allErr))) + if !output.err.isEmpty { + return continuation.resume(returning: (process, .err(output.err))) } - return continuation.resume(returning: (process, .out(allOut))) + return continuation.resume(returning: (process, .out(output.out))) } timer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { _ in diff --git a/phpmon/Next/TestableShell.swift b/phpmon/Next/TestableShell.swift index 11ee354..3605779 100644 --- a/phpmon/Next/TestableShell.swift +++ b/phpmon/Next/TestableShell.swift @@ -27,7 +27,7 @@ public class TestableShell: Shellable { func attach( _ command: String, - didReceiveOutput: @escaping (ShellOutput) -> Void, + didReceiveOutput: @escaping (String, ShellStream) -> Void, withTimeout timeout: TimeInterval ) async throws -> (Process, ShellOutput) { return (Process(), self.sync(command)) @@ -41,22 +41,17 @@ public class TestableShell: Shellable { } } -// TODO: Test env shell output should be modeled differently -// So the possible outcome is either: -// 1. Immediate with almost zero delay `.instant("string")` -// 2. Delayed but then all at once: `.delay(300, "string")` -// 3. A stream of data spread over multiple seconds: `.multiple([.delay(300, "hello"), .delay(300, "bye")])` - struct FakeShellOutput { let delay: TimeInterval - let output: ShellOutput + let output: String + let stream: ShellStream - static func instant(_ stdOut: String, _ stdErr: String? = nil) -> FakeShellOutput { - return FakeShellOutput(delay: 0, output: ShellOutput(out: stdOut, err: stdErr ?? "")) + static func instant(_ output: String, _ stream: ShellStream = .stdOut) -> FakeShellOutput { + return FakeShellOutput(delay: 0, output: output, stream: stream) } - static func delayed(_ delay: TimeInterval, _ stdOut: String, _ stdErr: String? = nil) -> FakeShellOutput { - return FakeShellOutput(delay: delay, output: ShellOutput(out: stdOut, err: stdErr ?? "")) + static func delayed(_ delay: TimeInterval, _ output: String, _ stream: ShellStream = .stdOut) -> FakeShellOutput { + return FakeShellOutput(delay: delay, output: output, stream: stream) } } @@ -67,37 +62,33 @@ struct BatchFakeShellOutput { Outputs the fake shell output as expected. */ public func output( - didReceiveOutput: @escaping (ShellOutput) -> Void, + didReceiveOutput: @escaping (String, ShellStream) -> Void, ignoreDelay: Bool = false ) async -> ShellOutput { - var allOut: String = "" - var allErr: String = "" + var output = ShellOutput(out: "", err: "") - Task { - self.items.forEach { fakeShellOutput in - let delay = UInt64(fakeShellOutput.delay * 1_000_000_000) - try await Task.sleep(nanoseconds: delay) + for item in items { + if !ignoreDelay { + let delay = UInt64(item.delay * 1_000_000_000) + try! await Task.sleep(nanoseconds: delay) + } - allOut += fakeShellOutput.output.out - - if fakeShellOutput.output.hasError { - allErr += fakeShellOutput.output.err - } + if item.stream == .stdErr { + output.err += item.output + } else if item.stream == .stdOut { + output.out += item.output } } - return ShellOutput( - out: allOut, - err: allErr - ) + return output } /** For testing purposes (and speed) we may omit the delay, regardless of its timespan. */ public func outputInstantaneously( - didReceiveOutput: @escaping (ShellOutput) -> Void - ) -> ShellOutput { - self.output(didReceiveOutput: didReceiveOutput, ignoreDelay: true) + didReceiveOutput: @escaping (String, ShellStream) -> Void = { _, _ in } + ) async -> ShellOutput { + return await self.output(didReceiveOutput: didReceiveOutput, ignoreDelay: true) } }