1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-12-21 03:10:06 +01:00
Files
app/tests/unit/Testables/Shell/RealShellTest.swift

129 lines
4.3 KiB
Swift

//
// RealShellTest.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 28/09/2022.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Testing
import Foundation
@Suite(.serialized)
struct RealShellTest {
var container: Container
init() async throws {
// Reset to the default shell
container = Container.real()
}
@Test func system_shell_is_default() async {
#expect(container.shell is RealShell)
let output = await container.shell.pipe("php -v")
#expect(output.out.contains("Copyright (c) The PHP Group"))
}
@Test func system_shell_can_be_used_synchronously() {
#expect(container.shell is RealShell)
let output = container.shell.sync("php -v")
#expect(output.out.contains("Copyright (c) The PHP Group"))
}
@Test func system_shell_has_path() {
let systemShell = container.shell as! RealShell
#expect(systemShell.PATH.contains(":/usr/local/bin"))
#expect(systemShell.PATH.contains(":/usr/bin"))
}
@Test func system_shell_can_buffer_output() async {
var bits: [String] = []
let (_, shellOutput) = try! await container.shell.attach(
"php -r \"echo 'Hello world' . PHP_EOL; usleep(500); echo 'Goodbye world';\"",
didReceiveOutput: { incoming, _ in
bits.append(incoming)
},
withTimeout: 2.0
)
#expect("Hello world\nGoodbye world" == shellOutput.out)
}
@Test func system_shell_can_timeout_and_throw_error() async {
await #expect(throws: ShellError.timedOut) {
try await container.shell.attach(
"php -r \"sleep(1);\"",
didReceiveOutput: { _, _ in },
withTimeout: .seconds(0.1)
)
}
}
@Test func can_run_multiple_shell_commands_in_parallel() async throws {
let start = ContinuousClock.now
await withTaskGroup(of: Void.self) { group in
group.addTask { await container.shell.quiet("php -r \"usleep(700000);\"") }
group.addTask { await container.shell.quiet("php -r \"usleep(700000);\"") }
group.addTask { await container.shell.quiet("php -r \"usleep(700000);\"") }
}
let duration = start.duration(to: .now)
#expect(duration < .milliseconds(2000)) // Should complete in ~700ms if parallel
}
/**
This test verifies that concurrent writes to `output.out` and `output.err`
from multiple readability handlers don't cause data races or crashes,
and that the output is correct (for both stdout and stderr output).
When Thread Sanitizer is enabled, this will also check if any potential
data races occur. None should, at this point. You can enable the
Thread Sanitizer by editing the Test Plan's Configurations.
This test was added specifically to diagnose and fix one such reported
data race, which was fixed by adding a serial queue to the shell's
`attach()` method, since the readability handlers actually run
on separate threads.
*/
@Test func attach_handles_concurrent_stdout_stderr_writes_safely() async throws {
// Create a PHP script that will output lots of text to STDOUT and STDERR.
let phpScript = "php -r 'for ($i = 1; $i <= 500; $i++) { fwrite(STDOUT, \"stdout-$i\" . PHP_EOL); fwrite(STDERR, \"stderr-$i\" . PHP_EOL); flush(); }'"
// Keep track of the total chunk count
var receivedChunks = 0
// We will now test the attach method
let (_, shellOutput) = try await container.shell.attach(
phpScript,
didReceiveOutput: { _, _ in
receivedChunks += 1
},
withTimeout: 5.0
)
// Verify all output was captured without corruption
let stdoutLines = shellOutput.out
.components(separatedBy: "\n")
.filter { !$0.isEmpty }
let stderrLines = shellOutput.err
.components(separatedBy: "\n")
.filter { !$0.isEmpty }
#expect(stdoutLines.count == 500)
#expect(stderrLines.count == 500)
// Verify content integrity
for i in 1...200 {
#expect(stdoutLines.contains("stdout-\(i)"))
#expect(stderrLines.contains("stderr-\(i)"))
}
}
}