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

♻️ All unit tests pass w/ DI container

This commit is contained in:
2025-10-16 14:03:16 +02:00
parent 79a23a2af2
commit 5b63211746
49 changed files with 349 additions and 206 deletions

View File

@@ -13,6 +13,10 @@
031E2B6A2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; }; 031E2B6A2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; };
031E2B6B2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; }; 031E2B6B2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; };
031E2B6C2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; }; 031E2B6C2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; };
031F24802EA1071A00CFB8D9 /* Container+Fake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031F247F2EA1071700CFB8D9 /* Container+Fake.swift */; };
031F24812EA1071A00CFB8D9 /* Container+Fake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031F247F2EA1071700CFB8D9 /* Container+Fake.swift */; };
031F24822EA1071A00CFB8D9 /* Container+Fake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031F247F2EA1071700CFB8D9 /* Container+Fake.swift */; };
031F24832EA1071A00CFB8D9 /* Container+Fake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031F247F2EA1071700CFB8D9 /* Container+Fake.swift */; };
03263A382E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; }; 03263A382E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; };
03263A392E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; }; 03263A392E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; };
03263A3A2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; }; 03263A3A2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; };
@@ -975,6 +979,8 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
0309E6662B0D4B2F002AC007 /* BrewExtensionsObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewExtensionsObservable.swift; sourceTree = "<group>"; }; 0309E6662B0D4B2F002AC007 /* BrewExtensionsObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewExtensionsObservable.swift; sourceTree = "<group>"; };
031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewPhpExtension.swift; sourceTree = "<group>"; }; 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewPhpExtension.swift; sourceTree = "<group>"; };
031F247F2EA1071700CFB8D9 /* Container+Fake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Container+Fake.swift"; sourceTree = "<group>"; };
031F24842EA1132300CFB8D9 /* PHP Monitor.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "PHP Monitor.xctestplan"; sourceTree = "<group>"; };
03263A372E86D5E800BD0415 /* UpdateScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateScheduler.swift; sourceTree = "<group>"; }; 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateScheduler.swift; sourceTree = "<group>"; };
0329A9A02E92A2A800A62A12 /* Container.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Container.swift; sourceTree = "<group>"; }; 0329A9A02E92A2A800A62A12 /* Container.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Container.swift; sourceTree = "<group>"; };
0329A9A22E92A68B00A62A12 /* WarningManager+Evaluations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WarningManager+Evaluations.swift"; sourceTree = "<group>"; }; 0329A9A22E92A68B00A62A12 /* WarningManager+Evaluations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WarningManager+Evaluations.swift"; sourceTree = "<group>"; };
@@ -1609,6 +1615,7 @@
C41C1B3522B0097F00E7CF16 /* phpmon */ = { C41C1B3522B0097F00E7CF16 /* phpmon */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
031F247F2EA1071700CFB8D9 /* Container+Fake.swift */,
0329A9A02E92A2A800A62A12 /* Container.swift */, 0329A9A02E92A2A800A62A12 /* Container.swift */,
C4B5853A2770FE2500DA4FBE /* Common */, C4B5853A2770FE2500DA4FBE /* Common */,
C41E181722CB61EB0072CF09 /* Domain */, C41E181722CB61EB0072CF09 /* Domain */,
@@ -1967,6 +1974,7 @@
C471E79628F9B4260021E251 /* tests */ = { C471E79628F9B4260021E251 /* tests */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
031F24842EA1132300CFB8D9 /* PHP Monitor.xctestplan */,
C4E2E86828FC2FF2003B070C /* Shared */, C4E2E86828FC2FF2003B070C /* Shared */,
C4F7807A25D7F84B000DBC97 /* unit */, C4F7807A25D7F84B000DBC97 /* unit */,
C471E7AE28F9B4940021E251 /* feature */, C471E7AE28F9B4940021E251 /* feature */,
@@ -2848,6 +2856,7 @@
C485707028BF452300539B36 /* PhpDoctorWindowController.swift in Sources */, C485707028BF452300539B36 /* PhpDoctorWindowController.swift in Sources */,
C4CE3BBA27B31F670086CA49 /* ComposerWindow.swift in Sources */, C4CE3BBA27B31F670086CA49 /* ComposerWindow.swift in Sources */,
C40D725A2A018ACC0054A067 /* BusyStatus.swift in Sources */, C40D725A2A018ACC0054A067 /* BusyStatus.swift in Sources */,
031F24802EA1071A00CFB8D9 /* Container+Fake.swift in Sources */,
C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */, C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */,
C4FACE83288F1F9700FC478F /* OnboardingWindowController.swift in Sources */, C4FACE83288F1F9700FC478F /* OnboardingWindowController.swift in Sources */,
C4415E8D2B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */, C4415E8D2B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */,
@@ -2956,6 +2965,7 @@
C471E85A28F9BB650021E251 /* DomainListTypeCell.swift in Sources */, C471E85A28F9BB650021E251 /* DomainListTypeCell.swift in Sources */,
C471E85B28F9BB650021E251 /* DomainListKindCell.swift in Sources */, C471E85B28F9BB650021E251 /* DomainListKindCell.swift in Sources */,
C4611E5E2AEAD2FB0010BE24 /* ConfigManagerView.swift in Sources */, C4611E5E2AEAD2FB0010BE24 /* ConfigManagerView.swift in Sources */,
031F24822EA1071A00CFB8D9 /* Container+Fake.swift in Sources */,
C4BF56AD2949381100379603 /* FakeValetInteractor.swift in Sources */, C4BF56AD2949381100379603 /* FakeValetInteractor.swift in Sources */,
C471E85C28F9BB650021E251 /* DomainListWindowController.swift in Sources */, C471E85C28F9BB650021E251 /* DomainListWindowController.swift in Sources */,
C471E85D28F9BB650021E251 /* DomainListVC.swift in Sources */, C471E85D28F9BB650021E251 /* DomainListVC.swift in Sources */,
@@ -3255,6 +3265,7 @@
C471E7FC28F9BACE0021E251 /* HomebrewDecodable.swift in Sources */, C471E7FC28F9BACE0021E251 /* HomebrewDecodable.swift in Sources */,
C4BB393C2981AFC700F8E797 /* PhpVersionSource.swift in Sources */, C4BB393C2981AFC700F8E797 /* PhpVersionSource.swift in Sources */,
C471E7F628F9BAC80021E251 /* PhpHelper.swift in Sources */, C471E7F628F9BAC80021E251 /* PhpHelper.swift in Sources */,
031F24812EA1071A00CFB8D9 /* Container+Fake.swift in Sources */,
039E1D7B2E5F0F300072D13D /* ValetUpgrader.swift in Sources */, 039E1D7B2E5F0F300072D13D /* ValetUpgrader.swift in Sources */,
C471E7EE28F9BAC30021E251 /* Constants.swift in Sources */, C471E7EE28F9BAC30021E251 /* Constants.swift in Sources */,
C40934A0298EE8E900D25014 /* AppUpdater.swift in Sources */, C40934A0298EE8E900D25014 /* AppUpdater.swift in Sources */,
@@ -3419,6 +3430,7 @@
C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */, C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */,
C4159AF728E4D40400545349 /* RealShellTest.swift in Sources */, C4159AF728E4D40400545349 /* RealShellTest.swift in Sources */,
C450C8C728C919EC002A2B4B /* PreferenceName.swift in Sources */, C450C8C728C919EC002A2B4B /* PreferenceName.swift in Sources */,
031F24832EA1071A00CFB8D9 /* Container+Fake.swift in Sources */,
C40D725B2A018ACC0054A067 /* BusyStatus.swift in Sources */, C40D725B2A018ACC0054A067 /* BusyStatus.swift in Sources */,
032DAC292E8BEB5B0018E01C /* RealApi.swift in Sources */, 032DAC292E8BEB5B0018E01C /* RealApi.swift in Sources */,
C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */, C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */,

View File

@@ -26,11 +26,16 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES">
shouldAutocreateTestPlan = "YES"> <TestPlans>
<TestPlanReference
reference = "container:tests/PHP Monitor.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables> <Testables>
<TestableReference <TestableReference
skipped = "NO"> skipped = "YES">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "C4F7807825D7F84B000DBC97" BlueprintIdentifier = "C4F7807825D7F84B000DBC97"
@@ -40,7 +45,7 @@
</BuildableReference> </BuildableReference>
</TestableReference> </TestableReference>
<TestableReference <TestableReference
skipped = "NO" skipped = "YES"
parallelizable = "YES"> parallelizable = "YES">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"

View File

@@ -18,30 +18,30 @@ class Actions {
// MARK: - Services // MARK: - Services
public func linkPhp() async { public func linkPhp() async {
await brew("link php --overwrite --force") await brew(container, "link php --overwrite --force")
} }
public func restartPhpFpm() async { public func restartPhpFpm() async {
await brew("services restart \(formulae.php)", sudo: formulae.php.elevated) await brew(container, "services restart \(formulae.php)", sudo: formulae.php.elevated)
} }
public func restartPhpFpm(version: String) async { public func restartPhpFpm(version: String) async {
let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)" let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)"
await brew("services restart \(formula)", sudo: formulae.php.elevated) await brew(container, "services restart \(formula)", sudo: formulae.php.elevated)
} }
public func restartNginx() async { public func restartNginx() async {
await brew("services restart \(formulae.nginx)", sudo: formulae.nginx.elevated) await brew(container, "services restart \(formulae.nginx)", sudo: formulae.nginx.elevated)
} }
public func restartDnsMasq() async { public func restartDnsMasq() async {
await brew("services restart \(formulae.dnsmasq)", sudo: formulae.dnsmasq.elevated) await brew(container, "services restart \(formulae.dnsmasq)", sudo: formulae.dnsmasq.elevated)
} }
public func stopValetServices() async { public func stopValetServices() async {
await brew("services stop \(formulae.php)", sudo: formulae.php.elevated) await brew(container, "services stop \(formulae.php)", sudo: formulae.php.elevated)
await brew("services stop \(formulae.nginx)", sudo: formulae.nginx.elevated) await brew(container, "services stop \(formulae.nginx)", sudo: formulae.nginx.elevated)
await brew("services stop \(formulae.dnsmasq)", sudo: formulae.dnsmasq.elevated) await brew(container, "services stop \(formulae.dnsmasq)", sudo: formulae.dnsmasq.elevated)
} }
public func fixHomebrewPermissions() throws { public func fixHomebrewPermissions() throws {
@@ -134,8 +134,8 @@ class Actions {
*/ */
public func fixMyValet() async { public func fixMyValet() async {
await InternalSwitcher().performSwitch(to: PhpEnvironments.brewPhpAlias) await InternalSwitcher().performSwitch(to: PhpEnvironments.brewPhpAlias)
await brew("services restart \(formulae.dnsmasq)", sudo: formulae.dnsmasq.elevated) await brew(container, "services restart \(formulae.dnsmasq)", sudo: formulae.dnsmasq.elevated)
await brew("services restart \(formulae.php)", sudo: formulae.php.elevated) await brew(container, "services restart \(formulae.php)", sudo: formulae.php.elevated)
await brew("services restart \(formulae.nginx)", sudo: formulae.nginx.elevated) await brew(container, "services restart \(formulae.nginx)", sudo: formulae.nginx.elevated)
} }
} }

View File

@@ -14,9 +14,9 @@ import Foundation
Runs a `brew` command. Can run as superuser. Runs a `brew` command. Can run as superuser.
*/ */
func brew( func brew(
_ container: Container = App.shared.container,
_ command: String, _ command: String,
sudo: Bool = false, sudo: Bool = false,
container: Container = App.shared.container,
) async { ) async {
await container.shell.quiet("\(sudo ? "sudo " : "")" + "\(container.paths.brew) \(command)") await container.shell.quiet("\(sudo ? "sudo " : "")" + "\(container.paths.brew) \(command)")
} }
@@ -25,12 +25,10 @@ func brew(
Runs `sed` in order to replace all occurrences of a string in a specific file with another. Runs `sed` in order to replace all occurrences of a string in a specific file with another.
*/ */
func sed( func sed(
_ container: Container = App.shared.container,
file: String, file: String,
original: String, original: String,
replacement: String, replacement: String
filesystem: FileSystemProtocol = App.shared.container.filesystem,
shell: ShellProtocol = App.shared.container.shell,
paths: Paths = App.shared.container.paths,
) async { ) async {
// Escape slashes (or `sed` won't work) // Escape slashes (or `sed` won't work)
let e_original = original.replacingOccurrences(of: "/", with: "\\/") let e_original = original.replacingOccurrences(of: "/", with: "\\/")
@@ -38,17 +36,21 @@ func sed(
// Check if gsed exists; it is able to follow symlinks, // Check if gsed exists; it is able to follow symlinks,
// which we want to do to toggle the extension // which we want to do to toggle the extension
if filesystem.fileExists("\(paths.binPath)/gsed") { if container.filesystem.fileExists("\(container.paths.binPath)/gsed") {
await shell.quiet("\(paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)") await container.shell.quiet("\(container.paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
} else { } else {
await shell.quiet("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)") await container.shell.quiet("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)")
} }
} }
/** /**
Uses `grep` to determine whether a particular query string can be found in a particular file. Uses `grep` to determine whether a particular query string can be found in a particular file.
*/ */
func grepContains(file: String, query: String, shell: ShellProtocol = App.shared.container.shell) async -> Bool { func grepContains(
shell: ShellProtocol = App.shared.container.shell,
file: String,
query: String
) async -> Bool {
return await shell.pipe(""" return await shell.pipe("""
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO" grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
""").out """).out

View File

@@ -10,7 +10,12 @@ import Foundation
extension String { extension String {
var replacingTildeWithHomeDirectory: String { var replacingTildeWithHomeDirectory: String {
return self.replacingOccurrences(of: "~", with: App.shared.container.paths.homePath) // Try and check if there's a shared container
if let paths = App.shared.container.paths {
return self.replacingOccurrences(of: "~", with: paths.homePath)
}
return self
} }
} }

View File

@@ -40,14 +40,14 @@ class ActivePhpInstallation {
// MARK: - Initializer // MARK: - Initializer
public static func load() -> ActivePhpInstallation? { public static func load(
let container = App.shared.container container: Container = App.shared.container
) -> ActivePhpInstallation? {
if !container.filesystem.fileExists(container.paths.phpConfig) { if !container.filesystem.fileExists(container.paths.phpConfig) {
return nil return nil
} }
return ActivePhpInstallation() return ActivePhpInstallation(container: container)
} }
init(container: Container = App.shared.container) { init(container: Container = App.shared.container) {
@@ -83,7 +83,7 @@ class ActivePhpInstallation {
// See if any extensions are present in said .ini files // See if any extensions are present in said .ini files
paths.forEach { (iniFilePath) in paths.forEach { (iniFilePath) in
if let file = PhpConfigurationFile.from(filePath: iniFilePath) { if let file = PhpConfigurationFile.from(container, filePath: iniFilePath) {
iniFiles.append(file) iniFiles.append(file)
} }
} }

View File

@@ -18,7 +18,7 @@ class PhpEnvironments {
*/ */
init(container: Container = App.shared.container) { init(container: Container = App.shared.container) {
self.container = container self.container = container
self.currentInstall = ActivePhpInstallation.load() self.currentInstall = ActivePhpInstallation.load(container: container)
} }
/** /**
@@ -170,7 +170,7 @@ class PhpEnvironments {
// Avoid inserting a duplicate // Avoid inserting a duplicate
if !supportedVersions.contains(phpAlias) && container.filesystem.fileExists("\(container.paths.optPath)/php/bin/php") { if !supportedVersions.contains(phpAlias) && container.filesystem.fileExists("\(container.paths.optPath)/php/bin/php") {
let phpAliasInstall = PhpInstallation(phpAlias) let phpAliasInstall = PhpInstallation(container, phpAlias)
// Before inserting, ensure that the actual output matches the alias // Before inserting, ensure that the actual output matches the alias
// if that isn't the case, our formula remains out-of-date // if that isn't the case, our formula remains out-of-date
if !phpAliasInstall.isMissingBinary { if !phpAliasInstall.isMissingBinary {
@@ -190,7 +190,7 @@ class PhpEnvironments {
var mappedVersions: [String: PhpInstallation] = [:] var mappedVersions: [String: PhpInstallation] = [:]
availablePhpVersions.forEach { version in availablePhpVersions.forEach { version in
mappedVersions[version] = PhpInstallation(version) mappedVersions[version] = PhpInstallation(container, version)
} }
cachedPhpInstallations = mappedVersions cachedPhpInstallations = mappedVersions

View File

@@ -9,6 +9,7 @@
import Foundation import Foundation
class PhpConfigurationFile: CreatedFromFile { class PhpConfigurationFile: CreatedFromFile {
var container: Container
struct ConfigValue { struct ConfigValue {
let lineIndex: Int let lineIndex: Int
@@ -32,24 +33,25 @@ class PhpConfigurationFile: CreatedFromFile {
/** Resolves a PHP configuration file (.ini) */ /** Resolves a PHP configuration file (.ini) */
static func from( static func from(
filePath: String, _ container: Container,
container: Container = App.shared.container filePath: String
) -> Self? { ) -> Self? {
let path = filePath.replacingOccurrences(of: "~", with: container.paths.homePath) let path = filePath.replacingOccurrences(of: "~", with: container.paths.homePath)
do { do {
let fileContents = try App.shared.container.filesystem.getStringFromFile(path) let fileContents = try container.filesystem.getStringFromFile(path)
return Self.init(path: path, contents: fileContents) return Self.init(container, path: path, contents: fileContents)
} catch { } catch {
Log.warn("Could not read the PHP configuration file at: `\(filePath)`") Log.warn("Could not read the PHP configuration file at: `\(filePath)`")
return nil return nil
} }
} }
required init(path: String, contents: String) { required init(_ container: Container, path: String, contents: String) {
self.container = container
self.filePath = path self.filePath = path
self.lines = contents.components(separatedBy: "\n") self.lines = contents.components(separatedBy: "\n")
self.extensions = PhpExtension.from(lines, filePath: path) self.extensions = PhpExtension.from(container, lines, filePath: path)
self.content = Self.parseConfig(lines: lines) self.content = Self.parseConfig(lines: lines)
} }
@@ -116,7 +118,7 @@ class PhpConfigurationFile: CreatedFromFile {
public func reload() { public func reload() {
self.lines = try! String(contentsOfFile: self.filePath) self.lines = try! String(contentsOfFile: self.filePath)
.components(separatedBy: "\n") .components(separatedBy: "\n")
self.extensions = PhpExtension.from(lines, filePath: self.filePath) self.extensions = PhpExtension.from(container, lines, filePath: self.filePath)
self.content = Self.parseConfig(lines: lines) self.content = Self.parseConfig(lines: lines)
} }

View File

@@ -7,6 +7,7 @@
// //
import Foundation import Foundation
import ContainerMacro
/** /**
A PHP extension that was detected in the php.ini file. A PHP extension that was detected in the php.ini file.
@@ -15,8 +16,8 @@ import Foundation
- Note: You need to know more about regular expressions to be able to deal with these NSRegularExpression - Note: You need to know more about regular expressions to be able to deal with these NSRegularExpression
instances. You can find more information here: https://nshipster.com/swift-regular-expressions/ instances. You can find more information here: https://nshipster.com/swift-regular-expressions/
*/ */
@ContainerAccess
class PhpExtension { class PhpExtension {
/// The file where this extension was located. /// The file where this extension was located.
var file: String var file: String
@@ -54,7 +55,9 @@ class PhpExtension {
/** /**
When registering an extension, we do that based on the line found inside the .ini file. When registering an extension, we do that based on the line found inside the .ini file.
*/ */
init(_ line: String, file: String) { init(_ container: Container, _ line: String, file: String) {
self.container = container
let regex = try! NSRegularExpression(pattern: Self.extensionRegex, options: []) let regex = try! NSRegularExpression(pattern: Self.extensionRegex, options: [])
let match = regex.matches(in: line, options: [], range: NSRange(location: 0, length: line.count)).first let match = regex.matches(in: line, options: [], range: NSRange(location: 0, length: line.count)).first
let range = Range(match!.range(withName: "name"), in: line)! let range = Range(match!.range(withName: "name"), in: line)!
@@ -82,7 +85,7 @@ class PhpExtension {
// ENABLED: Line where the comment delimiter (;) is removed // ENABLED: Line where the comment delimiter (;) is removed
: line.replacingOccurrences(of: "; ", with: "") : line.replacingOccurrences(of: "; ", with: "")
await sed(file: file, original: line, replacement: newLine) await sed(container, file: file, original: line, replacement: newLine)
self.enabled = !newLine.starts(with: ";") self.enabled = !newLine.starts(with: ";")
self.line = newLine self.line = newLine
@@ -96,15 +99,15 @@ class PhpExtension {
// MARK: - Static Methods // MARK: - Static Methods
static func from(_ lines: [String], filePath: String) -> [PhpExtension] { static func from(_ container: Container, _ lines: [String], filePath: String) -> [PhpExtension] {
return lines.filter { return lines.filter {
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
}.map { }.map {
return PhpExtension($0, file: filePath) return PhpExtension(container, $0, file: filePath)
} }
} }
static func from(filePath: String) -> [PhpExtension] { static func from(_ container: Container, filePath: String) -> [PhpExtension] {
let file = try? String(contentsOfFile: filePath) let file = try? String(contentsOfFile: filePath)
if file == nil { if file == nil {
@@ -113,6 +116,7 @@ class PhpExtension {
} }
return Self.from( return Self.from(
container,
file!.components(separatedBy: "\n"), file!.components(separatedBy: "\n"),
filePath: filePath filePath: filePath
) )

View File

@@ -11,7 +11,6 @@ import ContainerMacro
@ContainerAccess @ContainerAccess
class PhpInstallation { class PhpInstallation {
var versionNumber: VersionNumber var versionNumber: VersionNumber
var iniFiles: [PhpConfigurationFile] = [] var iniFiles: [PhpConfigurationFile] = []
@@ -40,7 +39,7 @@ class PhpInstallation {
In order to determine details about a PHP installation, In order to determine details about a PHP installation,
well simply run `php-config --version` in the relevant directory. well simply run `php-config --version` in the relevant directory.
*/ */
init(container: Container = App.shared.container, _ version: String) { init(_ container: Container, _ version: String) {
self.container = container self.container = container
let phpConfigExecutablePath = "\(container.paths.optPath)/php@\(version)/bin/php-config", let phpConfigExecutablePath = "\(container.paths.optPath)/php@\(version)/bin/php-config",
@@ -105,7 +104,7 @@ class PhpInstallation {
// See if any extensions are present in said .ini files // See if any extensions are present in said .ini files
paths.forEach { (iniFilePath) in paths.forEach { (iniFilePath) in
if let file = PhpConfigurationFile.from(filePath: iniFilePath) { if let file = PhpConfigurationFile.from(container, filePath: iniFilePath) {
iniFiles.append(file) iniFiles.append(file)
} }
} }

View File

@@ -53,7 +53,7 @@ class InternalSwitcher: PhpSwitcher {
if Valet.installed { if Valet.installed {
Log.info("Restarting nginx, just to be sure!") Log.info("Restarting nginx, just to be sure!")
await brew("services restart nginx", sudo: true) await brew(container, "services restart nginx", sudo: true)
} }
Log.info("The new version(s) have been linked!") Log.info("The new version(s) have been linked!")
@@ -78,10 +78,10 @@ class InternalSwitcher: PhpSwitcher {
func unlinkAndStopPhpVersion(_ version: String) async { func unlinkAndStopPhpVersion(_ version: String) async {
let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)" let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)"
await brew("unlink \(formula)") await brew(container, "unlink \(formula)")
if Valet.installed { if Valet.installed {
await brew("services stop \(formula)", sudo: true) await brew(container, "services stop \(formula)", sudo: true)
Log.info("Unlinked and stopped services for \(formula)") Log.info("Unlinked and stopped services for \(formula)")
} else { } else {
Log.info("Unlinked \(formula)") Log.info("Unlinked \(formula)")
@@ -93,13 +93,13 @@ class InternalSwitcher: PhpSwitcher {
if primary { if primary {
Log.info("\(formula) is the primary formula, linking...") Log.info("\(formula) is the primary formula, linking...")
await brew("link \(formula) --overwrite --force") await brew(container, "link \(formula) --overwrite --force")
} else { } else {
Log.info("\(formula) is an isolated PHP version, not linking!") Log.info("\(formula) is an isolated PHP version, not linking!")
} }
if Valet.installed { if Valet.installed {
await brew("services start \(formula)", sudo: true) await brew(container, "services start \(formula)", sudo: true)
if Valet.enabled(feature: .isolatedSites) && primary { if Valet.enabled(feature: .isolatedSites) && primary {
let socketVersion = version.replacingOccurrences(of: ".", with: "") let socketVersion = version.replacingOccurrences(of: ".", with: "")

View File

@@ -9,7 +9,5 @@
import Foundation import Foundation
protocol CreatedFromFile { protocol CreatedFromFile {
static func from(_ container: Container, filePath: String) -> Self?
static func from(filePath: String, container: Container) -> Self?
} }

View File

@@ -0,0 +1,34 @@
//
// Container+Fake.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/10/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
extension Container {
public func overrideFake(
shellExpectations: [String: BatchFakeShellOutput] = [:],
fileSystemFiles: [String: FakeFile] = [:],
commands: [String: String] = [:]
) {
self.shell = TestableShell(expectations: shellExpectations)
self.filesystem = TestableFileSystem(files: fileSystemFiles)
self.command = TestableCommand(commands: commands)
}
public static func fake(
shell: [String: BatchFakeShellOutput] = [:],
files: [String: FakeFile] = [:],
commands: [String: String] = [:]
) -> Container {
let container = Container()
container.prepare()
container.overrideFake(
shellExpectations: shell,
fileSystemFiles: files,
commands: commands
)
return container
}
}

View File

@@ -37,9 +37,9 @@ class Container {
self.command = TestableCommand(commands: config.commandOutput) self.command = TestableCommand(commands: config.commandOutput)
} }
public func overrideFake() { public static func real() -> Container {
self.shell = TestableShell(expectations: [:]) let container = Container()
self.filesystem = TestableFileSystem(files: [:]) container.prepare()
self.command = TestableCommand(commands: [:]) return container
} }
} }

View File

@@ -74,7 +74,7 @@ class App {
The dependency container. The dependency container.
This is supposed to be injected, so direct access is discouraged. This is supposed to be injected, so direct access is discouraged.
*/ */
let container = Container() var container: Container = Container()
/** The list of preferences that are currently active. */ /** The list of preferences that are currently active. */
var preferences: [PreferenceName: Bool]! var preferences: [PreferenceName: Bool]!

View File

@@ -28,7 +28,7 @@ class AppUpdater {
let caskUrl = Constants.Urls.UpdateCheckEndpoint let caskUrl = Constants.Urls.UpdateCheckEndpoint
guard let caskFile = await CaskFile.from(url: caskUrl) else { guard let caskFile = await CaskFile.from(App.shared.container, url: caskUrl) else {
Log.err("The contents of the CaskFile at '\(caskUrl.absoluteString)' could not be retrieved.") Log.err("The contents of the CaskFile at '\(caskUrl.absoluteString)' could not be retrieved.")
presentCouldNotRetrieveUpdateIfInteractive() presentCouldNotRetrieveUpdateIfInteractive()
return .networkError return .networkError

View File

@@ -110,6 +110,7 @@ class ValetServicesManager: ServicesManager {
// Run the command // Run the command
await brew( await brew(
container,
"services \(action) \(wrapper.formula.name)", "services \(action) \(wrapper.formula.name)",
sudo: wrapper.formula.elevated sudo: wrapper.formula.elevated
) )

View File

@@ -30,16 +30,18 @@ struct BrewPhpExtension: Hashable, Comparable {
return "\(name)@\(phpVersion)" return "\(name)@\(phpVersion)"
} }
init(path: String, name: String, phpVersion: String) { init(_ container: Container, path: String, name: String, phpVersion: String) {
self.path = path self.path = path
self.name = name self.name = name
self.phpVersion = phpVersion self.phpVersion = phpVersion
self.isInstalled = BrewPhpExtension.hasInstallationReceipt( self.isInstalled = BrewPhpExtension.hasInstallationReceipt(
for: "\(name)@\(phpVersion)" container, for: "\(name)@\(phpVersion)"
) )
self.dependencies = BrewPhpExtension.extractDependencies(from: path) self.dependencies = BrewPhpExtension.extractDependencies(
container, from: path
)
} }
var hasAlternativeInstall: Bool { var hasAlternativeInstall: Bool {
@@ -58,8 +60,8 @@ struct BrewPhpExtension: Hashable, Comparable {
.first { $0.dependencies.contains("shivammathur/extensions/\(self.formulaName)") } .first { $0.dependencies.contains("shivammathur/extensions/\(self.formulaName)") }
} }
static func hasInstallationReceipt(for formulaName: String) -> Bool { static func hasInstallationReceipt(_ container: Container, for formulaName: String) -> Bool {
return App.shared.container.filesystem.fileExists("\(App.shared.container.paths.optPath)/\(formulaName)/INSTALL_RECEIPT.json") return container.filesystem.fileExists("\(container.paths.optPath)/\(formulaName)/INSTALL_RECEIPT.json")
} }
static func < (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool { static func < (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool {
@@ -70,11 +72,11 @@ struct BrewPhpExtension: Hashable, Comparable {
return lhs.name == rhs.name return lhs.name == rhs.name
} }
private static func extractDependencies(from path: String) -> [String] { private static func extractDependencies(_ container: Container, from path: String) -> [String] {
let regexPattern = #"depends_on "(.*)""# let regexPattern = #"depends_on "(.*)""#
var dependencies: [String] = [] var dependencies: [String] = []
guard let content = try? App.shared.container.filesystem.getStringFromFile(path) else { guard let content = try? container.filesystem.getStringFromFile(path) else {
return [] return []
} }

View File

@@ -10,9 +10,8 @@ import Foundation
import ContainerMacro import ContainerMacro
struct BrewPhpFormula: Equatable { struct BrewPhpFormula: Equatable {
var container: Container { /// The dependency container.
return App.shared.container let container: Container
}
/// Name of the formula. /// Name of the formula.
let name: String let name: String
@@ -38,12 +37,14 @@ struct BrewPhpFormula: Equatable {
} }
init( init(
_ container: Container,
name: String, name: String,
displayName: String, displayName: String,
installedVersion: String?, installedVersion: String?,
upgradeVersion: String?, upgradeVersion: String?,
prerelease: Bool = false prerelease: Bool = false
) { ) {
self.container = container
self.name = name self.name = name
self.displayName = displayName self.displayName = displayName
self.installedVersion = installedVersion self.installedVersion = installedVersion

View File

@@ -63,6 +63,7 @@ class BrewPhpFormulaeHandler: HandlesBrewPhpFormulae {
} }
return BrewPhpFormula( return BrewPhpFormula(
container,
name: formula, name: formula,
displayName: "PHP \(version)", displayName: "PHP \(version)",
installedVersion: fullVersion, installedVersion: fullVersion,

View File

@@ -9,10 +9,10 @@
import Foundation import Foundation
class BrewTapFormulae { class BrewTapFormulae {
public static func from(tap: String) -> [String: [BrewPhpExtension]] { public static func from(_ container: Container, tap: String) -> [String: [BrewPhpExtension]] {
let directory = "\(App.shared.container.paths.tapPath)/\(tap)/Formula" let directory = "\(container.paths.tapPath)/\(tap)/Formula"
let files = try? App.shared.container.filesystem.getShallowContentsOfDirectory(directory) let files = try? container.filesystem.getShallowContentsOfDirectory(directory)
var availableExtensions = [String: [BrewPhpExtension]]() var availableExtensions = [String: [BrewPhpExtension]]()
@@ -35,7 +35,8 @@ class BrewTapFormulae {
// Create a new BrewPhpExtension object (determines if installed) // Create a new BrewPhpExtension object (determines if installed)
let phpExtension = BrewPhpExtension( let phpExtension = BrewPhpExtension(
path: "\(App.shared.container.paths.tapPath)/\(tap)/Formula/\(file)", container,
path: "\(container.paths.tapPath)/\(tap)/Formula/\(file)",
name: phpExtensionName, name: phpExtensionName,
phpVersion: phpVersion phpVersion: phpVersion
) )

View File

@@ -24,11 +24,11 @@ struct CaskFile {
return self.properties["version"]! return self.properties["version"]!
} }
private static func loadFromApi(_ url: URL) async -> String { private static func loadFromApi(_ container: Container, _ url: URL) async -> String {
if App.hasLoadedTestableConfiguration || url.absoluteString.contains("https://raw.githubusercontent.com") { if isRunningTests || App.hasLoadedTestableConfiguration || url.absoluteString.contains("https://raw.githubusercontent.com") {
return await App.shared.container.shell.pipe("curl -s --max-time 10 '\(url.absoluteString)'").out return await container.shell.pipe("curl -s --max-time 10 '\(url.absoluteString)'").out
} else { } else {
return await App.shared.container.shell.pipe(""" return await container.shell.pipe("""
curl -s --max-time 10 \ curl -s --max-time 10 \
-H "User-Agent: phpmon-curl/1.0" \ -H "User-Agent: phpmon-curl/1.0" \
-H "X-phpmon-version: \(App.shortVersion) (\(App.bundleVersion))" \ -H "X-phpmon-version: \(App.shortVersion) (\(App.bundleVersion))" \
@@ -39,13 +39,13 @@ struct CaskFile {
} }
} }
public static func from(url: URL) async -> CaskFile? { public static func from(_ container: Container, url: URL) async -> CaskFile? {
var string: String? var string: String?
if url.scheme == "file" { if url.scheme == "file" {
string = try? String(contentsOf: url) string = try? String(contentsOf: url)
} else { } else {
string = await CaskFile.loadFromApi(url) string = await CaskFile.loadFromApi(container, url)
} }
guard let string else { guard let string else {

View File

@@ -9,7 +9,6 @@
import Foundation import Foundation
class NginxConfigurationFile: CreatedFromFile { class NginxConfigurationFile: CreatedFromFile {
/// Contents of the Nginx file in question, as a string. /// Contents of the Nginx file in question, as a string.
var contents: String! var contents: String!
@@ -21,8 +20,8 @@ class NginxConfigurationFile: CreatedFromFile {
/** Resolves an nginx configuration file (.conf) */ /** Resolves an nginx configuration file (.conf) */
static func from( static func from(
_ container: Container,
filePath: String, filePath: String,
container: Container = App.shared.container
) -> Self? { ) -> Self? {
let path = filePath.replacingOccurrences(of: "~", with: container.paths.homePath) let path = filePath.replacingOccurrences(of: "~", with: container.paths.homePath)

View File

@@ -87,7 +87,7 @@ class ValetDomainScanner: DomainScanner {
private func isSite(_ entry: String, forPath path: String) -> Bool { private func isSite(_ entry: String, forPath path: String) -> Bool {
let siteDir = path + "/" + entry let siteDir = path + "/" + entry
return (App.shared.container.filesystem.isDirectory(siteDir) || App.shared.container.filesystem.isSymlink(siteDir)) return (container.filesystem.isDirectory(siteDir) || container.filesystem.isSymlink(siteDir))
} }
// MARK: - Proxies // MARK: - Proxies
@@ -100,7 +100,7 @@ class ValetDomainScanner: DomainScanner {
return !$0.starts(with: ".") return !$0.starts(with: ".")
} }
.compactMap { .compactMap {
return NginxConfigurationFile.from(filePath: "\(directoryPath)/\($0)") return NginxConfigurationFile.from(container, filePath: "\(directoryPath)/\($0)")
} }
.filter { .filter {
return $0.proxy != nil return $0.proxy != nil

View File

@@ -40,7 +40,7 @@ class FakeValetSite: ValetSite {
} }
if let isolated = isolated { if let isolated = isolated {
self.isolatedPhpVersion = PhpInstallation(isolated) self.isolatedPhpVersion = PhpInstallation(container, isolated)
} }
if container.phpEnvs.currentInstall != nil { if container.phpEnvs.currentInstall != nil {

View File

@@ -280,7 +280,7 @@ class ValetSite: ValetListable {
) -> String? { ) -> String? {
if container.filesystem.fileExists(filePath) { if container.filesystem.fileExists(filePath) {
return NginxConfigurationFile return NginxConfigurationFile
.from(filePath: filePath)? .from(container, filePath: filePath)?
.isolatedVersion ?? nil .isolatedVersion ?? nil
} }

View File

@@ -36,7 +36,7 @@ class BytePhpPreference: PhpPreference {
didSet { updatedFieldValue() } didSet { updatedFieldValue() }
} }
override init(container: Container = App.shared.container, key: String) { override init(_ container: Container = App.shared.container, key: String) {
let value = container.command.execute( let value = container.command.execute(
path: container.paths.php, arguments: ["-r", "echo ini_get('\(key)');"], path: container.paths.php, arguments: ["-r", "echo ini_get('\(key)');"],
trimNewlines: false trimNewlines: false
@@ -48,7 +48,7 @@ class BytePhpPreference: PhpPreference {
self.value = value self.value = value
} }
super.init(container: container, key: key) super.init(container, key: key)
} }
// MARK: Save Value // MARK: Save Value

View File

@@ -14,7 +14,7 @@ import ContainerMacro
class PhpPreference { class PhpPreference {
let key: String let key: String
init(container: Container = App.shared.container, key: String) { init(_ container: Container = App.shared.container, key: String) {
self.container = container self.container = container
self.key = key self.key = key
} }

View File

@@ -12,7 +12,7 @@ extension WarningManager {
return [ return [
Warning( Warning(
command: { command: {
return await App.shared.container.shell.pipe("sysctl -n sysctl.proc_translated").out return await self.container.shell.pipe("sysctl -n sysctl.proc_translated").out
.trimmingCharacters(in: .whitespacesAndNewlines) == "1" .trimmingCharacters(in: .whitespacesAndNewlines) == "1"
}, },
name: "Running PHP Monitor with Rosetta on Apple Silicon", name: "Running PHP Monitor with Rosetta on Apple Silicon",
@@ -23,8 +23,8 @@ extension WarningManager {
), ),
Warning( Warning(
command: { command: {
return !App.shared.container.shell.PATH.contains("\(App.shared.container.paths.homePath)/.config/phpmon/bin") && return !self.container.shell.PATH.contains("\(self.container.paths.homePath)/.config/phpmon/bin") &&
!App.shared.container.filesystem.isWriteableFile("/usr/local/bin/") !self.container.filesystem.isWriteableFile("/usr/local/bin/")
}, },
name: "Helpers cannot be symlinked and not in PATH", name: "Helpers cannot be symlinked and not in PATH",
title: "warnings.helper_permissions.title", title: "warnings.helper_permissions.title",
@@ -34,7 +34,7 @@ extension WarningManager {
"warnings.helper_permissions.symlink" "warnings.helper_permissions.symlink"
] }, ] },
url: "https://github.com/nicoverbruggen/phpmon/wiki/PHP-Monitor-helper-binaries", url: "https://github.com/nicoverbruggen/phpmon/wiki/PHP-Monitor-helper-binaries",
fix: App.shared.container.paths.shell == "/bin/zsh" ? { fix: self.container.paths.shell == "/bin/zsh" ? {
// Add to PATH // Add to PATH
await ZshRunCommand().addPhpMonitorPath() await ZshRunCommand().addPhpMonitorPath()
// Finally, perform environment checks again // Finally, perform environment checks again
@@ -43,7 +43,7 @@ extension WarningManager {
), ),
Warning( Warning(
command: { command: {
App.shared.container.phpEnvs.currentInstall?.extensions.contains { $0.name == "xdebug" } ?? false self.container.phpEnvs.currentInstall?.extensions.contains { $0.name == "xdebug" } ?? false
&& !Xdebug().enabled && !Xdebug().enabled
}, },
name: "Missing configuration file for `xdebug.mode`", name: "Missing configuration file for `xdebug.mode`",
@@ -53,17 +53,17 @@ extension WarningManager {
] }, ] },
url: "https://xdebug.org/docs/install#mode", url: "https://xdebug.org/docs/install#mode",
fix: { fix: {
if let php = App.shared.container.phpEnvs.currentInstall { if let php = self.container.phpEnvs.currentInstall {
if let xdebug = php.extensions.first(where: { $0.name == "xdebug" }), if let xdebug = php.extensions.first(where: { $0.name == "xdebug" }),
let original = try? App.shared.container.filesystem.getStringFromFile(xdebug.file) { let original = try? self.container.filesystem.getStringFromFile(xdebug.file) {
// Append xdebug.mode = off to the file // Append xdebug.mode = off to the file
try? App.shared.container.filesystem.writeAtomicallyToFile( try? self.container.filesystem.writeAtomicallyToFile(
xdebug.file, xdebug.file,
content: original + "\nxdebug.mode = off" content: original + "\nxdebug.mode = off"
) )
// Reload extension configuration by updating PHP installation info (reload) // Reload extension configuration by updating PHP installation info (reload)
App.shared.container.phpEnvs.currentInstall = ActivePhpInstallation() self.container.phpEnvs.currentInstall = ActivePhpInstallation()
// Finally, reload warnings // Finally, reload warnings
await self.checkEnvironment() await self.checkEnvironment()
@@ -82,7 +82,7 @@ extension WarningManager {
] }, ] },
url: "https://github.com/shivammathur/homebrew-php", url: "https://github.com/shivammathur/homebrew-php",
fix: { fix: {
await App.shared.container.shell.quiet("brew tap shivammathur/php") await self.container.shell.quiet("brew tap shivammathur/php")
await BrewDiagnostics.shared.loadInstalledTaps() await BrewDiagnostics.shared.loadInstalledTaps()
await self.checkEnvironment() await self.checkEnvironment()
} }
@@ -98,7 +98,7 @@ extension WarningManager {
] }, ] },
url: "https://github.com/shivammathur/homebrew-extensions", url: "https://github.com/shivammathur/homebrew-extensions",
fix: { fix: {
await App.shared.container.shell.quiet("brew tap shivammathur/extensions") await self.container.shell.quiet("brew tap shivammathur/extensions")
await BrewDiagnostics.shared.loadInstalledTaps() await BrewDiagnostics.shared.loadInstalledTaps()
await self.checkEnvironment() await self.checkEnvironment()
} }

View File

@@ -23,7 +23,10 @@ class BrewExtensionsObservable: ObservableObject {
} }
public func loadExtensionData(for version: String) { public func loadExtensionData(for version: String) {
let tapFormulae = BrewTapFormulae.from(tap: "shivammathur/homebrew-extensions") let tapFormulae = BrewTapFormulae.from(
App.shared.container,
tap: "shivammathur/homebrew-extensions"
)
if let filteredTapFormulae = tapFormulae[version] { if let filteredTapFormulae = tapFormulae[version] {
self.extensions = filteredTapFormulae self.extensions = filteredTapFormulae

View File

@@ -11,8 +11,11 @@ import Foundation
// swiftlint:disable function_body_length // swiftlint:disable function_body_length
class FakeBrewFormulaeHandler: HandlesBrewPhpFormulae { class FakeBrewFormulaeHandler: HandlesBrewPhpFormulae {
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula] { public func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula] {
let container = App.shared.container
return [ return [
BrewPhpFormula( BrewPhpFormula(
container,
name: "php@9.9", name: "php@9.9",
displayName: "PHP 9.9", displayName: "PHP 9.9",
installedVersion: nil, installedVersion: nil,
@@ -20,6 +23,7 @@ class FakeBrewFormulaeHandler: HandlesBrewPhpFormulae {
prerelease: true prerelease: true
), ),
BrewPhpFormula( BrewPhpFormula(
container,
name: "php@8.4", name: "php@8.4",
displayName: "PHP 8.4", displayName: "PHP 8.4",
installedVersion: nil, installedVersion: nil,
@@ -27,6 +31,7 @@ class FakeBrewFormulaeHandler: HandlesBrewPhpFormulae {
prerelease: true prerelease: true
), ),
BrewPhpFormula( BrewPhpFormula(
container,
name: "php", name: "php",
displayName: "PHP 8.3", displayName: "PHP 8.3",
installedVersion: nil, installedVersion: nil,
@@ -34,42 +39,49 @@ class FakeBrewFormulaeHandler: HandlesBrewPhpFormulae {
prerelease: true prerelease: true
), ),
BrewPhpFormula( BrewPhpFormula(
container,
name: "php@8.2", name: "php@8.2",
displayName: "PHP 8.2", displayName: "PHP 8.2",
installedVersion: "8.2.3", installedVersion: "8.2.3",
upgradeVersion: "8.2.4" upgradeVersion: "8.2.4"
), ),
BrewPhpFormula( BrewPhpFormula(
container,
name: "php@8.1", name: "php@8.1",
displayName: "PHP 8.1", displayName: "PHP 8.1",
installedVersion: "8.1.17", installedVersion: "8.1.17",
upgradeVersion: nil upgradeVersion: nil
), ),
BrewPhpFormula( BrewPhpFormula(
container,
name: "php@8.0", name: "php@8.0",
displayName: "PHP 8.0", displayName: "PHP 8.0",
installedVersion: nil, installedVersion: nil,
upgradeVersion: nil upgradeVersion: nil
), ),
BrewPhpFormula( BrewPhpFormula(
container,
name: "php@7.4", name: "php@7.4",
displayName: "PHP 7.4", displayName: "PHP 7.4",
installedVersion: nil, installedVersion: nil,
upgradeVersion: nil upgradeVersion: nil
), ),
BrewPhpFormula( BrewPhpFormula(
container,
name: "php@7.3", name: "php@7.3",
displayName: "PHP 7.3", displayName: "PHP 7.3",
installedVersion: nil, installedVersion: nil,
upgradeVersion: nil upgradeVersion: nil
), ),
BrewPhpFormula( BrewPhpFormula(
container,
name: "php@7.2", name: "php@7.2",
displayName: "PHP 7.2", displayName: "PHP 7.2",
installedVersion: nil, installedVersion: nil,
upgradeVersion: nil upgradeVersion: nil
), ),
BrewPhpFormula( BrewPhpFormula(
container,
name: "php@7.1", name: "php@7.1",
displayName: "PHP 7.1", displayName: "PHP 7.1",
installedVersion: nil, installedVersion: nil,

View File

@@ -0,0 +1,25 @@
{
"configurations" : [
{
"id" : "CDAF5C24-6A14-4AD7-B204-61734226DBF0",
"name" : "Configuration 1",
"options" : {
}
}
],
"defaultOptions" : {
},
"testTargets" : [
{
"parallelizable" : false,
"target" : {
"containerPath" : "container:PHP Monitor.xcodeproj",
"identifier" : "C4F7807825D7F84B000DBC97",
"name" : "Unit Tests"
}
}
],
"version" : 1
}

View File

@@ -9,13 +9,11 @@
import XCTest import XCTest
class FeatureTestCase: XCTestCase { class FeatureTestCase: XCTestCase {
// TODO: make fake filesystem accessible via test case
public func assertFileSystemHas( public func assertFileSystemHas(
_ path: String, _ path: String,
file: StaticString = #filePath, file: StaticString = #filePath,
line: UInt = #line, line: UInt = #line,
fs: TestableFileSystem in fs: TestableFileSystem
) { ) {
XCTAssertTrue(fs.files.keys.contains(path), file: file, line: line) XCTAssertTrue(fs.files.keys.contains(path), file: file, line: line)
} }
@@ -24,7 +22,7 @@ class FeatureTestCase: XCTestCase {
_ path: String, _ path: String,
file: StaticString = #filePath, file: StaticString = #filePath,
line: UInt = #line, line: UInt = #line,
fs: TestableFileSystem in fs: TestableFileSystem
) { ) {
XCTAssertFalse(fs.files.keys.contains(path), file: file, line: line) XCTAssertFalse(fs.files.keys.contains(path), file: file, line: line)
} }
@@ -34,7 +32,7 @@ class FeatureTestCase: XCTestCase {
contents: String, contents: String,
file: StaticString = #filePath, file: StaticString = #filePath,
line: UInt = #line, line: UInt = #line,
fs: TestableFileSystem in fs: TestableFileSystem
) { ) {
XCTAssertEqual(contents, fs.files[path]?.content, file: file, line: line) XCTAssertEqual(contents, fs.files[path]?.content, file: file, line: line)
} }

View File

@@ -9,39 +9,36 @@
import XCTest import XCTest
final class InternalSwitcherTest: FeatureTestCase { final class InternalSwitcherTest: FeatureTestCase {
public func testDefaultPhpFpmPoolIsMoved() async { public func testDefaultPhpFpmPoolIsMoved() async {
ActiveFileSystem.useTestable([ let c = Container.fake(files: [
"/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf": .fake(.text) "/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf": .fake(.text)
]) ]), fs = c.filesystem as! TestableFileSystem
let outcome = await InternalSwitcher().disableDefaultPhpFpmPool("8.1") let outcome = await InternalSwitcher(container: c).disableDefaultPhpFpmPool("8.1")
XCTAssertTrue(outcome) XCTAssertTrue(outcome)
assertFileSystemHas("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon") assertFileSystemHas("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon", in: fs)
assertFileSystemDoesNotHave("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf") assertFileSystemDoesNotHave("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf", in: fs)
} }
public func testExistingDisabledByPhpMonFileIsRemoved() async { public func testExistingDisabledByPhpMonFileIsRemoved() async {
ActiveFileSystem.useTestable([ let c = Container.fake(files: [
"/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf": .fake(.text, "system generated"), "/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf": .fake(.text, "system generated"),
"/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon": .fake(.text, "phpmon generated") "/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon": .fake(.text, "phpmon generated")
]) ]), fs = c.filesystem as! TestableFileSystem
assertFileHasContents( assertFileHasContents("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon",
"/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon", contents: "phpmon generated", in: fs)
contents: "phpmon generated"
)
let outcome = await InternalSwitcher().disableDefaultPhpFpmPool("8.1") let outcome = await InternalSwitcher(container: c).disableDefaultPhpFpmPool("8.1")
XCTAssertTrue(outcome) XCTAssertTrue(outcome)
assertFileSystemHas("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon") assertFileSystemHas("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon", in: fs)
assertFileSystemDoesNotHave("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf") assertFileSystemDoesNotHave("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf", in: fs)
assertFileHasContents( assertFileHasContents(
"/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon", "/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon",
contents: "system generated" contents: "system generated", in: fs
) )
} }

View File

@@ -10,8 +10,10 @@ import Testing
struct CommandTest { struct CommandTest {
@Test func determinePhpVersion() { @Test func determinePhpVersion() {
let version = Command.execute( let container = Container.real()
path: Paths.php,
let version = container.command.execute(
path: container.paths.php,
arguments: ["-v"], arguments: ["-v"],
trimNewlines: false trimNewlines: false
) )

View File

@@ -9,8 +9,14 @@
import Testing import Testing
struct BytePhpPreferenceTest { struct BytePhpPreferenceTest {
var container: Container
init () async throws {
container = Container.real()
}
@Test func can_extract_memory_value() throws { @Test func can_extract_memory_value() throws {
let pref = BytePhpPreference(key: "memory_limit") let pref = BytePhpPreference(container, key: "memory_limit")
#expect(pref.internalValue == "512M") #expect(pref.internalValue == "512M")
#expect(pref.unit == .megabyte) #expect(pref.unit == .megabyte)

View File

@@ -11,9 +11,14 @@ import Foundation
@Suite(.serialized) @Suite(.serialized)
struct CaskFileParserTest { struct CaskFileParserTest {
var container: Container
init() async throws { init() async throws {
ActiveShell.useSystem() container = Container.real()
}
var Shell: ShellProtocol {
return container.shell
} }
// MARK: - Test Files // MARK: - Test Files
@@ -22,7 +27,7 @@ struct CaskFileParserTest {
} }
@Test func can_extract_fields_from_cask_file() async throws { @Test func can_extract_fields_from_cask_file() async throws {
guard let caskFile = await CaskFile.from(url: CaskFileParserTest.exampleFilePath) else { guard let caskFile = await CaskFile.from(container, url: CaskFileParserTest.exampleFilePath) else {
Issue.record("The CaskFile could not be parsed, check the log for more info") Issue.record("The CaskFile could not be parsed, check the log for more info")
return return
} }
@@ -48,7 +53,7 @@ struct CaskFileParserTest {
@Test func can_extract_fields_from_remote_cask_file() async throws { @Test func can_extract_fields_from_remote_cask_file() async throws {
let url = URL(string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon.rb")! let url = URL(string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon.rb")!
guard let caskFile = await CaskFile.from(url: url) else { guard let caskFile = await CaskFile.from(container, url: url) else {
Issue.record("The remote CaskFile could not be parsed, check the log for more info") Issue.record("The remote CaskFile could not be parsed, check the log for more info")
return return
} }

View File

@@ -10,28 +10,32 @@ import Testing
import Foundation import Foundation
struct ExtensionEnumeratorTest { struct ExtensionEnumeratorTest {
var container: Container
init() async throws { init() async throws {
ActiveFileSystem.useTestable([ let paths = Paths(container: Container.fake())
"\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.1.rb": .fake(.text, "<test>"),
"\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.2.rb": .fake(.text, "<test>"), container = Container.fake(files: [
"\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.3.rb": .fake(.text, "<test>"), "\(paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.1.rb": .fake(.text, "<test>"),
"\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.4.rb": .fake(.text, "<test>") "\(paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.2.rb": .fake(.text, "<test>"),
"\(paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.3.rb": .fake(.text, "<test>"),
"\(paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.4.rb": .fake(.text, "<test>")
]) ])
} }
@Test func can_read_formulae() throws { @Test func can_read_formulae() throws {
let directory = "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula" let directory = "\(container.paths.tapPath)/shivammathur/homebrew-extensions/Formula"
let files = try FileSystem.getShallowContentsOfDirectory(directory) let files = try container.filesystem.getShallowContentsOfDirectory(directory)
#expect(Set(files) == Set(["xdebug@8.1.rb", "xdebug@8.2.rb", "xdebug@8.3.rb", "xdebug@8.4.rb"])) #expect(Set(files) == Set(["xdebug@8.1.rb", "xdebug@8.2.rb", "xdebug@8.3.rb", "xdebug@8.4.rb"]))
} }
@Test func can_parse_formulae_based_on_syntax() throws { @Test func can_parse_formulae_based_on_syntax() throws {
let formulae = BrewTapFormulae.from(tap: "shivammathur/homebrew-extensions") let formulae = BrewTapFormulae.from(container, tap: "shivammathur/homebrew-extensions")
#expect(formulae["8.1"] == [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.1")]) #expect(formulae["8.1"] == [BrewPhpExtension(container, path: "/", name: "xdebug", phpVersion: "8.1")])
#expect(formulae["8.2"] == [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.2")]) #expect(formulae["8.2"] == [BrewPhpExtension(container, path: "/", name: "xdebug", phpVersion: "8.2")])
#expect(formulae["8.3"] == [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.3")]) #expect(formulae["8.3"] == [BrewPhpExtension(container, path: "/", name: "xdebug", phpVersion: "8.3")])
#expect(formulae["8.4"] == [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.4")]) #expect(formulae["8.4"] == [BrewPhpExtension(container, path: "/", name: "xdebug", phpVersion: "8.4")])
} }
} }

View File

@@ -53,12 +53,12 @@ struct HomebrewPackageTest {
/// or the JSON API of the Homebrew output may have changed. /// or the JSON API of the Homebrew output may have changed.
@Test(.disabled("Uses system command; enable at your own risk")) @Test(.disabled("Uses system command; enable at your own risk"))
func can_parse_services_json_from_cli_output() async throws { func can_parse_services_json_from_cli_output() async throws {
ActiveShell.useSystem() let container = Container.real()
let services = try! JSONDecoder().decode( let services = try! JSONDecoder().decode(
[HomebrewService].self, [HomebrewService].self,
from: await Shell.pipe( from: await container.shell.pipe(
"sudo \(Paths.brew) services info --all --json" "sudo \(container.paths.brew) services info --all --json"
).out.data(using: .utf8)! ).out.data(using: .utf8)!
).filter({ service in ).filter({ service in
return ["php", "nginx", "dnsmasq"].contains(service.name) return ["php", "nginx", "dnsmasq"].contains(service.name)
@@ -76,12 +76,11 @@ struct HomebrewPackageTest {
/// or the JSON API of the Homebrew output may have changed. /// or the JSON API of the Homebrew output may have changed.
@Test(.disabled("Uses system command; enable at your own risk")) @Test(.disabled("Uses system command; enable at your own risk"))
func can_load_extension_json_from_cli_output() async throws { func can_load_extension_json_from_cli_output() async throws {
let container = Container.real()
ActiveShell.useSystem()
let package = try! JSONDecoder().decode( let package = try! JSONDecoder().decode(
[HomebrewPackage].self, [HomebrewPackage].self,
from: await Shell.pipe("\(Paths.brew) info php --json").out.data(using: .utf8)! from: await container.shell.pipe("\(container.paths.brew) info php --json").out.data(using: .utf8)!
).first! ).first!
#expect(package.full_name == "php") #expect(package.full_name == "php")

View File

@@ -10,42 +10,45 @@ import Testing
import Foundation import Foundation
struct HomebrewUpgradableTest { struct HomebrewUpgradableTest {
var container: Container
init() throws {
container = Container.fake(
shell: [
"/opt/homebrew/bin/brew update >/dev/null && /opt/homebrew/bin/brew outdated --json --formulae"
: .instant(try! String(contentsOf: Self.outdatedFileUrl)),
"/opt/homebrew/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"
: .instant("/opt/homebrew/etc/php/8.2/conf.d/php-memory-limits.ini"),
"/opt/homebrew/opt/php@8.1.16/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"
: .instant("/opt/homebrew/etc/php/8.1/conf.d/php-memory-limits.ini"),
"/opt/homebrew/opt/php@8.2.3/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"
: .instant("/opt/homebrew/etc/php/8.2/conf.d/php-memory-limits.ini"),
"/opt/homebrew/opt/php@7.4.11/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"
: .instant("/opt/homebrew/etc/php/7.4/conf.d/php-memory-limits.ini")
],
files: [
"/opt/homebrew/etc/php/8.2/conf.d/php-memory-limits.ini": .fake(.text),
"/opt/homebrew/etc/php/8.1/conf.d/php-memory-limits.ini": .fake(.text),
"/opt/homebrew/etc/php/7.4/conf.d/php-memory-limits.ini": .fake(.text)
]
)
}
static var outdatedFileUrl: URL { static var outdatedFileUrl: URL {
return TestBundle.url(forResource: "brew-outdated", withExtension: "json")! return TestBundle.url(forResource: "brew-outdated", withExtension: "json")!
} }
@Test func upgradable_php_versions_can_be_determined() async throws { @Test func upgradable_php_versions_can_be_determined() async throws {
// Do not execute production cli commands
ActiveShell.useTestable([
"/opt/homebrew/bin/brew update >/dev/null && /opt/homebrew/bin/brew outdated --json --formulae"
: .instant(try! String(contentsOf: Self.outdatedFileUrl)),
"/opt/homebrew/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"
: .instant("/opt/homebrew/etc/php/8.2/conf.d/php-memory-limits.ini"),
"/opt/homebrew/opt/php@8.1.16/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"
: .instant("/opt/homebrew/etc/php/8.1/conf.d/php-memory-limits.ini"),
"/opt/homebrew/opt/php@8.2.3/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"
: .instant("/opt/homebrew/etc/php/8.2/conf.d/php-memory-limits.ini"),
"/opt/homebrew/opt/php@7.4.11/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"
: .instant("/opt/homebrew/etc/php/7.4/conf.d/php-memory-limits.ini")
])
// Do not read our production files
ActiveFileSystem.useTestable([
"/opt/homebrew/etc/php/8.2/conf.d/php-memory-limits.ini": .fake(.text),
"/opt/homebrew/etc/php/8.1/conf.d/php-memory-limits.ini": .fake(.text),
"/opt/homebrew/etc/php/7.4/conf.d/php-memory-limits.ini": .fake(.text)
])
// This config file assumes our PHP alias (`php`) is v8.2 // This config file assumes our PHP alias (`php`) is v8.2
PhpEnvironments.brewPhpAlias = "8.2" PhpEnvironments.brewPhpAlias = "8.2"
let env = App.shared.container.phpEnvs let env = container.phpEnvs!
env.cachedPhpInstallations = [ env.cachedPhpInstallations = [
"8.1": PhpInstallation("8.1.16"), "8.1": PhpInstallation(container, "8.1.16"),
"8.2": PhpInstallation("8.2.3"), "8.2": PhpInstallation(container, "8.2.3"),
"7.4": PhpInstallation("7.4.11") "7.4": PhpInstallation(container, "7.4.11")
] ]
let data = await BrewPhpFormulaeHandler().loadPhpVersions(loadOutdated: true) let data = await BrewPhpFormulaeHandler(container: container)
.loadPhpVersions(loadOutdated: true)
#expect(true == data.contains(where: { formula in #expect(true == data.contains(where: { formula in
formula.installedVersion == "8.1.16" && formula.upgradeVersion == "8.1.17" formula.installedVersion == "8.1.16" && formula.upgradeVersion == "8.1.17"

View File

@@ -10,6 +10,11 @@ import Testing
import Foundation import Foundation
struct NginxConfigurationTest { struct NginxConfigurationTest {
var container: Container
init () async throws {
container = Container.real()
}
// MARK: - Test Files // MARK: - Test Files
@@ -36,33 +41,33 @@ struct NginxConfigurationTest {
// MARK: - Tests // MARK: - Tests
@Test func can_determine_site_name_and_tld() throws { @Test func can_determine_site_name_and_tld() throws {
#expect("nginx-site" == NginxConfigurationFile.from(filePath: Self.regularUrl.path)?.domain) #expect("nginx-site" == NginxConfigurationFile.from(container, filePath: Self.regularUrl.path)?.domain)
#expect("test" == NginxConfigurationFile.from(filePath: Self.regularUrl.path)?.tld) #expect("test" == NginxConfigurationFile.from(container, filePath: Self.regularUrl.path)?.tld)
} }
@Test func can_determine_isolation() throws { @Test func can_determine_isolation() throws {
#expect(nil == NginxConfigurationFile.from(filePath: Self.regularUrl.path)?.isolatedVersion) #expect(nil == NginxConfigurationFile.from(container, filePath: Self.regularUrl.path)?.isolatedVersion)
#expect("8.1" == NginxConfigurationFile.from(filePath: Self.isolatedUrl.path)?.isolatedVersion) #expect("8.1" == NginxConfigurationFile.from(container, filePath: Self.isolatedUrl.path)?.isolatedVersion)
} }
@Test func can_determine_proxy() throws { @Test func can_determine_proxy() throws {
let proxied = NginxConfigurationFile.from(filePath: Self.proxyUrl.path)! let proxied = NginxConfigurationFile.from(container, filePath: Self.proxyUrl.path)!
#expect(proxied.contents.contains("# valet stub: proxy.valet.conf")) #expect(proxied.contents.contains("# valet stub: proxy.valet.conf"))
#expect("http://127.0.0.1:90" == proxied.proxy) #expect("http://127.0.0.1:90" == proxied.proxy)
let normal = NginxConfigurationFile.from(filePath: Self.regularUrl.path)! let normal = NginxConfigurationFile.from(container, filePath: Self.regularUrl.path)!
#expect(false == normal.contents.contains("# valet stub: proxy.valet.conf")) #expect(false == normal.contents.contains("# valet stub: proxy.valet.conf"))
#expect(nil == normal.proxy) #expect(nil == normal.proxy)
} }
@Test func can_determine_secured_proxy() throws { @Test func can_determine_secured_proxy() throws {
let proxied = NginxConfigurationFile.from(filePath: Self.secureProxyUrl.path)! let proxied = NginxConfigurationFile.from(container, filePath: Self.secureProxyUrl.path)!
#expect(proxied.contents.contains("# valet stub: secure.proxy.valet.conf")) #expect(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
#expect("http://127.0.0.1:90" == proxied.proxy) #expect("http://127.0.0.1:90" == proxied.proxy)
} }
@Test func can_determine_proxy_with_custom_tld() throws { @Test func can_determine_proxy_with_custom_tld() throws {
let proxied = NginxConfigurationFile.from(filePath: Self.customTldProxyUrl.path)! let proxied = NginxConfigurationFile.from(container, filePath: Self.customTldProxyUrl.path)!
#expect(proxied.contents.contains("# valet stub: secure.proxy.valet.conf")) #expect(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
#expect("http://localhost:8080" == proxied.proxy) #expect("http://localhost:8080" == proxied.proxy)
} }

View File

@@ -11,21 +11,25 @@ import Foundation
@Suite(.serialized) @Suite(.serialized)
class PhpConfigurationFileTest { class PhpConfigurationFileTest {
var container: Container
init() {
self.container = Container.real()
}
static var phpIniFileUrl: URL { static var phpIniFileUrl: URL {
return TestBundle.url(forResource: "php", withExtension: "ini")! return TestBundle.url(forResource: "php", withExtension: "ini")!
} }
@Test func can_load_extension() throws { @Test func can_load_extension() throws {
ActiveFileSystem.useSystem() let iniFile = PhpConfigurationFile.from(container, filePath: Self.phpIniFileUrl.path)
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)
#expect(iniFile != nil) #expect(iniFile != nil)
#expect(!iniFile!.extensions.isEmpty) #expect(!iniFile!.extensions.isEmpty)
} }
@Test func can_check_key_existence() throws { @Test func can_check_key_existence() throws {
print(Self.phpIniFileUrl.path) let iniFile = PhpConfigurationFile.from(container, filePath: Self.phpIniFileUrl.path)!
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)!
#expect(iniFile.has(key: "error_reporting")) #expect(iniFile.has(key: "error_reporting"))
#expect(iniFile.has(key: "display_errors")) #expect(iniFile.has(key: "display_errors"))
@@ -33,7 +37,7 @@ class PhpConfigurationFileTest {
} }
@Test func can_check_key_value() throws { @Test func can_check_key_value() throws {
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)! let iniFile = PhpConfigurationFile.from(container, filePath: Self.phpIniFileUrl.path)!
#expect(iniFile.get(for: "error_reporting") != nil) #expect(iniFile.get(for: "error_reporting") != nil)
#expect(iniFile.get(for: "error_reporting") == "E_ALL") #expect(iniFile.get(for: "error_reporting") == "E_ALL")
@@ -46,8 +50,7 @@ class PhpConfigurationFileTest {
let destination = Utility let destination = Utility
.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")! .copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
let configurationFile = PhpConfigurationFile let configurationFile = PhpConfigurationFile.from(container, filePath: destination.path)!
.from(filePath: destination.path)!
// 0. Verify the original value // 0. Verify the original value
#expect(configurationFile.get(for: "error_reporting") == "E_ALL") #expect(configurationFile.get(for: "error_reporting") == "E_ALL")

View File

@@ -11,8 +11,10 @@ import Foundation
@Suite(.serialized) @Suite(.serialized)
struct PhpExtensionTest { struct PhpExtensionTest {
var container: Container
init () async throws { init () async throws {
ActiveShell.useSystem() container = Container.real()
} }
static var phpIniFileUrl: URL { static var phpIniFileUrl: URL {
@@ -20,13 +22,13 @@ struct PhpExtensionTest {
} }
@Test func can_load_extension() throws { @Test func can_load_extension() throws {
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path) let extensions = PhpExtension.from(container, filePath: Self.phpIniFileUrl.path)
#expect(!extensions.isEmpty) #expect(!extensions.isEmpty)
} }
@Test func extension_name_is_correct() throws { @Test func extension_name_is_correct() throws {
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path) let extensions = PhpExtension.from(container, filePath: Self.phpIniFileUrl.path)
let extensionNames = extensions.map { (ext) -> String in let extensionNames = extensions.map { (ext) -> String in
return ext.name return ext.name
@@ -45,7 +47,7 @@ struct PhpExtensionTest {
} }
@Test func extension_status_is_correct() throws { @Test func extension_status_is_correct() throws {
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path) let extensions = PhpExtension.from(container, filePath: Self.phpIniFileUrl.path)
// xdebug should be enabled // xdebug should be enabled
#expect(extensions[0].enabled == true) #expect(extensions[0].enabled == true)
@@ -56,7 +58,7 @@ struct PhpExtensionTest {
@Test func toggle_works_as_expected() async throws { @Test func toggle_works_as_expected() async throws {
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")! let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
let extensions = PhpExtension.from(filePath: destination.path) let extensions = PhpExtension.from(container, filePath: destination.path)
#expect(extensions.count == 6) #expect(extensions.count == 6)
// Try to disable xdebug (should be detected first)! // Try to disable xdebug (should be detected first)!
@@ -71,6 +73,6 @@ struct PhpExtensionTest {
#expect(file.contains("; zend_extension=\"xdebug.so\"")) #expect(file.contains("; zend_extension=\"xdebug.so\""))
// Make sure if we load the data again, it's disabled // Make sure if we load the data again, it's disabled
#expect(PhpExtension.from(filePath: destination.path).first!.enabled == false) #expect(PhpExtension.from(container, filePath: destination.path).first!.enabled == false)
} }
} }

View File

@@ -74,7 +74,7 @@ struct RealFileSystemTest {
"\(temporaryDirectory)/brew/etc/lib/c" "\(temporaryDirectory)/brew/etc/lib/c"
) )
let contents = try! FileSystem.getShallowContentsOfDirectory("\(temporaryDirectory)/brew/etc/lib/c") let contents = try! filesystem.getShallowContentsOfDirectory("\(temporaryDirectory)/brew/etc/lib/c")
#expect([] == contents) #expect([] == contents)
} }

View File

@@ -11,8 +11,9 @@ import Foundation
@Suite(.serialized) @Suite(.serialized)
struct TestableFileSystemTest { struct TestableFileSystemTest {
var container: Container
init() throws { init() throws {
ActiveFileSystem.useTestable([ container = Container.fake(files: [
"/home/user/bin/foo": .fake(.binary), "/home/user/bin/foo": .fake(.binary),
"/home/user/docs": .fake(.symlink, "/home/user/documents"), "/home/user/docs": .fake(.symlink, "/home/user/documents"),
"/home/user/documents/script.sh": .fake(.text, "echo 'cool';"), "/home/user/documents/script.sh": .fake(.text, "echo 'cool';"),
@@ -22,6 +23,10 @@ struct TestableFileSystemTest {
]) ])
} }
var FileSystem: FileSystemProtocol {
return container.filesystem
}
@Test func testable_fs_is_in_use() { @Test func testable_fs_is_in_use() {
#expect(FileSystem is TestableFileSystem) #expect(FileSystem is TestableFileSystem)
} }

View File

@@ -11,10 +11,15 @@ import Foundation
@Suite(.serialized) @Suite(.serialized)
struct RealShellTest { struct RealShellTest {
var container: Container
init() async throws { init() async throws {
// Reset to the default shell // Reset to the default shell
ActiveShell.useSystem() container = Container.real()
}
var Shell: ShellProtocol {
return container.shell
} }
@Test func system_shell_is_default() async { @Test func system_shell_is_default() async {

View File

@@ -63,8 +63,8 @@ struct TestableShellTest {
} }
@Test func fake_shell_has_path() { @Test func fake_shell_has_path() {
ActiveShell.useTestable([:]) let container = Container.fake()
#expect(Shell.PATH == "/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin") #expect(container.shell.PATH == "/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin")
} }
} }

View File

@@ -11,6 +11,8 @@ import Foundation
struct TestableConfigurationTest { struct TestableConfigurationTest {
@Test func configuration_can_be_saved_as_json() async { @Test func configuration_can_be_saved_as_json() async {
let container = Container.real()
// WORKING // WORKING
var configuration = TestableConfigurations.working var configuration = TestableConfigurations.working
@@ -39,8 +41,8 @@ struct TestableConfigurationTest {
) )
// Verify that the files were written to disk // Verify that the files were written to disk
#expect(FileSystem.fileExists(NSHomeDirectory() + "/.phpmon_fconf_working.json")) #expect(container.filesystem.fileExists(NSHomeDirectory() + "/.phpmon_fconf_working.json"))
#expect(FileSystem.fileExists(NSHomeDirectory() + "/.phpmon_fconf_working_no_valet.json")) #expect(container.filesystem.fileExists(NSHomeDirectory() + "/.phpmon_fconf_working_no_valet.json"))
#expect(FileSystem.fileExists(NSHomeDirectory() + "/.phpmon_fconf_broken.json")) #expect(container.filesystem.fileExists(NSHomeDirectory() + "/.phpmon_fconf_broken.json"))
} }
} }

View File

@@ -9,9 +9,10 @@
import XCTest import XCTest
class PhpVersionDetectionTest: XCTestCase { class PhpVersionDetectionTest: XCTestCase {
func test_can_detect_valid_php_versions() async throws { func test_can_detect_valid_php_versions() async throws {
let outcome = await App.shared.container.phpEnvs.extractPhpVersions( let container = Container.real()
let outcome = await container.phpEnvs.extractPhpVersions(
from: [ from: [
"", // empty lines should be omitted "", // empty lines should be omitted
"php@8.0", "php@8.0",