1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2026-04-01 01:00:08 +02:00

Make testable modifications possible

This commit is contained in:
2026-02-27 14:51:03 +01:00
parent 7384da4d08
commit a5cca1e09d
5 changed files with 177 additions and 9 deletions

View File

@@ -178,6 +178,31 @@ class TestableFileSystem: FileSystemProtocol {
}
}
// MARK: - Transaction Helpers
func createSymlink(_ path: String, destination: String) {
let path = path.replacingTildeWithHomeDirectory
let destination = destination.replacingTildeWithHomeDirectory
accessQueue.sync {
self.createIntermediateDirectories(path)
self.files[path] = .fake(.symlink, destination)
}
}
func writeFile(_ path: String, content: String, overwrite: Bool) throws {
let path = path.replacingTildeWithHomeDirectory
try accessQueue.sync {
if !overwrite, files[path] != nil {
throw TestableFileSystemError.alreadyExists
}
self.createIntermediateDirectories(path)
self.files[path] = .fake(.text, content)
}
}
// MARK: - Checks
func isExecutableFile(_ path: String) -> Bool {

View File

@@ -11,9 +11,13 @@ import Foundation
final class TrackableTestableShell: TestableShell {
private let commandTracker: CommandTracker
init(expectations: [String: BatchFakeShellOutput], _ commandTracker: CommandTracker) {
init(
expectations: [String: BatchFakeShellOutput],
filesystem: TestableFileSystem?,
_ commandTracker: CommandTracker
) {
self.commandTracker = commandTracker
super.init(expectations: expectations)
super.init(expectations: expectations, filesystem: filesystem)
}
override func sync(_ command: String) -> ShellOutput {

View File

@@ -13,11 +13,13 @@ public class TestableShell: ShellProtocol {
return "/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin"
}
init(expectations: [String: BatchFakeShellOutput]) {
init(expectations: [String: BatchFakeShellOutput], filesystem: TestableFileSystem? = nil) {
self.expectations = expectations
self.filesystem = filesystem
}
var expectations: [String: BatchFakeShellOutput] = [:]
var filesystem: TestableFileSystem?
@discardableResult
func sync(_ command: String) -> ShellOutput {
@@ -28,7 +30,9 @@ public class TestableShell: ShellProtocol {
return .err("No Expected Output")
}
return expectation.syncOutput()
let output = expectation.syncOutput()
applyTransactions(for: expectation)
return output
}
@discardableResult
@@ -66,6 +70,7 @@ public class TestableShell: ShellProtocol {
didReceiveOutput(output, type)
}, ignoreDelay: isRunningTests)
applyTransactions(for: expectation)
return (Process(), output)
}
@@ -73,6 +78,20 @@ public class TestableShell: ShellProtocol {
// does nothing
}
private func applyTransactions(for expectation: BatchFakeShellOutput) {
if !expectation.transactions.isEmpty {
assert(filesystem != nil, "Transactions require a filesystem")
}
guard let filesystem else {
return
}
expectation.transactions.forEach { transaction in
transaction.apply(to: filesystem, shell: self)
}
}
}
struct FakeShellOutput: Codable {
@@ -91,6 +110,7 @@ struct FakeShellOutput: Codable {
struct BatchFakeShellOutput: Codable {
var items: [FakeShellOutput]
var transactions: [FakeShellTransaction] = []
static func with(_ items: [FakeShellOutput]) -> BatchFakeShellOutput {
return BatchFakeShellOutput(items: items)
@@ -122,6 +142,8 @@ struct BatchFakeShellOutput: Codable {
await delay(seconds: item.delay)
}
didReceiveOutput(item.output, item.stream)
if item.stream == .stdErr {
output.err += item.output
} else if item.stream == .stdOut {
@@ -164,3 +186,107 @@ struct BatchFakeShellOutput: Codable {
return await self.output(didReceiveOutput: didReceiveOutput, ignoreDelay: true)
}
}
struct FakeShellTransaction: Codable {
enum TransactionType: String, Codable {
case createSymlink
case writeFile
case remove
case move
case createDirectory
case makeExecutable
case setShellOutput
}
var type: TransactionType
var path: String?
var destination: String?
var content: String?
var overwrite: Bool?
var from: String?
var to: String?
var command: String?
var output: BatchFakeShellOutput?
static func createSymlink(path: String, destination: String) -> FakeShellTransaction {
FakeShellTransaction(type: .createSymlink, path: path, destination: destination)
}
static func symlink(_ path: String, to destination: String) -> FakeShellTransaction {
createSymlink(path: path, destination: destination)
}
static func writeFile(path: String, content: String, overwrite: Bool) -> FakeShellTransaction {
FakeShellTransaction(type: .writeFile, path: path, content: content, overwrite: overwrite)
}
static func file(_ path: String, content: String, overwrite: Bool) -> FakeShellTransaction {
writeFile(path: path, content: content, overwrite: overwrite)
}
static func remove(path: String) -> FakeShellTransaction {
FakeShellTransaction(type: .remove, path: path)
}
static func remove(_ path: String) -> FakeShellTransaction {
remove(path: path)
}
static func move(from: String, to: String) -> FakeShellTransaction {
FakeShellTransaction(type: .move, from: from, to: to)
}
static func move(_ from: String, to: String) -> FakeShellTransaction {
move(from: from, to: to)
}
static func createDirectory(path: String) -> FakeShellTransaction {
FakeShellTransaction(type: .createDirectory, path: path)
}
static func directory(_ path: String) -> FakeShellTransaction {
createDirectory(path: path)
}
static func makeExecutable(path: String) -> FakeShellTransaction {
FakeShellTransaction(type: .makeExecutable, path: path)
}
static func executable(_ path: String) -> FakeShellTransaction {
makeExecutable(path: path)
}
static func setShellOutput(command: String, output: BatchFakeShellOutput) -> FakeShellTransaction {
FakeShellTransaction(type: .setShellOutput, command: command, output: output)
}
static func shellOutput(_ command: String, output: BatchFakeShellOutput) -> FakeShellTransaction {
setShellOutput(command: command, output: output)
}
func apply(to filesystem: TestableFileSystem, shell: TestableShell) {
switch type {
case .createSymlink:
assert(path != nil && destination != nil, "createSymlink requires path and destination")
filesystem.createSymlink(path!, destination: destination!)
case .writeFile:
assert(path != nil && content != nil && overwrite != nil, "writeFile requires path, content, overwrite")
try? filesystem.writeFile(path!, content: content!, overwrite: overwrite!)
case .remove:
assert(path != nil, "remove requires path")
try? filesystem.remove(path!)
case .move:
assert(from != nil && to != nil, "move requires from and to")
try? filesystem.move(from: from!, to: to!)
case .createDirectory:
assert(path != nil, "createDirectory requires path")
try? filesystem.createDirectory(path!, withIntermediateDirectories: true)
case .makeExecutable:
assert(path != nil, "makeExecutable requires path")
try? filesystem.makeExecutable(path!)
case .setShellOutput:
assert(command != nil && output != nil, "setShellOutput requires command and output")
shell.expectations[command!] = output!
}
}
}

View File

@@ -116,16 +116,18 @@ class Container: @unchecked Sendable {
) {
self.commandTracker = CommandTracker()
let filesystem = TestableFileSystem(files: fileSystemFiles)
// Depending on whether we want to fire command tracking, load different handlers
if commandTracking {
self.shell = TrackableTestableShell(expectations: shellExpectations, commandTracker)
self.shell = TrackableTestableShell(expectations: shellExpectations, filesystem: filesystem, commandTracker)
self.command = TrackableTestableCommand(commands: commands, commandTracker)
} else {
self.shell = TestableShell(expectations: shellExpectations)
self.shell = TestableShell(expectations: shellExpectations, filesystem: filesystem)
self.command = TestableCommand(commands: commands)
}
self.filesystem = TestableFileSystem(files: fileSystemFiles)
self.filesystem = filesystem
self.webApi = TestableWebApi(
getResponses: webApiGetResponses,

View File

@@ -73,8 +73,19 @@ final class StartupTest: UITestCase {
final func test_launch_halts_and_automic_fix_can_be_applied() throws {
var configuration = TestableConfigurations.working
// TODO: Make fake shell output closure accessible w/ container so fake tests can manipulate the app's state
configuration.shellOutput["/opt/homebrew/bin/brew link php"] = .delayed(0.5, "Linked PHP.", .stdOut)
configuration.shellOutput["/opt/homebrew/bin/brew link php"] = BatchFakeShellOutput(
items: [.delayed(0.5, "Linked PHP.", .stdOut)],
transactions: [
.symlink(
"/opt/homebrew/bin/php",
to: "/opt/homebrew/Cellar/php/8.4.5/bin/php"
),
.shellOutput(
"/opt/homebrew/bin/brew link php",
output: .instant("PHP already linked.")
)
]
)
configuration.filesystem["/opt/homebrew/bin/php"] = nil // PHP binary must be missing
let app = launch(