mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-13 14:30:06 +02:00
🏗 WIP
This commit is contained in:
@@ -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)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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)
|
||||||
)
|
)
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user