1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-08-13 14:30:06 +02:00
This commit is contained in:
2022-10-04 17:57:05 +02:00
parent c26c491340
commit 953ccb3792
6 changed files with 52 additions and 93 deletions

View File

@@ -9,54 +9,27 @@
import XCTest import XCTest
class FakeShellTest: XCTestCase { class FakeShellTest: XCTestCase {
func test_fake_shell_output_can_be_declared() async {
func test_fake_shell_output_can_be_declared() {
let greeting = BatchFakeShellOutput(items: [ let greeting = BatchFakeShellOutput(items: [
.instant("Hello world"), .instant("Hello world\n"),
.delayed(0.3, "Goodbye world") .delayed(0.3, "Goodbye world")
]) ])
let output = greeting.outputInstantaneously() let output = await greeting.outputInstantaneously()
XCTAssertEqual("Hello world\nGoodbye world", output.out) XCTAssertEqual("Hello world\nGoodbye world", output.out)
} }
/* func test_fake_shell_can_output_in_realtime() async {
func test_we_can_predefine_responses_for_dummy_shell() { let greeting = BatchFakeShellOutput(items: [
let expectedPhpOutput = """ .instant("Hello world\n"),
PHP 8.1.10 (cli) (built: Sep 3 2022 12:09:27) (NTS) .delayed(2, "Goodbye world")
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
]) ])
XCTAssertTrue(Shell is TestableShell) let output = await greeting.output(didReceiveOutput: { output, _ in
print(output)
})
XCTAssertEqual(expectedPhpOutput, Shell.sync("php -v").out) XCTAssertEqual("Hello world\nGoodbye world", output.out)
XCTAssertEqual(expectedPhpOutput, Shell.sync("php --version").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)
}
*/
} }

View File

