1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2026-04-02 17:40: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 */; };
031E2B6B2B1525A7007C29E1 /* 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 */; };
03263A392E86D5EC00BD0415 /* 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 */
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>"; };
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>"; };
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>"; };
@@ -1609,6 +1615,7 @@
C41C1B3522B0097F00E7CF16 /* phpmon */ = {
isa = PBXGroup;
children = (
031F247F2EA1071700CFB8D9 /* Container+Fake.swift */,
0329A9A02E92A2A800A62A12 /* Container.swift */,
C4B5853A2770FE2500DA4FBE /* Common */,
C41E181722CB61EB0072CF09 /* Domain */,
@@ -1967,6 +1974,7 @@
C471E79628F9B4260021E251 /* tests */ = {
isa = PBXGroup;
children = (
031F24842EA1132300CFB8D9 /* PHP Monitor.xctestplan */,
C4E2E86828FC2FF2003B070C /* Shared */,
C4F7807A25D7F84B000DBC97 /* unit */,
C471E7AE28F9B4940021E251 /* feature */,
@@ -2848,6 +2856,7 @@
C485707028BF452300539B36 /* PhpDoctorWindowController.swift in Sources */,
C4CE3BBA27B31F670086CA49 /* ComposerWindow.swift in Sources */,
C40D725A2A018ACC0054A067 /* BusyStatus.swift in Sources */,
031F24802EA1071A00CFB8D9 /* Container+Fake.swift in Sources */,
C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */,
C4FACE83288F1F9700FC478F /* OnboardingWindowController.swift in Sources */,
C4415E8D2B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */,
@@ -2956,6 +2965,7 @@
C471E85A28F9BB650021E251 /* DomainListTypeCell.swift in Sources */,
C471E85B28F9BB650021E251 /* DomainListKindCell.swift in Sources */,
C4611E5E2AEAD2FB0010BE24 /* ConfigManagerView.swift in Sources */,
031F24822EA1071A00CFB8D9 /* Container+Fake.swift in Sources */,
C4BF56AD2949381100379603 /* FakeValetInteractor.swift in Sources */,
C471E85C28F9BB650021E251 /* DomainListWindowController.swift in Sources */,
C471E85D28F9BB650021E251 /* DomainListVC.swift in Sources */,
@@ -3255,6 +3265,7 @@
C471E7FC28F9BACE0021E251 /* HomebrewDecodable.swift in Sources */,
C4BB393C2981AFC700F8E797 /* PhpVersionSource.swift in Sources */,
C471E7F628F9BAC80021E251 /* PhpHelper.swift in Sources */,
031F24812EA1071A00CFB8D9 /* Container+Fake.swift in Sources */,
039E1D7B2E5F0F300072D13D /* ValetUpgrader.swift in Sources */,
C471E7EE28F9BAC30021E251 /* Constants.swift in Sources */,
C40934A0298EE8E900D25014 /* AppUpdater.swift in Sources */,
@@ -3419,6 +3430,7 @@
C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */,
C4159AF728E4D40400545349 /* RealShellTest.swift in Sources */,
C450C8C728C919EC002A2B4B /* PreferenceName.swift in Sources */,
031F24832EA1071A00CFB8D9 /* Container+Fake.swift in Sources */,
C40D725B2A018ACC0054A067 /* BusyStatus.swift in Sources */,
032DAC292E8BEB5B0018E01C /* RealApi.swift in Sources */,
C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */,

View File

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

View File

@@ -18,30 +18,30 @@ class Actions {
// MARK: - Services
public func linkPhp() async {
await brew("link php --overwrite --force")
await brew(container, "link php --overwrite --force")
}
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 {
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 {
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 {
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 {
await brew("services stop \(formulae.php)", sudo: formulae.php.elevated)
await brew("services stop \(formulae.nginx)", sudo: formulae.nginx.elevated)
await brew("services stop \(formulae.dnsmasq)", sudo: formulae.dnsmasq.elevated)
await brew(container, "services stop \(formulae.php)", sudo: formulae.php.elevated)
await brew(container, "services stop \(formulae.nginx)", sudo: formulae.nginx.elevated)
await brew(container, "services stop \(formulae.dnsmasq)", sudo: formulae.dnsmasq.elevated)
}
public func fixHomebrewPermissions() throws {
@@ -134,8 +134,8 @@ class Actions {
*/
public func fixMyValet() async {
await InternalSwitcher().performSwitch(to: PhpEnvironments.brewPhpAlias)
await brew("services restart \(formulae.dnsmasq)", sudo: formulae.dnsmasq.elevated)
await brew("services restart \(formulae.php)", sudo: formulae.php.elevated)
await brew("services restart \(formulae.nginx)", sudo: formulae.nginx.elevated)
await brew(container, "services restart \(formulae.dnsmasq)", sudo: formulae.dnsmasq.elevated)
await brew(container, "services restart \(formulae.php)", sudo: formulae.php.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.
*/
func brew(
_ container: Container = App.shared.container,
_ command: String,
sudo: Bool = false,
container: Container = App.shared.container,
) async {
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.
*/
func sed(
_ container: Container = App.shared.container,
file: String,
original: String,
replacement: String,
filesystem: FileSystemProtocol = App.shared.container.filesystem,
shell: ShellProtocol = App.shared.container.shell,
paths: Paths = App.shared.container.paths,
replacement: String
) async {
// Escape slashes (or `sed` won't work)
let e_original = original.replacingOccurrences(of: "/", with: "\\/")
@@ -38,17 +36,21 @@ func sed(
// Check if gsed exists; it is able to follow symlinks,
// which we want to do to toggle the extension
if filesystem.fileExists("\(paths.binPath)/gsed") {
await shell.quiet("\(paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
if container.filesystem.fileExists("\(container.paths.binPath)/gsed") {
await container.shell.quiet("\(container.paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
} 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.
*/
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("""
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
""").out

View File

@@ -10,7 +10,12 @@ import Foundation
extension 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
public static func load() -> ActivePhpInstallation? {
let container = App.shared.container
public static func load(
container: Container = App.shared.container
) -> ActivePhpInstallation? {
if !container.filesystem.fileExists(container.paths.phpConfig) {
return nil
}
return ActivePhpInstallation()
return ActivePhpInstallation(container: container)
}
init(container: Container = App.shared.container) {
@@ -83,7 +83,7 @@ class ActivePhpInstallation {
// See if any extensions are present in said .ini files
paths.forEach { (iniFilePath) in
if let file = PhpConfigurationFile.from(filePath: iniFilePath) {
if let file = PhpConfigurationFile.from(container, filePath: iniFilePath) {
iniFiles.append(file)
}
}

View File

@@ -18,7 +18,7 @@ class PhpEnvironments {
*/
init(container: Container = App.shared.container) {
self.container = container
self.currentInstall = ActivePhpInstallation.load()
self.currentInstall = ActivePhpInstallation.load(container: container)
}
/**
@@ -170,7 +170,7 @@ class PhpEnvironments {
// Avoid inserting a duplicate
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
// if that isn't the case, our formula remains out-of-date
if !phpAliasInstall.isMissingBinary {
@@ -190,7 +190,7 @@ class PhpEnvironments {
var mappedVersions: [String: PhpInstallation] = [:]
availablePhpVersions.forEach { version in
mappedVersions[version] = PhpInstallation(version)
mappedVersions[version] = PhpInstallation(container, version)
}
cachedPhpInstallations = mappedVersions

View File

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

View File

@@ -7,6 +7,7 @@
//
import Foundation
import ContainerMacro
/**
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
instances. You can find more information here: https://nshipster.com/swift-regular-expressions/
*/
@ContainerAccess
class PhpExtension {
/// The file where this extension was located.
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.
*/
init(_ line: String, file: String) {
init(_ container: Container, _ line: String, file: String) {
self.container = container
let regex = try! NSRegularExpression(pattern: Self.extensionRegex, options: [])
let match = regex.matches(in: line, options: [], range: NSRange(location: 0, length: line.count)).first
let range = Range(match!.range(withName: "name"), in: line)!
@@ -82,7 +85,7 @@ class PhpExtension {
// ENABLED: Line where the comment delimiter (;) is removed
: 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.line = newLine
@@ -96,15 +99,15 @@ class PhpExtension {
// 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 $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
}.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)
if file == nil {
@@ -113,6 +116,7 @@ class PhpExtension {
}
return Self.from(
container,
file!.components(separatedBy: "\n"),
filePath: filePath
)

View File

@@ -11,7 +11,6 @@ import ContainerMacro
@ContainerAccess
class PhpInstallation {
var versionNumber: VersionNumber
var iniFiles: [PhpConfigurationFile] = []
@@ -40,7 +39,7 @@ class PhpInstallation {
In order to determine details about a PHP installation,
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
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
paths.forEach { (iniFilePath) in
if let file = PhpConfigurationFile.from(filePath: iniFilePath) {
if let file = PhpConfigurationFile.from(container, filePath: iniFilePath) {
iniFiles.append(file)
}
}

View File

@@ -53,7 +53,7 @@ class InternalSwitcher: PhpSwitcher {
if Valet.installed {
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!")
@@ -78,10 +78,10 @@ class InternalSwitcher: PhpSwitcher {
func unlinkAndStopPhpVersion(_ version: String) async {
let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)"
await brew("unlink \(formula)")
await brew(container, "unlink \(formula)")
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)")
} else {
Log.info("Unlinked \(formula)")
@@ -93,13 +93,13 @@ class InternalSwitcher: PhpSwitcher {
if primary {
Log.info("\(formula) is the primary formula, linking...")
await brew("link \(formula) --overwrite --force")
await brew(container, "link \(formula) --overwrite --force")
} else {
Log.info("\(formula) is an isolated PHP version, not linking!")
}
if Valet.installed {
await brew("services start \(formula)", sudo: true)
await brew(container, "services start \(formula)", sudo: true)
if Valet.enabled(feature: .isolatedSites) && primary {
let socketVersion = version.replacingOccurrences(of: ".", with: "")

View File

@@ -9,7 +9,5 @@
import Foundation
protocol CreatedFromFile {
static func from(filePath: String, container: Container) -> Self?
static func from(_ container: Container, filePath: String) -> 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)
}
public func overrideFake() {
self.shell = TestableShell(expectations: [:])
self.filesystem = TestableFileSystem(files: [:])
self.command = TestableCommand(commands: [:])
public static func real() -> Container {
let container = Container()
container.prepare()
return container
}
}

View File

@@ -74,7 +74,7 @@ class App {
The dependency container.
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. */
var preferences: [PreferenceName: Bool]!

View File

@@ -28,7 +28,7 @@ class AppUpdater {
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.")
presentCouldNotRetrieveUpdateIfInteractive()
return .networkError

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,6 @@
import Foundation
class NginxConfigurationFile: CreatedFromFile {
/// Contents of the Nginx file in question, as a string.
var contents: String!
@@ -21,8 +20,8 @@ class NginxConfigurationFile: CreatedFromFile {
/** Resolves an nginx configuration file (.conf) */
static func from(
_ container: Container,
filePath: String,
container: Container = App.shared.container
) -> Self? {
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 {
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
@@ -100,7 +100,7 @@ class ValetDomainScanner: DomainScanner {
return !$0.starts(with: ".")
}
.compactMap {
return NginxConfigurationFile.from(filePath: "\(directoryPath)/\($0)")
return NginxConfigurationFile.from(container, filePath: "\(directoryPath)/\($0)")
}
.filter {
return $0.proxy != nil

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ class BytePhpPreference: PhpPreference {
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(
path: container.paths.php, arguments: ["-r", "echo ini_get('\(key)');"],
trimNewlines: false
@@ -48,7 +48,7 @@ class BytePhpPreference: PhpPreference {
self.value = value
}
super.init(container: container, key: key)
super.init(container, key: key)
}
// MARK: Save Value

View File

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

View File

@@ -12,7 +12,7 @@ extension WarningManager {
return [
Warning(
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"
},
name: "Running PHP Monitor with Rosetta on Apple Silicon",
@@ -23,8 +23,8 @@ extension WarningManager {
),
Warning(
command: {
return !App.shared.container.shell.PATH.contains("\(App.shared.container.paths.homePath)/.config/phpmon/bin") &&
!App.shared.container.filesystem.isWriteableFile("/usr/local/bin/")
return !self.container.shell.PATH.contains("\(self.container.paths.homePath)/.config/phpmon/bin") &&
!self.container.filesystem.isWriteableFile("/usr/local/bin/")
},
name: "Helpers cannot be symlinked and not in PATH",
title: "warnings.helper_permissions.title",
@@ -34,7 +34,7 @@ extension WarningManager {
"warnings.helper_permissions.symlink"
] },
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
await ZshRunCommand().addPhpMonitorPath()
// Finally, perform environment checks again
@@ -43,7 +43,7 @@ extension WarningManager {
),
Warning(
command: {
App.shared.container.phpEnvs.currentInstall?.extensions.contains { $0.name == "xdebug" } ?? false
self.container.phpEnvs.currentInstall?.extensions.contains { $0.name == "xdebug" } ?? false
&& !Xdebug().enabled
},
name: "Missing configuration file for `xdebug.mode`",
@@ -53,17 +53,17 @@ extension WarningManager {
] },
url: "https://xdebug.org/docs/install#mode",
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" }),
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
try? App.shared.container.filesystem.writeAtomicallyToFile(
try? self.container.filesystem.writeAtomicallyToFile(
xdebug.file,
content: original + "\nxdebug.mode = off"
)
// Reload extension configuration by updating PHP installation info (reload)
App.shared.container.phpEnvs.currentInstall = ActivePhpInstallation()
self.container.phpEnvs.currentInstall = ActivePhpInstallation()
// Finally, reload warnings
await self.checkEnvironment()
@@ -82,7 +82,7 @@ extension WarningManager {
] },
url: "https://github.com/shivammathur/homebrew-php",
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 self.checkEnvironment()
}
@@ -98,7 +98,7 @@ extension WarningManager {
] },
url: "https://github.com/shivammathur/homebrew-extensions",
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 self.checkEnvironment()
}

View File

@@ -23,7 +23,10 @@ class BrewExtensionsObservable: ObservableObject {
}
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] {
self.extensions = filteredTapFormulae

View File

@@ -11,8 +11,11 @@ import Foundation
// swiftlint:disable function_body_length
class FakeBrewFormulaeHandler: HandlesBrewPhpFormulae {
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula] {
let container = App.shared.container
return [
BrewPhpFormula(
container,
name: "php@9.9",
displayName: "PHP 9.9",
installedVersion: nil,
@@ -20,6 +23,7 @@ class FakeBrewFormulaeHandler: HandlesBrewPhpFormulae {
prerelease: true
),
BrewPhpFormula(
container,
name: "php@8.4",
displayName: "PHP 8.4",
installedVersion: nil,
@@ -27,6 +31,7 @@ class FakeBrewFormulaeHandler: HandlesBrewPhpFormulae {
prerelease: true
),
BrewPhpFormula(
container,
name: "php",
displayName: "PHP 8.3",
installedVersion: nil,
@@ -34,42 +39,49 @@ class FakeBrewFormulaeHandler: HandlesBrewPhpFormulae {
prerelease: true
),
BrewPhpFormula(
container,
name: "php@8.2",
displayName: "PHP 8.2",
installedVersion: "8.2.3",
upgradeVersion: "8.2.4"
),
BrewPhpFormula(
container,
name: "php@8.1",
displayName: "PHP 8.1",
installedVersion: "8.1.17",
upgradeVersion: nil
),
BrewPhpFormula(
container,
name: "php@8.0",
displayName: "PHP 8.0",
installedVersion: nil,
upgradeVersion: nil
),
BrewPhpFormula(
container,
name: "php@7.4",
displayName: "PHP 7.4",
installedVersion: nil,
upgradeVersion: nil
),
BrewPhpFormula(
container,
name: "php@7.3",
displayName: "PHP 7.3",
installedVersion: nil,
upgradeVersion: nil
),
BrewPhpFormula(
container,
name: "php@7.2",
displayName: "PHP 7.2",
installedVersion: nil,
upgradeVersion: nil
),
BrewPhpFormula(
container,
name: "php@7.1",
displayName: "PHP 7.1",
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
class FeatureTestCase: XCTestCase {
// TODO: make fake filesystem accessible via test case
public func assertFileSystemHas(
_ path: String,
file: StaticString = #filePath,
line: UInt = #line,
fs: TestableFileSystem
in fs: TestableFileSystem
) {
XCTAssertTrue(fs.files.keys.contains(path), file: file, line: line)
}
@@ -24,7 +22,7 @@ class FeatureTestCase: XCTestCase {
_ path: String,
file: StaticString = #filePath,
line: UInt = #line,
fs: TestableFileSystem
in fs: TestableFileSystem
) {
XCTAssertFalse(fs.files.keys.contains(path), file: file, line: line)
}
@@ -34,7 +32,7 @@ class FeatureTestCase: XCTestCase {
contents: String,
file: StaticString = #filePath,
line: UInt = #line,
fs: TestableFileSystem
in fs: TestableFileSystem
) {
XCTAssertEqual(contents, fs.files[path]?.content, file: file, line: line)
}

View File

@@ -9,39 +9,36 @@
import XCTest
final class InternalSwitcherTest: FeatureTestCase {
public func testDefaultPhpFpmPoolIsMoved() async {
ActiveFileSystem.useTestable([
let c = Container.fake(files: [
"/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)
assertFileSystemHas("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon")
assertFileSystemDoesNotHave("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf")
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", in: fs)
}
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.disabled-by-phpmon": .fake(.text, "phpmon generated")
])
]), fs = c.filesystem as! TestableFileSystem
assertFileHasContents(
"/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon",
contents: "phpmon generated"
)
assertFileHasContents("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon",
contents: "phpmon generated", in: fs)
let outcome = await InternalSwitcher().disableDefaultPhpFpmPool("8.1")
let outcome = await InternalSwitcher(container: c).disableDefaultPhpFpmPool("8.1")
XCTAssertTrue(outcome)
assertFileSystemHas("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon")
assertFileSystemDoesNotHave("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf")
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", in: fs)
assertFileHasContents(
"/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 {
@Test func determinePhpVersion() {
let version = Command.execute(
path: Paths.php,
let container = Container.real()
let version = container.command.execute(
path: container.paths.php,
arguments: ["-v"],
trimNewlines: false
)

View File

@@ -9,8 +9,14 @@
import Testing
struct BytePhpPreferenceTest {
var container: Container
init () async throws {
container = Container.real()
}
@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.unit == .megabyte)

View File

@@ -11,9 +11,14 @@ import Foundation
@Suite(.serialized)
struct CaskFileParserTest {
var container: Container
init() async throws {
ActiveShell.useSystem()
container = Container.real()
}
var Shell: ShellProtocol {
return container.shell
}
// MARK: - Test Files
@@ -22,7 +27,7 @@ struct CaskFileParserTest {
}
@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")
return
}
@@ -48,7 +53,7 @@ struct CaskFileParserTest {
@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")!
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")
return
}

View File

@@ -10,28 +10,32 @@ import Testing
import Foundation
struct ExtensionEnumeratorTest {
var container: Container
init() async throws {
ActiveFileSystem.useTestable([
"\(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>"),
"\(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>")
let paths = Paths(container: Container.fake())
container = Container.fake(files: [
"\(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>"),
"\(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 {
let directory = "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula"
let files = try FileSystem.getShallowContentsOfDirectory(directory)
let directory = "\(container.paths.tapPath)/shivammathur/homebrew-extensions/Formula"
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"]))
}
@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.2"] == [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.2")])
#expect(formulae["8.3"] == [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.3")])
#expect(formulae["8.4"] == [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.4")])
#expect(formulae["8.1"] == [BrewPhpExtension(container, path: "/", name: "xdebug", phpVersion: "8.1")])
#expect(formulae["8.2"] == [BrewPhpExtension(container, path: "/", name: "xdebug", phpVersion: "8.2")])
#expect(formulae["8.3"] == [BrewPhpExtension(container, path: "/", name: "xdebug", phpVersion: "8.3")])
#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.
@Test(.disabled("Uses system command; enable at your own risk"))
func can_parse_services_json_from_cli_output() async throws {
ActiveShell.useSystem()
let container = Container.real()
let services = try! JSONDecoder().decode(
[HomebrewService].self,
from: await Shell.pipe(
"sudo \(Paths.brew) services info --all --json"
from: await container.shell.pipe(
"sudo \(container.paths.brew) services info --all --json"
).out.data(using: .utf8)!
).filter({ service in
return ["php", "nginx", "dnsmasq"].contains(service.name)
@@ -76,12 +76,11 @@ struct HomebrewPackageTest {
/// or the JSON API of the Homebrew output may have changed.
@Test(.disabled("Uses system command; enable at your own risk"))
func can_load_extension_json_from_cli_output() async throws {
ActiveShell.useSystem()
let container = Container.real()
let package = try! JSONDecoder().decode(
[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!
#expect(package.full_name == "php")

View File

@@ -10,42 +10,45 @@ import Testing
import Foundation
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 {
return TestBundle.url(forResource: "brew-outdated", withExtension: "json")!
}
@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
PhpEnvironments.brewPhpAlias = "8.2"
let env = App.shared.container.phpEnvs
let env = container.phpEnvs!
env.cachedPhpInstallations = [
"8.1": PhpInstallation("8.1.16"),
"8.2": PhpInstallation("8.2.3"),
"7.4": PhpInstallation("7.4.11")
"8.1": PhpInstallation(container, "8.1.16"),
"8.2": PhpInstallation(container, "8.2.3"),
"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
formula.installedVersion == "8.1.16" && formula.upgradeVersion == "8.1.17"

View File

@@ -10,6 +10,11 @@ import Testing
import Foundation
struct NginxConfigurationTest {
var container: Container
init () async throws {
container = Container.real()
}
// MARK: - Test Files
@@ -36,33 +41,33 @@ struct NginxConfigurationTest {
// MARK: - Tests
@Test func can_determine_site_name_and_tld() throws {
#expect("nginx-site" == NginxConfigurationFile.from(filePath: Self.regularUrl.path)?.domain)
#expect("test" == NginxConfigurationFile.from(filePath: Self.regularUrl.path)?.tld)
#expect("nginx-site" == NginxConfigurationFile.from(container, filePath: Self.regularUrl.path)?.domain)
#expect("test" == NginxConfigurationFile.from(container, filePath: Self.regularUrl.path)?.tld)
}
@Test func can_determine_isolation() throws {
#expect(nil == NginxConfigurationFile.from(filePath: Self.regularUrl.path)?.isolatedVersion)
#expect("8.1" == NginxConfigurationFile.from(filePath: Self.isolatedUrl.path)?.isolatedVersion)
#expect(nil == NginxConfigurationFile.from(container, filePath: Self.regularUrl.path)?.isolatedVersion)
#expect("8.1" == NginxConfigurationFile.from(container, filePath: Self.isolatedUrl.path)?.isolatedVersion)
}
@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("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(nil == normal.proxy)
}
@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("http://127.0.0.1:90" == proxied.proxy)
}
@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("http://localhost:8080" == proxied.proxy)
}

View File

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

View File

@@ -11,8 +11,10 @@ import Foundation
@Suite(.serialized)
struct PhpExtensionTest {
var container: Container
init () async throws {
ActiveShell.useSystem()
container = Container.real()
}
static var phpIniFileUrl: URL {
@@ -20,13 +22,13 @@ struct PhpExtensionTest {
}
@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)
}
@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
return ext.name
@@ -45,7 +47,7 @@ struct PhpExtensionTest {
}
@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
#expect(extensions[0].enabled == true)
@@ -56,7 +58,7 @@ struct PhpExtensionTest {
@Test func toggle_works_as_expected() async throws {
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)
// Try to disable xdebug (should be detected first)!
@@ -71,6 +73,6 @@ struct PhpExtensionTest {
#expect(file.contains("; zend_extension=\"xdebug.so\""))
// 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"
)
let contents = try! FileSystem.getShallowContentsOfDirectory("\(temporaryDirectory)/brew/etc/lib/c")
let contents = try! filesystem.getShallowContentsOfDirectory("\(temporaryDirectory)/brew/etc/lib/c")
#expect([] == contents)
}

View File

@@ -11,8 +11,9 @@ import Foundation
@Suite(.serialized)
struct TestableFileSystemTest {
var container: Container
init() throws {
ActiveFileSystem.useTestable([
container = Container.fake(files: [
"/home/user/bin/foo": .fake(.binary),
"/home/user/docs": .fake(.symlink, "/home/user/documents"),
"/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() {
#expect(FileSystem is TestableFileSystem)
}

View File

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

View File

@@ -63,8 +63,8 @@ struct TestableShellTest {
}
@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 {
@Test func configuration_can_be_saved_as_json() async {
let container = Container.real()
// WORKING
var configuration = TestableConfigurations.working
@@ -39,8 +41,8 @@ struct TestableConfigurationTest {
)
// Verify that the files were written to disk
#expect(FileSystem.fileExists(NSHomeDirectory() + "/.phpmon_fconf_working.json"))
#expect(FileSystem.fileExists(NSHomeDirectory() + "/.phpmon_fconf_working_no_valet.json"))
#expect(FileSystem.fileExists(NSHomeDirectory() + "/.phpmon_fconf_broken.json"))
#expect(container.filesystem.fileExists(NSHomeDirectory() + "/.phpmon_fconf_working.json"))
#expect(container.filesystem.fileExists(NSHomeDirectory() + "/.phpmon_fconf_working_no_valet.json"))
#expect(container.filesystem.fileExists(NSHomeDirectory() + "/.phpmon_fconf_broken.json"))
}
}

View File

@@ -9,9 +9,10 @@
import XCTest
class PhpVersionDetectionTest: XCTestCase {
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: [
"", // empty lines should be omitted
"php@8.0",