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:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user