@@ -33,8 +33,8 @@ class SystemShellTest: XCTestCase {
let (_, shellOutput) = try! await Shell.attach( let (_, shellOutput) = try! await Shell.attach(
"php -r \"echo 'Hello world' . PHP_EOL; usleep(200); echo 'Goodbye world';\"", "php -r \"echo 'Hello world' . PHP_EOL; usleep(200); echo 'Goodbye world';\"",
didReceiveOutput: { incoming in didReceiveOutput: { incoming, _ in
bits.append(incoming.out) bits.append(incoming)
}, },
withTimeout: 2.0 withTimeout: 2.0
) )
@@ -50,7 +50,7 @@ class SystemShellTest: XCTestCase {
do { do {
_ = try await Shell.attach( _ = try await Shell.attach(
"php -r \"sleep(1);\"", "php -r \"sleep(1);\"",
didReceiveOutput: { _ in }, didReceiveOutput: { _, _ in },
withTimeout: 0.1 withTimeout: 0.1
) )
} catch { } catch {

View File

@@ -57,16 +57,9 @@ import Foundation
let (process, _) = try await Shell.attach( let (process, _) = try await Shell.attach(
command, command,
didReceiveOutput: { [weak self] output in didReceiveOutput: { [weak self] (incoming, _) in
guard let window = self?.window else { return } guard let window = self?.window else { return }
window.addToConsole(incoming)
if output.hasError {
window.addToConsole(output.err)
}
if !output.out.isEmpty {
window.addToConsole(output.out)
}
}, },
withTimeout: .minutes(5) withTimeout: .minutes(5)
) )

View File

@@ -8,6 +8,10 @@
import Foundation import Foundation
enum ShellStream {
case stdOut, stdErr, stdIn
}
struct ShellOutput { struct ShellOutput {
var out: String var out: String
var err: String var err: String
@@ -56,7 +60,7 @@ protocol Shellable {
*/ */
func attach( func attach(
_ command: String, _ command: String,
didReceiveOutput: @escaping (ShellOutput) -> Void, didReceiveOutput: @escaping (String, ShellStream) -> Void,
withTimeout timeout: TimeInterval withTimeout timeout: TimeInterval
) async throws -> (Process, ShellOutput) ) async throws -> (Process, ShellOutput)
} }

View File

@@ -116,18 +116,16 @@ class SystemShell: Shellable {
func attach( func attach(
_ command: String, _ command: String,
didReceiveOutput: @escaping (ShellOutput) -> Void, didReceiveOutput: @escaping (String, ShellStream) -> Void,
withTimeout timeout: TimeInterval = 5.0 withTimeout timeout: TimeInterval = 5.0
) async throws -> (Process, ShellOutput) { ) async throws -> (Process, ShellOutput) {
let task = getShellProcess(for: command) let task = getShellProcess(for: command)
var output = ShellOutput(out: "", err: "")
var allOut: String = "" task.listen { incoming in
var allErr: String = "" output.out += incoming; didReceiveOutput(incoming, .stdOut)
} didReceiveStandardErrorData: { incoming in
task.listen { stdOut in output.err += incoming; didReceiveOutput(incoming, .stdErr)
allOut += stdOut; didReceiveOutput(.out(stdOut))
} didReceiveStandardErrorData: { stdErr in
allErr += stdErr; didReceiveOutput(.err(stdErr))
} }
return try await withCheckedThrowingContinuation({ continuation in return try await withCheckedThrowingContinuation({ continuation in
@@ -138,11 +136,11 @@ class SystemShell: Shellable {
timer?.invalidate() timer?.invalidate()
if !allErr.isEmpty { if !output.err.isEmpty {
return continuation.resume(returning: (process, .err(allErr))) 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 timer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { _ in

View File

@@ -27,7 +27,7 @@ public class TestableShell: Shellable {
func attach( func attach(
_ command: String, _ command: String,
didReceiveOutput: @escaping (ShellOutput) -> Void, didReceiveOutput: @escaping (String, ShellStream) -> Void,
withTimeout timeout: TimeInterval withTimeout timeout: TimeInterval
) async throws -> (Process, ShellOutput) { ) async throws -> (Process, ShellOutput) {
return (Process(), self.sync(command)) 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 { struct FakeShellOutput {
let delay: TimeInterval let delay: TimeInterval
let output: ShellOutput let output: String
let stream: ShellStream
static func instant(_ stdOut: String, _ stdErr: String? = nil) -> FakeShellOutput { static func instant(_ output: String, _ stream: ShellStream = .stdOut) -> FakeShellOutput {
return FakeShellOutput(delay: 0, output: ShellOutput(out: stdOut, err: stdErr ?? "")) return FakeShellOutput(delay: 0, output: output, stream: stream)
} }
static func delayed(_ delay: TimeInterval, _ stdOut: String, _ stdErr: String? = nil) -> FakeShellOutput { static func delayed(_ delay: TimeInterval, _ output: String, _ stream: ShellStream = .stdOut) -> FakeShellOutput {
return FakeShellOutput(delay: delay, output: ShellOutput(out: stdOut, err: stdErr ?? "")) return FakeShellOutput(delay: delay, output: output, stream: stream)
} }
} }
@@ -67,37 +62,33 @@ struct BatchFakeShellOutput {
Outputs the fake shell output as expected. Outputs the fake shell output as expected.
*/ */
public func output( public func output(
didReceiveOutput: @escaping (ShellOutput) -> Void, didReceiveOutput: @escaping (String, ShellStream) -> Void,
ignoreDelay: Bool = false ignoreDelay: Bool = false
) async -> ShellOutput { ) async -> ShellOutput {
var allOut: String = "" var output = ShellOutput(out: "", err: "")
var allErr: String = ""
Task { for item in items {
self.items.forEach { fakeShellOutput in if !ignoreDelay {
let delay = UInt64(fakeShellOutput.delay * 1_000_000_000) let delay = UInt64(item.delay * 1_000_000_000)
try await Task.sleep(nanoseconds: delay) try! await Task.sleep(nanoseconds: delay)
allOut += fakeShellOutput.output.out
if fakeShellOutput.output.hasError {
allErr += fakeShellOutput.output.err
}
}
} }
return ShellOutput( if item.stream == .stdErr {
out: allOut, output.err += item.output
err: allErr } else if item.stream == .stdOut {
) output.out += item.output
}
}
return output
} }
/** /**
For testing purposes (and speed) we may omit the delay, regardless of its timespan. For testing purposes (and speed) we may omit the delay, regardless of its timespan.
*/ */
public func outputInstantaneously( public func outputInstantaneously(
didReceiveOutput: @escaping (ShellOutput) -> Void didReceiveOutput: @escaping (String, ShellStream) -> Void = { _, _ in }
) -> ShellOutput { ) async -> ShellOutput {
self.output(didReceiveOutput: didReceiveOutput, ignoreDelay: true) return await self.output(didReceiveOutput: didReceiveOutput, ignoreDelay: true)
} }
} }