diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 3e8469f..082bf9d 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -63,6 +63,7 @@ C40FE738282ABA4F00A302C2 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40FE736282ABA4F00A302C2 /* AppVersion.swift */; }; C40FE73B282ABB2E00A302C2 /* AppVersionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40FE739282ABB2E00A302C2 /* AppVersionTest.swift */; }; C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; }; + C413E43528DA3EB100AE33C7 /* ShellTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C413E43428DA3EB100AE33C7 /* ShellTest.swift */; }; C415937F27A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */; }; C415938027A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */; }; C415D3B72770F294005EF286 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3B62770F294005EF286 /* Actions.swift */; }; @@ -357,6 +358,7 @@ C40FE736282ABA4F00A302C2 /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = ""; }; C40FE739282ABB2E00A302C2 /* AppVersionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionTest.swift; sourceTree = ""; }; C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewPackage.swift; sourceTree = ""; }; + C413E43428DA3EB100AE33C7 /* ShellTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellTest.swift; sourceTree = ""; }; C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpFrameworks.swift; sourceTree = ""; }; C415D3B62770F294005EF286 /* Actions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = ""; }; C415D3E72770F692005EF286 /* AppDelegate+InterApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+InterApp.swift"; sourceTree = ""; }; @@ -655,6 +657,14 @@ path = Core; sourceTree = ""; }; + C413E43328DA3E8F00AE33C7 /* Concord */ = { + isa = PBXGroup; + children = ( + C413E43428DA3EB100AE33C7 /* ShellTest.swift */, + ); + path = Concord; + sourceTree = ""; + }; C41C1B2A22B0097F00E7CF16 = { isa = PBXGroup; children = ( @@ -1140,6 +1150,7 @@ C4F7807A25D7F84B000DBC97 /* phpmon-tests */ = { isa = PBXGroup; children = ( + C413E43328DA3E8F00AE33C7 /* Concord */, C4F7807D25D7F84B000DBC97 /* Info.plist */, C43A8A1925D9CD1000591B77 /* Utility.swift */, C40C7F1C27720E1400DDDCDC /* Test Files */, @@ -1290,6 +1301,7 @@ /* Begin PBXShellScriptBuildPhase section */ C4F5FBCB28216985001065C5 /* Run `swiftlint` */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -1460,6 +1472,7 @@ C41CA5EE2774F8EE00A2C80E /* DomainListVC+Actions.swift in Sources */, C4FACE81288F1C0D00FC478F /* PreferencesWindowController+Hotkey.swift in Sources */, 54D9E0B727E4F51E003B9AD9 /* HotKey.swift in Sources */, + C413E43528DA3EB100AE33C7 /* ShellTest.swift in Sources */, C4205A7F27F4D21800191A39 /* ValetProxy.swift in Sources */, C42F26742805B4B400938AC7 /* DomainListable.swift in Sources */, C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */, diff --git a/phpmon-tests/Concord/ShellTest.swift b/phpmon-tests/Concord/ShellTest.swift new file mode 100644 index 0000000..6467068 --- /dev/null +++ b/phpmon-tests/Concord/ShellTest.swift @@ -0,0 +1,32 @@ +// +// ShellTest.swift +// phpmon-tests +// +// Created by Nico Verbruggen on 20/09/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import XCTest + +class ShellTest: XCTestCase { + func test_default_shell_is_system_shell() { + XCTAssertTrue(NewShell.shared is SystemShell) + + XCTAssertTrue(NewShell.shared.syncPipe("php -v") + .contains("Copyright (c) The PHP Group")) + + let expectedPhpOutput = """ + PHP 8.1.10 (cli) (built: Sep 3 2022 12:09:27) (NTS) + 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 + """ + + NewShell.useTestable([ + "php -v": expectedPhpOutput + ]) + + XCTAssertEqual(expectedPhpOutput, NewShell.shared.syncPipe("php -v")) + } +} diff --git a/phpmon/Common/Core/NewShell.swift b/phpmon/Common/Core/NewShell.swift index 2c7d69c..5ee43ea 100644 --- a/phpmon/Common/Core/NewShell.swift +++ b/phpmon/Common/Core/NewShell.swift @@ -9,20 +9,64 @@ import Foundation class NewShell { - static var shared: Shellable! + static var shared: Shellable = SystemShell() - public func useTestable(_ expectations: [String: String]) { + /// Uses a testable shell with predefined responses. You specify the terminal's output. + public static func useTestable(_ expectations: [String: String]) { Self.shared = TestableShell(expectations: expectations) } + + /// Reverts back to the system shell. You do not need to call this, only after using `useTestable()`. + public static func useSystem() { + Self.shared = SystemShell() + } } protocol Shellable { - func pipe(_ command: String) -> String + func syncPipe(_ command: String) -> String + func pipe(_ command: String) async -> String } class SystemShell: Shellable { - func pipe(_ command: String) -> String { - return "shell output" + public var launchPath: String = "/bin/sh" + + public var exports: String = "" + + private func getShellProcess(for command: String) -> Process { + var completeCommand = "" + + // Basic export (PATH) + completeCommand += "export PATH=\(Paths.binPath):$PATH && " + + // Put additional exports in between + if !self.exports.isEmpty { + completeCommand += "\(self.exports) && " + } + + completeCommand += command + + let task = Process() + task.launchPath = self.launchPath + task.arguments = ["--noprofile", "-norc", "--login", "-c", completeCommand] + return task + } + + func syncPipe(_ command: String) -> String { + let task = getShellProcess(for: command) + let pipe = Pipe() + + task.standardOutput = pipe + task.launch() + + return String( + data: pipe.fileHandleForReading.readDataToEndOfFile(), + encoding: .utf8 + ) ?? "" + } + + func pipe(_ command: String) async -> String { + // TODO + return "" } } @@ -33,7 +77,11 @@ class TestableShell: Shellable { var expectations: [String: String] = [:] - func pipe(_ command: String) -> String { + func pipe(_ command: String) async -> String { + return expectations[command] ?? "" + } + + func syncPipe(_ command: String) -> String { return expectations[command] ?? "" } }