From 3965c3476cb03ba5ed63eee4d5ef3bed40b638c7 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Fri, 12 Sep 2025 14:45:07 +0200 Subject: [PATCH 01/33] =?UTF-8?q?=F0=9F=94=A7=20Bump=20build=20and=20versi?= =?UTF-8?q?on=20for=20Xcode=2026=20builds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index ff6bebf..5ff7eb7 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -3820,7 +3820,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1588; + CURRENT_PROJECT_VERSION = 1590; DEAD_CODE_STRIPPING = YES; DEBUG = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -3832,7 +3832,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 25.08; + MARKETING_VERSION = 25.09; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon; PRODUCT_MODULE_NAME = PHP_Monitor; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3850,7 +3850,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1588; + CURRENT_PROJECT_VERSION = 1590; DEAD_CODE_STRIPPING = YES; DEBUG = NO; ENABLE_HARDENED_RUNTIME = YES; @@ -3862,7 +3862,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 25.08; + MARKETING_VERSION = 25.09; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon; PRODUCT_MODULE_NAME = PHP_Monitor; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4083,7 +4083,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1588; + CURRENT_PROJECT_VERSION = 1590; DEAD_CODE_STRIPPING = YES; DEBUG = NO; ENABLE_HARDENED_RUNTIME = YES; @@ -4095,7 +4095,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 25.08; + MARKETING_VERSION = 25.09; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.dev; PRODUCT_MODULE_NAME = PHP_Monitor; PRODUCT_NAME = "$(TARGET_NAME) DEV"; @@ -4199,7 +4199,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1588; + CURRENT_PROJECT_VERSION = 1590; DEAD_CODE_STRIPPING = YES; DEBUG = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -4211,7 +4211,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 25.08; + MARKETING_VERSION = 25.09; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.dev; PRODUCT_MODULE_NAME = PHP_Monitor; PRODUCT_NAME = "$(TARGET_NAME) DEV"; @@ -4315,7 +4315,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1588; + CURRENT_PROJECT_VERSION = 1590; DEAD_CODE_STRIPPING = YES; DEBUG = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -4327,7 +4327,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 25.08; + MARKETING_VERSION = 25.09; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.eap; PRODUCT_MODULE_NAME = PHP_Monitor; PRODUCT_NAME = "$(TARGET_NAME) EAP"; @@ -4494,7 +4494,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1588; + CURRENT_PROJECT_VERSION = 1590; DEAD_CODE_STRIPPING = YES; DEBUG = NO; ENABLE_HARDENED_RUNTIME = YES; @@ -4506,7 +4506,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 25.08; + MARKETING_VERSION = 25.09; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.eap; PRODUCT_MODULE_NAME = PHP_Monitor; PRODUCT_NAME = "$(TARGET_NAME) EAP"; From 7f4612f767292854bf9fdda1f00ee0dd2ff9f4db Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Fri, 12 Sep 2025 15:02:27 +0200 Subject: [PATCH 02/33] =?UTF-8?q?=F0=9F=90=9B=20Fix=20alignment=20issue=20?= =?UTF-8?q?on=20macOS=2026?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpmon/Domain/App/Base.lproj/Main.storyboard | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/phpmon/Domain/App/Base.lproj/Main.storyboard b/phpmon/Domain/App/Base.lproj/Main.storyboard index f1a7b70..23f5f18 100644 --- a/phpmon/Domain/App/Base.lproj/Main.storyboard +++ b/phpmon/Domain/App/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -1443,7 +1443,7 @@ Gw - + @@ -717,7 +717,7 @@ Gw - + @@ -735,7 +735,7 @@ Gw - + @@ -750,7 +750,7 @@ Gw - + @@ -758,7 +758,7 @@ Gw - " + "" } - // swiftlint:enable void_function_in_ternary // MARK: - Reverting diff --git a/tests/Shared/TestableConfigurations.swift b/tests/Shared/TestableConfigurations.swift index 4e3b803..669df5f 100644 --- a/tests/Shared/TestableConfigurations.swift +++ b/tests/Shared/TestableConfigurations.swift @@ -138,7 +138,14 @@ class TestableConfigurations { : .instant(ShellStrings.shared.brewServicesAsRoot), "/opt/homebrew/bin/brew services info --all --json" : .instant(ShellStrings.shared.brewServicesAsUser), - "curl -s --max-time 10 '\(Constants.Urls.DevBuildCaskFile.absoluteString)'" + """ + curl -s --max-time 10 \ + -H "User-Agent: phpmon \(App.shortVersion)" \ + -H "X-phpmon-version: \(App.bundleVersion)" \ + -H "X-phpmon-os-version: \(App.macVersion)" \ + -H "X-phpmon-bundle-id: \(App.identifier)" \ + '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)' + """ : .delayed(0.5, """ cask 'phpmon-dev' do depends_on formula: 'gnu-sed' diff --git a/tests/ui/UpdateCheckTest.swift b/tests/ui/UpdateCheckTest.swift index 5cf82be..5bc936d 100644 --- a/tests/ui/UpdateCheckTest.swift +++ b/tests/ui/UpdateCheckTest.swift @@ -32,7 +32,14 @@ final class UpdateCheckTest: UITestCase { // Ensure an update is available configuration.shellOutput[ - "curl -s --max-time 10 '\(Constants.Urls.DevBuildCaskFile.absoluteString)'" + """ + curl -s --max-time 10 \ + -H "User-Agent: phpmon \(App.shortVersion)" \ + -H "X-phpmon-version: \(App.bundleVersion)" \ + -H "X-phpmon-os-version: \(App.macVersion)" \ + -H "X-phpmon-bundle-id: \(App.identifier)" \ + '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)' + """ ] = .delayed(0.5, """ cask 'phpmon-dev' do depends_on formula: 'gnu-sed' @@ -64,7 +71,14 @@ final class UpdateCheckTest: UITestCase { // Ensure an update is available configuration.shellOutput[ - "curl -s --max-time 10 '\(Constants.Urls.DevBuildCaskFile.absoluteString)'" + """ + curl -s --max-time 10 \ + -H "User-Agent: phpmon \(App.shortVersion)" \ + -H "X-phpmon-version: \(App.bundleVersion)" \ + -H "X-phpmon-os-version: \(App.macVersion)" \ + -H "X-phpmon-bundle-id: \(App.identifier)" \ + '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)' + """ ] = .delayed(0.5, """ cask 'phpmon-dev' do depends_on formula: 'gnu-sed' @@ -104,7 +118,14 @@ final class UpdateCheckTest: UITestCase { // Ensure an update is available configuration.shellOutput[ - "curl -s --max-time 10 '\(Constants.Urls.DevBuildCaskFile.absoluteString)'" + """ + curl -s --max-time 10 \ + -H "User-Agent: phpmon \(App.shortVersion)" \ + -H "X-phpmon-version: \(App.bundleVersion)" \ + -H "X-phpmon-os-version: \(App.macVersion)" \ + -H "X-phpmon-bundle-id: \(App.identifier)" \ + '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)' + """ ] = .delayed(0.5, "404 PAGE NOT FOUND") // Wait for the menu to open and search for updates diff --git a/tests/unit/Parsers/CaskFileParserTest.swift b/tests/unit/Parsers/CaskFileParserTest.swift index 8b74e80..bd783dd 100644 --- a/tests/unit/Parsers/CaskFileParserTest.swift +++ b/tests/unit/Parsers/CaskFileParserTest.swift @@ -40,7 +40,9 @@ class CaskFileParserTest: XCTestCase { } func test_can_extract_fields_from_remote_cask_file() async throws { - guard let caskFile = await CaskFile.from(url: Constants.Urls.StableBuildCaskFile) else { + let url = URL(string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon.rb")! + + guard let caskFile = await CaskFile.from(url: url) else { return XCTFail("The remote CaskFile could not be parsed, check the log for more info") } From 0afbf0ddd4159311845c4be2691a10ca953b418e Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Thu, 25 Sep 2025 16:38:33 +0200 Subject: [PATCH 11/33] =?UTF-8?q?=E2=9C=85=20Address=20testable=20configur?= =?UTF-8?q?ations=20for=20fake=20Valet=20sites?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/Shared/TestableConfigurations.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/Shared/TestableConfigurations.swift b/tests/Shared/TestableConfigurations.swift index 669df5f..68820bc 100644 --- a/tests/Shared/TestableConfigurations.swift +++ b/tests/Shared/TestableConfigurations.swift @@ -180,6 +180,10 @@ class TestableConfigurations { : .delayed(0.2, "OK"), "ln -sF ~/.config/valet/valet82.sock ~/.config/valet/valet.sock" : .instant("OK"), + "ln -sF ~/.config/valet/valet83.sock ~/.config/valet/valet.sock" + : .instant("OK"), + "ln -sF ~/.config/valet/valet84.sock ~/.config/valet/valet.sock" + : .instant("OK"), "/opt/homebrew/bin/brew update >/dev/null && /opt/homebrew/bin/brew outdated --json --formulae": .delayed(2.0, """ { "formulae": [ @@ -206,6 +210,8 @@ class TestableConfigurations { .automaticBackgroundUpdateCheck: false ], phpVersions: [ + VersionNumber(major: 8, minor: 4, patch: 5), + VersionNumber(major: 8, minor: 3, patch: 5), VersionNumber(major: 8, minor: 2, patch: 6), VersionNumber(major: 8, minor: 1, patch: 0), VersionNumber(major: 8, minor: 0, patch: 0), From 3d403f5ef68f289f0277c19ca224b533a31fa86b Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Thu, 25 Sep 2025 17:05:32 +0200 Subject: [PATCH 12/33] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Modernize=20test=20s?= =?UTF-8?q?uite=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PHP/PHP Version/PhpEnvironments.swift | 8 +- tests/Shared/TestableConfigurations.swift | 25 +- tests/ui/MainMenuTest.swift | 7 +- tests/unit/Parsers/HomebrewPackageTest.swift | 4 +- tests/unit/Test Files/brew/brew-formula.json | 1120 +++++++++++------ 5 files changed, 733 insertions(+), 431 deletions(-) diff --git a/phpmon/Common/PHP/PHP Version/PhpEnvironments.swift b/phpmon/Common/PHP/PHP Version/PhpEnvironments.swift index bc620bb..33fedab 100644 --- a/phpmon/Common/PHP/PHP Version/PhpEnvironments.swift +++ b/phpmon/Common/PHP/PHP Version/PhpEnvironments.swift @@ -111,7 +111,13 @@ class PhpEnvironments { It's possible for the alias to be newer than the actual installed version of PHP. */ static var homebrewBrewPhpAlias: String { - if PhpEnvironments.shared.homebrewPackage == nil { return "8.2" } + if PhpEnvironments.shared.homebrewPackage == nil { + // For UI testing and as a fallback, determine this version by using (fake) php-config + let version = Command.execute(path: "/opt/homebrew/bin/php-config", + arguments: ["--version"], + trimNewlines: true) + return try! VersionNumber.parse(version).short + } return PhpEnvironments.shared.homebrewPackage.version } diff --git a/tests/Shared/TestableConfigurations.swift b/tests/Shared/TestableConfigurations.swift index 68820bc..9f6c2de 100644 --- a/tests/Shared/TestableConfigurations.swift +++ b/tests/Shared/TestableConfigurations.swift @@ -52,17 +52,16 @@ class TestableConfigurations { shellOutput: [ "/opt/homebrew/bin/brew --version" : .instant(""" - Homebrew 4.0.17-93-gb0dc84b - Homebrew/homebrew-core (git revision 4113c35d80d; last commit 2023-04-06) - Homebrew/homebrew-cask (git revision bcd8ecb74c; last commit 2023-04-06) + Homebrew 4.6.11 """), "/opt/homebrew/bin/php -v" : .instant(""" - PHP 8.2.6 (cli) (built: May 11 2023 12:51:38) (NTS) + PHP 8.4.5 (cli) (built: Aug 26 2025 13:36:28) (NTS) Copyright (c) The PHP Group - Zend Engine v4.2.6, Copyright (c) Zend Technologies - with Zend OPcache v8.2.6, Copyright (c), by Zend Technologies - with Xdebug v3.2.0, Copyright (c) 2002-2022, by Derick Rethans + Built by Homebrew + Zend Engine v4.4.12, Copyright (c) Zend Technologies + with Xdebug v3.4.5, Copyright (c) 2002-2025, by Derick Rethans + with Zend OPcache v8.4.12, Copyright (c), by Zend Technologies """), "sysctl -n sysctl.proc_translated" : .instant("0"), @@ -105,7 +104,7 @@ class TestableConfigurations { %admin ALL=(root) NOPASSWD:SETENV: VALET """), "valet --version" - : .instant("Laravel Valet 3.1.11"), + : .instant("Laravel Valet 4.9.0"), "/opt/homebrew/bin/brew tap" : .instant(""" homebrew/cask @@ -150,7 +149,7 @@ class TestableConfigurations { cask 'phpmon-dev' do depends_on formula: 'gnu-sed' - version '6.0.0_1000' + version '25.08.0_1000' sha256 '1cb147bd1b1fbd52971d90dff577465b644aee7c878f15ede57f46e8f217067a' url 'https://github.com/nicoverbruggen/phpmon/releases/download/v6.0/phpmon-dev.zip' @@ -178,10 +177,6 @@ class TestableConfigurations { : .delayed(0.2, "OK"), "sudo /opt/homebrew/bin/brew services start dnsmasq" : .delayed(0.2, "OK"), - "ln -sF ~/.config/valet/valet82.sock ~/.config/valet/valet.sock" - : .instant("OK"), - "ln -sF ~/.config/valet/valet83.sock ~/.config/valet/valet.sock" - : .instant("OK"), "ln -sF ~/.config/valet/valet84.sock ~/.config/valet/valet.sock" : .instant("OK"), "/opt/homebrew/bin/brew update >/dev/null && /opt/homebrew/bin/brew outdated --json --formulae": .delayed(2.0, """ @@ -190,9 +185,9 @@ class TestableConfigurations { { "name": "php", "installed_versions": [ - "8.2.6" + "8.4.5" ], - "current_version": "8.2.11", + "current_version": "8.4.11", "pinned": false, "pinned_version": null } diff --git a/tests/ui/MainMenuTest.swift b/tests/ui/MainMenuTest.swift index b3d88ad..e268c99 100644 --- a/tests/ui/MainMenuTest.swift +++ b/tests/ui/MainMenuTest.swift @@ -18,11 +18,12 @@ final class MainMenuTest: UITestCase { let app = launch(openMenu: true) assertAllExist([ - // "Switch to PHP 8.2 (php)" should be visible since it is aliased to `php` - app.menuItems["\("mi_php_switch".localized) 8.2 (php)"], + // "Switch to PHP 8.4 (php)" should be visible since it is aliased to `php` + app.menuItems["\("mi_php_switch".localized) 8.4 (php)"], // "Switch to PHP 8.1" should be the non-disabled option + app.menuItems["\("mi_php_switch".localized) 8.3 (php@8.3)"], + app.menuItems["\("mi_php_switch".localized) 8.2 (php@8.2)"], app.menuItems["\("mi_php_switch".localized) 8.1 (php@8.1)"], - // "Switch to PHP 8.0" should be the non-disabled option app.menuItems["\("mi_php_switch".localized) 8.0 (php@8.0)"], // We should see the about and quit items app.menuItems["mi_about".localized], diff --git a/tests/unit/Parsers/HomebrewPackageTest.swift b/tests/unit/Parsers/HomebrewPackageTest.swift index a8a8916..e6a5f8a 100644 --- a/tests/unit/Parsers/HomebrewPackageTest.swift +++ b/tests/unit/Parsers/HomebrewPackageTest.swift @@ -24,9 +24,9 @@ class HomebrewPackageTest: XCTestCase { ).first! XCTAssertEqual(package.full_name, "php") - XCTAssertEqual(package.aliases.first!, "php@8.2") + XCTAssertEqual(package.aliases.first!, "php@8.4") XCTAssertEqual(package.installed.contains(where: { installed in - installed.version.starts(with: "8.2") + installed.version.starts(with: "8.4") }), true) } diff --git a/tests/unit/Test Files/brew/brew-formula.json b/tests/unit/Test Files/brew/brew-formula.json index 895492a..d4bdaf4 100644 --- a/tests/unit/Test Files/brew/brew-formula.json +++ b/tests/unit/Test Files/brew/brew-formula.json @@ -1,416 +1,716 @@ [ - { - "full_name": "php", - "tap": "homebrew/core", - "oldname": null, - "aliases": [ - "php@8.2" + { + "name": "php", + "full_name": "php", + "tap": "homebrew/core", + "oldnames": [], + "aliases": [ + "php@8.4" + ], + "versioned_formulae": [ + "php@8.3", + "php@8.2", + "php@8.1" + ], + "desc": "General-purpose scripting language", + "license": "PHP-3.01", + "homepage": "https://www.php.net/", + "versions": { + "stable": "8.4.12", + "head": "HEAD", + "bottle": true + }, + "urls": { + "stable": { + "url": "https://www.php.net/distributions/php-8.4.12.tar.xz", + "tag": null, + "revision": null, + "using": null, + "checksum": "c1b7978cbb5054eed6c749bde4444afc16a3f2268101fb70a7d5d9b1083b12ad" + }, + "head": { + "url": "https://github.com/php/php-src.git", + "branch": "master", + "using": null + } + }, + "revision": 0, + "version_scheme": 0, + "autobump": true, + "no_autobump_message": null, + "skip_livecheck": false, + "bottle": { + "stable": { + "rebuild": 2, + "root_url": "https://ghcr.io/v2/homebrew/core", + "files": { + "arm64_tahoe": { + "cellar": "/opt/homebrew/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/php/blobs/sha256:e40592e2731a866feb2b91f16d094e9c9cf605db90035762a2ee367316bc5403", + "sha256": "e40592e2731a866feb2b91f16d094e9c9cf605db90035762a2ee367316bc5403" + }, + "arm64_sequoia": { + "cellar": "/opt/homebrew/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/php/blobs/sha256:8a2728015733295d45652fd123c902301a617f5e5fc138daca5f873f58fbc6ae", + "sha256": "8a2728015733295d45652fd123c902301a617f5e5fc138daca5f873f58fbc6ae" + }, + "arm64_sonoma": { + "cellar": "/opt/homebrew/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/php/blobs/sha256:db9c8789e22f0b191805554eaf9a2a77f84e69a9cae6cd1764b6144f4e8bbcd0", + "sha256": "db9c8789e22f0b191805554eaf9a2a77f84e69a9cae6cd1764b6144f4e8bbcd0" + }, + "sonoma": { + "cellar": "/usr/local/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/php/blobs/sha256:c3c3cfade78d29299f78825a66691f8583380cea416a3f7aa35f0abf9e8574da", + "sha256": "c3c3cfade78d29299f78825a66691f8583380cea416a3f7aa35f0abf9e8574da" + }, + "arm64_linux": { + "cellar": "/home/linuxbrew/.linuxbrew/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/php/blobs/sha256:ccf76dc0da52b6d72e19bbcff284628cea4c36508eaff4aa8ff614d3749686a4", + "sha256": "ccf76dc0da52b6d72e19bbcff284628cea4c36508eaff4aa8ff614d3749686a4" + }, + "x86_64_linux": { + "cellar": "/home/linuxbrew/.linuxbrew/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/php/blobs/sha256:56a6b123525c3ff49a58b05d05c9e8da50cb349973f048dc1d8963f64a042bfa", + "sha256": "56a6b123525c3ff49a58b05d05c9e8da50cb349973f048dc1d8963f64a042bfa" + } + } + } + }, + "pour_bottle_only_if": null, + "keg_only": false, + "keg_only_reason": null, + "options": [], + "build_dependencies": [ + "httpd", + "pkgconf" + ], + "dependencies": [ + "apr", + "apr-util", + "argon2", + "autoconf", + "curl", + "freetds", + "gd", + "gettext", + "gmp", + "icu4c@77", + "krb5", + "libpq", + "libsodium", + "libzip", + "net-snmp", + "oniguruma", + "openldap", + "openssl@3", + "pcre2", + "sqlite", + "tidy-html5", + "unixodbc", + "gcc" + ], + "test_dependencies": [ + "httpd" + ], + "recommended_dependencies": [], + "optional_dependencies": [], + "uses_from_macos": [ + { + "xz": "build" + }, + "bzip2", + "libedit", + "libffi", + "libxml2", + "libxslt", + "zlib" + ], + "uses_from_macos_bounds": [ + {}, + {}, + {}, + { + "since": "catalina" + }, + {}, + {}, + {} + ], + "requirements": [], + "conflicts_with": [], + "conflicts_with_reasons": [], + "link_overwrite": [], + "caveats": "To enable PHP in Apache add the following to httpd.conf and restart Apache:\n LoadModule php_module $HOMEBREW_PREFIX/opt/php/lib/httpd/modules/libphp.so\n\n \n SetHandler application/x-httpd-php\n \n\nFinally, check DirectoryIndex includes index.php\n DirectoryIndex index.php index.html\n\nThe php.ini and php-fpm.ini file can be found in:\n $HOMEBREW_PREFIX/etc/php/8.4/\n", + "installed": [ + { + "version": "8.4.5", + "used_options": [], + "built_as_bottle": false, + "poured_from_bottle": false, + "time": null, + "runtime_dependencies": null, + "installed_as_dependency": false, + "installed_on_request": false + }, + { + "version": "8.4.12", + "used_options": [], + "built_as_bottle": true, + "poured_from_bottle": true, + "time": 1758463084, + "runtime_dependencies": [ + { + "full_name": "apr", + "version": "1.7.6", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "1.7.6", + "declared_directly": true + }, + { + "full_name": "ca-certificates", + "version": "2025-09-09", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "2025-09-09", + "declared_directly": false + }, + { + "full_name": "openssl@3", + "version": "3.5.2", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "3.5.2", + "declared_directly": true + }, + { + "full_name": "apr-util", + "version": "1.6.3", + "revision": 1, + "bottle_rebuild": 0, + "pkg_version": "1.6.3_1", + "declared_directly": true + }, + { + "full_name": "argon2", + "version": "20190702", + "revision": 1, + "bottle_rebuild": 0, + "pkg_version": "20190702_1", + "declared_directly": true + }, + { + "full_name": "m4", + "version": "1.4.20", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "1.4.20", + "declared_directly": false + }, + { + "full_name": "autoconf", + "version": "2.72", + "revision": 0, + "bottle_rebuild": 1, + "pkg_version": "2.72", + "declared_directly": true + }, + { + "full_name": "brotli", + "version": "1.1.0", + "revision": 0, + "bottle_rebuild": 2, + "pkg_version": "1.1.0", + "declared_directly": false + }, + { + "full_name": "libnghttp2", + "version": "1.67.1", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "1.67.1", + "declared_directly": false + }, + { + "full_name": "libnghttp3", + "version": "1.11.0", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "1.11.0", + "declared_directly": false + }, + { + "full_name": "libngtcp2", + "version": "1.15.1", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "1.15.1", + "declared_directly": false + }, + { + "full_name": "libssh2", + "version": "1.11.1", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "1.11.1", + "declared_directly": false + }, + { + "full_name": "rtmpdump", + "version": "2.4-20151223", + "revision": 3, + "bottle_rebuild": 0, + "pkg_version": "2.4-20151223_3", + "declared_directly": false + }, + { + "full_name": "lz4", + "version": "1.10.0", + "revision": 0, + "bottle_rebuild": 1, + "pkg_version": "1.10.0", + "declared_directly": false + }, + { + "full_name": "xz", + "version": "5.8.1", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "5.8.1", + "declared_directly": false + }, + { + "full_name": "zstd", + "version": "1.5.7", + "revision": 0, + "bottle_rebuild": 1, + "pkg_version": "1.5.7", + "declared_directly": false + }, + { + "full_name": "curl", + "version": "8.16.0", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "8.16.0", + "declared_directly": true + }, + { + "full_name": "libtool", + "version": "2.5.4", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "2.5.4", + "declared_directly": false + }, + { + "full_name": "unixodbc", + "version": "2.3.12", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "2.3.12", + "declared_directly": true + }, + { + "full_name": "freetds", + "version": "1.5.6", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "1.5.6", + "declared_directly": true + }, + { + "full_name": "libpng", + "version": "1.6.50", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "1.6.50", + "declared_directly": false + }, + { + "full_name": "freetype", + "version": "2.14.1", + "revision": 1, + "bottle_rebuild": 0, + "pkg_version": "2.14.1_1", + "declared_directly": false + }, + { + "full_name": "libunistring", + "version": "1.3", + "revision": 0, + "bottle_rebuild": 1, + "pkg_version": "1.3", + "declared_directly": false + }, + { + "full_name": "gettext", + "version": "0.26", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "0.26", + "declared_directly": true + }, + { + "full_name": "fontconfig", + "version": "2.17.1", + "revision": 0, + "bottle_rebuild": 1, + "pkg_version": "2.17.1", + "declared_directly": false + }, + { + "full_name": "jpeg-turbo", + "version": "3.1.2", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "3.1.2", + "declared_directly": false + }, + { + "full_name": "giflib", + "version": "5.2.2", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "5.2.2", + "declared_directly": false + }, + { + "full_name": "highway", + "version": "1.3.0", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "1.3.0", + "declared_directly": false + }, + { + "full_name": "imath", + "version": "3.2.1", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "3.2.1", + "declared_directly": false + }, + { + "full_name": "libtiff", + "version": "4.7.1", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "4.7.1", + "declared_directly": false + }, + { + "full_name": "little-cms2", + "version": "2.17", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "2.17", + "declared_directly": false + }, + { + "full_name": "libdeflate", + "version": "1.24", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "1.24", + "declared_directly": false + }, + { + "full_name": "openjph", + "version": "0.23.1", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "0.23.1", + "declared_directly": false + }, + { + "full_name": "openexr", + "version": "3.4.0", + "revision": 1, + "bottle_rebuild": 0, + "pkg_version": "3.4.0_1", + "declared_directly": false + }, + { + "full_name": "webp", + "version": "1.6.0", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "1.6.0", + "declared_directly": false + }, + { + "full_name": "jpeg-xl", + "version": "0.11.1", + "revision": 3, + "bottle_rebuild": 0, + "pkg_version": "0.11.1_3", + "declared_directly": false + }, + { + "full_name": "libvmaf", + "version": "3.0.0", + "revision": 0, + "bottle_rebuild": 1, + "pkg_version": "3.0.0", + "declared_directly": false + }, + { + "full_name": "aom", + "version": "3.13.1", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "3.13.1", + "declared_directly": false + }, + { + "full_name": "libavif", + "version": "1.3.0", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "1.3.0", + "declared_directly": false + }, + { + "full_name": "gd", + "version": "2.3.3", + "revision": 6, + "bottle_rebuild": 0, + "pkg_version": "2.3.3_6", + "declared_directly": true + }, + { + "full_name": "gmp", + "version": "6.3.0", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "6.3.0", + "declared_directly": true + }, + { + "full_name": "icu4c@77", + "version": "77.1", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "77.1", + "declared_directly": true + }, + { + "full_name": "krb5", + "version": "1.22.1", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "1.22.1", + "declared_directly": true + }, + { + "full_name": "libpq", + "version": "17.6", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "17.6", + "declared_directly": true + }, + { + "full_name": "libsodium", + "version": "1.0.20", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "1.0.20", + "declared_directly": true + }, + { + "full_name": "libzip", + "version": "1.11.4", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "1.11.4", + "declared_directly": true + }, + { + "full_name": "net-snmp", + "version": "5.9.4", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "5.9.4", + "declared_directly": true + }, + { + "full_name": "oniguruma", + "version": "6.9.10", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "6.9.10", + "declared_directly": true + }, + { + "full_name": "openldap", + "version": "2.6.10", + "revision": 1, + "bottle_rebuild": 1, + "pkg_version": "2.6.10_1", + "declared_directly": true + }, + { + "full_name": "pcre2", + "version": "10.46", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "10.46", + "declared_directly": true + }, + { + "full_name": "readline", + "version": "8.3.1", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "8.3.1", + "declared_directly": false + }, + { + "full_name": "sqlite", + "version": "3.50.4", + "revision": 0, + "bottle_rebuild": 1, + "pkg_version": "3.50.4", + "declared_directly": true + }, + { + "full_name": "tidy-html5", + "version": "5.8.0", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "5.8.0", + "declared_directly": true + }, + { + "full_name": "isl", + "version": "0.27", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "0.27", + "declared_directly": false + }, + { + "full_name": "mpfr", + "version": "4.2.2", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "4.2.2", + "declared_directly": false + }, + { + "full_name": "libmpc", + "version": "1.3.1", + "revision": 0, + "bottle_rebuild": 0, + "pkg_version": "1.3.1", + "declared_directly": false + }, + { + "full_name": "gcc", + "version": "15.1.0", + "revision": 0, + "bottle_rebuild": 2, + "pkg_version": "15.1.0", + "declared_directly": true + } ], - "versioned_formulae": [ - "php@8.1", - "php@8.0", - "php@7.4" - ], - "desc": "General-purpose scripting language", - "license": "PHP-3.01", - "homepage": "https://www.php.net/", - "versions": { - "stable": "8.2.2", - "head": "HEAD", - "bottle": true + "installed_as_dependency": true, + "installed_on_request": true + } + ], + "linked_keg": "8.4.12", + "pinned": false, + "outdated": false, + "deprecated": false, + "deprecation_date": null, + "deprecation_reason": null, + "deprecation_replacement_formula": null, + "deprecation_replacement_cask": null, + "disabled": false, + "disable_date": null, + "disable_reason": null, + "disable_replacement_formula": null, + "disable_replacement_cask": null, + "post_install_defined": true, + "service": { + "run": [ + "/opt/homebrew/opt/php/sbin/php-fpm", + "--nodaemonize" + ], + "run_type": "immediate", + "keep_alive": { + "always": true + }, + "working_dir": "/opt/homebrew/var", + "error_log_path": "/opt/homebrew/var/log/php-fpm.log" + }, + "tap_git_head": "373a872625b689ca6fbc7bfb8e22ef39e7d9e46b", + "ruby_source_path": "Formula/p/php.rb", + "ruby_source_checksum": { + "sha256": "cf38870ab4612023ac385bb5b391dad56e6aac69366b2773f35232317dcbd7dd" + }, + "head_dependencies": { + "build_dependencies": [ + "bison", + "re2c", + "httpd", + "pkgconf" + ], + "dependencies": [ + "apr", + "apr-util", + "argon2", + "autoconf", + "curl", + "freetds", + "gd", + "gettext", + "gmp", + "icu4c@77", + "krb5", + "libpq", + "libsodium", + "libzip", + "net-snmp", + "oniguruma", + "openldap", + "openssl@3", + "pcre2", + "sqlite", + "tidy-html5", + "unixodbc", + "gcc" + ], + "test_dependencies": [ + "httpd" + ], + "recommended_dependencies": [], + "optional_dependencies": [], + "uses_from_macos": [ + { + "xz": "build" }, - "urls": { - "stable": { - "url": "https://www.php.net/distributions/php-8.2.2.tar.xz", - "tag": null, - "revision": null, - "checksum": "bdc4aa38e652bac86039601840bae01c0c3653972eaa6f9f93d5f71953a7ee33" - }, - "head": { - "url": "https://github.com/php/php-src.git", - "branch": "master" - } + "bzip2", + "libedit", + "libffi", + "libxml2", + "libxslt", + "zlib" + ], + "uses_from_macos_bounds": [ + {}, + {}, + {}, + { + "since": "catalina" }, - "revision": 0, - "version_scheme": 0, - "bottle": { - "stable": { - "rebuild": 0, - "root_url": "https://ghcr.io/v2/homebrew/core", - "files": { - "arm64_ventura": { - "cellar": "/opt/homebrew/Cellar", - "url": "https://ghcr.io/v2/homebrew/core/php/blobs/sha256:ad2e6a6f1cdc65c22b39bd607cbb7305958951cf58ee87d5060717be5a8b5a45", - "sha256": "ad2e6a6f1cdc65c22b39bd607cbb7305958951cf58ee87d5060717be5a8b5a45" - }, - "arm64_monterey": { - "cellar": "/opt/homebrew/Cellar", - "url": "https://ghcr.io/v2/homebrew/core/php/blobs/sha256:27069c973e63f38a3cb4fad1c7a2e17853bcffe318c8a957ff96a1026dff0cac", - "sha256": "27069c973e63f38a3cb4fad1c7a2e17853bcffe318c8a957ff96a1026dff0cac" - }, - "arm64_big_sur": { - "cellar": "/opt/homebrew/Cellar", - "url": "https://ghcr.io/v2/homebrew/core/php/blobs/sha256:ceef280bcd57e5f794ae59cc75e83d407c9704aa3d238b282bda52cbc644d0dd", - "sha256": "ceef280bcd57e5f794ae59cc75e83d407c9704aa3d238b282bda52cbc644d0dd" - }, - "ventura": { - "cellar": "/usr/local/Cellar", - "url": "https://ghcr.io/v2/homebrew/core/php/blobs/sha256:22f733b7b0b0ed95cd6b0a1534b9eca4cf63fe54647394c3f7e7ac019eb019ff", - "sha256": "22f733b7b0b0ed95cd6b0a1534b9eca4cf63fe54647394c3f7e7ac019eb019ff" - }, - "monterey": { - "cellar": "/usr/local/Cellar", - "url": "https://ghcr.io/v2/homebrew/core/php/blobs/sha256:9ff8f5e1df5e849567cdb2ddea6d3c2a2b9cae024842c9ac65b35a01657bfc37", - "sha256": "9ff8f5e1df5e849567cdb2ddea6d3c2a2b9cae024842c9ac65b35a01657bfc37" - }, - "big_sur": { - "cellar": "/usr/local/Cellar", - "url": "https://ghcr.io/v2/homebrew/core/php/blobs/sha256:11fd1ea6da8ef728b7cacd4da8a51ed125069595abf4e37ae1552d418560c5fb", - "sha256": "11fd1ea6da8ef728b7cacd4da8a51ed125069595abf4e37ae1552d418560c5fb" - }, - "x86_64_linux": { - "cellar": "/home/linuxbrew/.linuxbrew/Cellar", - "url": "https://ghcr.io/v2/homebrew/core/php/blobs/sha256:baaa41e60f9e8125fe8f549d4813a8476a8947a1f10d7817a2ee36d8baa625f3", - "sha256": "baaa41e60f9e8125fe8f549d4813a8476a8947a1f10d7817a2ee36d8baa625f3" - } - } - } - }, - "keg_only": false, - "keg_only_reason": null, - "options": [ - - ], - "build_dependencies": [ - "httpd", - "pkg-config" - ], - "dependencies": [ - "apr", - "apr-util", - "argon2", - "aspell", - "autoconf", - "curl", - "freetds", - "gd", - "gettext", - "gmp", - "icu4c", - "krb5", - "libpq", - "libsodium", - "libzip", - "oniguruma", - "openldap", - "openssl@1.1", - "pcre2", - "sqlite", - "tidy-html5", - "unixodbc" - ], - "test_dependencies": [ - "httpd" - ], - "recommended_dependencies": [ - - ], - "optional_dependencies": [ - - ], - "uses_from_macos": [ - { - "xz": "build" - }, - "bzip2", - "libedit", - "libffi", - "libxml2", - "libxslt", - "zlib" - ], - "requirements": [ - - ], - "conflicts_with": [ - - ], - "caveats": "To enable PHP in Apache add the following to httpd.conf and restart Apache:\n LoadModule php_module $(brew --prefix)/opt/php/lib/httpd/modules/libphp.so\n\n \n SetHandler application/x-httpd-php\n \n\nFinally, check DirectoryIndex includes index.php\n DirectoryIndex index.php index.html\n\nThe php.ini and php-fpm.ini file can be found in:\n $(brew --prefix)/etc/php/8.2/\n", - "installed": [ - { - "version": "8.2.2", - "used_options": [ - - ], - "built_as_bottle": true, - "poured_from_bottle": true, - "time": 1675654665, - "runtime_dependencies": [ - { - "full_name": "apr", - "version": "1.7.2", - "declared_directly": true - }, - { - "full_name": "ca-certificates", - "version": "2023-01-10", - "declared_directly": false - }, - { - "full_name": "openssl@1.1", - "version": "1.1.1s", - "declared_directly": true - }, - { - "full_name": "apr-util", - "version": "1.6.3", - "declared_directly": true - }, - { - "full_name": "argon2", - "version": "20190702", - "declared_directly": true - }, - { - "full_name": "aspell", - "version": "0.60.8", - "declared_directly": true - }, - { - "full_name": "m4", - "version": "1.4.19", - "declared_directly": false - }, - { - "full_name": "autoconf", - "version": "2.71", - "declared_directly": true - }, - { - "full_name": "brotli", - "version": "1.0.9", - "declared_directly": false - }, - { - "full_name": "libunistring", - "version": "1.1", - "declared_directly": false - }, - { - "full_name": "gettext", - "version": "0.21.1", - "declared_directly": true - }, - { - "full_name": "libidn2", - "version": "2.3.4", - "declared_directly": false - }, - { - "full_name": "libnghttp2", - "version": "1.51.0", - "declared_directly": false - }, - { - "full_name": "libssh2", - "version": "1.10.0", - "declared_directly": false - }, - { - "full_name": "openldap", - "version": "2.6.3", - "declared_directly": true - }, - { - "full_name": "rtmpdump", - "version": "2.4+20151223", - "declared_directly": false - }, - { - "full_name": "lz4", - "version": "1.9.4", - "declared_directly": false - }, - { - "full_name": "xz", - "version": "5.4.1", - "declared_directly": false - }, - { - "full_name": "zstd", - "version": "1.5.2", - "declared_directly": false - }, - { - "full_name": "curl", - "version": "7.87.0", - "declared_directly": true - }, - { - "full_name": "libtool", - "version": "2.4.7", - "declared_directly": false - }, - { - "full_name": "unixodbc", - "version": "2.3.11", - "declared_directly": true - }, - { - "full_name": "freetds", - "version": "1.3.17", - "declared_directly": true - }, - { - "full_name": "libpng", - "version": "1.6.39", - "declared_directly": false - }, - { - "full_name": "freetype", - "version": "2.12.1", - "declared_directly": false - }, - { - "full_name": "fontconfig", - "version": "2.14.2", - "declared_directly": false - }, - { - "full_name": "jpeg-turbo", - "version": "2.1.5", - "declared_directly": false - }, - { - "full_name": "giflib", - "version": "5.2.1", - "declared_directly": false - }, - { - "full_name": "highway", - "version": "1.0.3", - "declared_directly": false - }, - { - "full_name": "imath", - "version": "3.1.6", - "declared_directly": false - }, - { - "full_name": "libtiff", - "version": "4.4.0", - "declared_directly": false - }, - { - "full_name": "little-cms2", - "version": "2.14", - "declared_directly": false - }, - { - "full_name": "openexr", - "version": "3.1.5", - "declared_directly": false - }, - { - "full_name": "webp", - "version": "1.3.0", - "declared_directly": false - }, - { - "full_name": "jpeg-xl", - "version": "0.8.1", - "declared_directly": false - }, - { - "full_name": "libvmaf", - "version": "2.3.1", - "declared_directly": false - }, - { - "full_name": "aom", - "version": "3.5.0", - "declared_directly": false - }, - { - "full_name": "libavif", - "version": "0.11.1", - "declared_directly": false - }, - { - "full_name": "gd", - "version": "2.3.3", - "declared_directly": true - }, - { - "full_name": "gmp", - "version": "6.2.1", - "declared_directly": true - }, - { - "full_name": "icu4c", - "version": "72.1", - "declared_directly": true - }, - { - "full_name": "krb5", - "version": "1.20.1", - "declared_directly": true - }, - { - "full_name": "libpq", - "version": "15.1", - "declared_directly": true - }, - { - "full_name": "libsodium", - "version": "1.0.18", - "declared_directly": true - }, - { - "full_name": "libzip", - "version": "1.9.2", - "declared_directly": true - }, - { - "full_name": "oniguruma", - "version": "6.9.8", - "declared_directly": true - }, - { - "full_name": "pcre2", - "version": "10.42", - "declared_directly": true - }, - { - "full_name": "readline", - "version": "8.2.1", - "declared_directly": false - }, - { - "full_name": "sqlite", - "version": "3.40.1", - "declared_directly": true - }, - { - "full_name": "tidy-html5", - "version": "5.8.0", - "declared_directly": true - } - ], - "installed_as_dependency": false, - "installed_on_request": true - } - ], - "linked_keg": "8.2.2", - "pinned": false, - "outdated": false, - "deprecated": false, - "deprecation_date": null, - "deprecation_reason": null, - "disabled": false, - "disable_date": null, - "disable_reason": null, - "tap_git_head": "0bbb89420e74756a5a5c145ed7efa4a32f7e7e7c" + {}, + {}, + {} + ] } - + } ] From 5b6a804667d420c30d5d9e578ba105d5b34ef455 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Thu, 25 Sep 2025 17:38:00 +0200 Subject: [PATCH 13/33] =?UTF-8?q?=E2=9C=85=20Fix=20some=20more=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/ui/StartupTest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/StartupTest.swift b/tests/ui/StartupTest.swift index 2c00802..2fd4d30 100644 --- a/tests/ui/StartupTest.swift +++ b/tests/ui/StartupTest.swift @@ -48,7 +48,7 @@ final class StartupTest: UITestCase { final func test_get_warning_about_missing_fpm_symlink() throws { var configuration = TestableConfigurations.working - configuration.filesystem["/opt/homebrew/etc/php/8.2/php-fpm.d/valet-fpm.conf"] = nil + configuration.filesystem["/opt/homebrew/etc/php/8.4/php-fpm.d/valet-fpm.conf"] = nil let app = launch(with: configuration) From 1a8fe7e7fcae1b544f5ff9b89f5fb3a18d6ad6c5 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Fri, 26 Sep 2025 15:46:37 +0200 Subject: [PATCH 14/33] =?UTF-8?q?=E2=9C=A8=20Improved=20update=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpmon/Common/Core/Constants.swift | 14 ++++- .../Analytics/LoggableEvent.swift | 12 ++-- phpmon/Domain/Menu/MainMenu+Startup.swift | 58 ++++++++++++++++++- .../Domain/Preferences/PreferenceName.swift | 14 ++++- phpmon/Domain/Preferences/Preferences.swift | 7 ++- phpmon/Modules/Domain List/Favorites.swift | 4 +- 6 files changed, 95 insertions(+), 14 deletions(-) diff --git a/phpmon/Common/Core/Constants.swift b/phpmon/Common/Core/Constants.swift index d075832..b984224 100644 --- a/phpmon/Common/Core/Constants.swift +++ b/phpmon/Common/Core/Constants.swift @@ -21,13 +21,25 @@ struct Constants { /** The amount of seconds that is considered the threshold for PHP Monitor to mark any given launch as a "slow" launch. - + If the startup procedure was slow (or hangs), this message should be displayed. This is based on an appropriate launch time on a basic M1 Apple chip, with some margin for slower Intel chips. */ static let SlowBootThresholdInterval: TimeInterval = 30.0 + /** + The interval between automatic background update checks. + */ + static let AutomaticUpdateCheckInterval: TimeInterval = 60 // 60.0 * 60 * 24 // 24 hours + + /** + The minimum interval that must pass before allowing another + automatic update check. This prevents excessive checking + on frequent app restarts (due to crashes or bad config). + */ + static let MinimumUpdateCheckInterval: TimeInterval = 60 // 60.0 * 60 // 60 minutes + /** PHP Monitor supplies a hardcoded list of PHP packages in its own PHP Version Manager. diff --git a/phpmon/Domain/Integrations/Analytics/LoggableEvent.swift b/phpmon/Domain/Integrations/Analytics/LoggableEvent.swift index 4439f6d..373105a 100644 --- a/phpmon/Domain/Integrations/Analytics/LoggableEvent.swift +++ b/phpmon/Domain/Integrations/Analytics/LoggableEvent.swift @@ -6,6 +6,12 @@ // Copyright © 2025 Nico Verbruggen. All rights reserved. // +// TODO: Add anonymous analytics system +// Batch events and dispatch them every hour. +// Reset the counts when send successfully. +// That's the plan. Currently not implemented! +// Also, there should be an opt-out. + enum LoggableEvent: String { case menuOpened = "menu_opened" @@ -17,9 +23,5 @@ enum LoggableEvent: String { case openedSettings = "opened_settings" - // TODO: Add one for each feature and make sure each feature used actually increments a count somewhere - // Ensure that the events are broadcast within 24 hrs since launch OR when the app quits - // If the events are broadcast after 24 hrs of the app being running, reset analytics - // Alternatively, batch events and dispatch them every hour (and keep track of what was sent) - // I will think about this some more, these are just ideas for now + // TODO: Add more tracked things. } diff --git a/phpmon/Domain/Menu/MainMenu+Startup.swift b/phpmon/Domain/Menu/MainMenu+Startup.swift index 314e628..3e12f50 100644 --- a/phpmon/Domain/Menu/MainMenu+Startup.swift +++ b/phpmon/Domain/Menu/MainMenu+Startup.swift @@ -142,7 +142,7 @@ extension MainMenu { } } else { // Check for updates - await AppUpdater().checkForUpdates(userInitiated: false) + await performAutomaticUpdateCheck() // Check if the linked version has changed between launches of phpmon await PhpGuard().compareToLastGlobalVersion() @@ -205,4 +205,60 @@ extension MainMenu { Log.info("Detected applications: \(appNames)") } + + /** + Perform an automatic update check and schedule the next one. + */ + private func performAutomaticUpdateCheck() async { + guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else { + // The user has chosen not to receive update notifications + return + } + + guard automaticUpdateCheckIsNotThrottled() else { + // If we are throttled, just schedule a regular check 24 hours from now + scheduleUpdateCheckTimer() + return + } + + await AppUpdater().checkForUpdates(userInitiated: false) + + UserDefaults.standard.set(Date(), forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue) + + scheduleUpdateCheckTimer() + } + + /** + Determine whether another automatic update check should occur based on the last check timestamp. + Returns true if a check should happen, false otherwise. + */ + private func automaticUpdateCheckIsNotThrottled() -> Bool { + guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else { + return false + } + + let minimumTimeAgo = Date().addingTimeInterval(-Constants.MinimumUpdateCheckInterval) + let lastCheckTime = UserDefaults.standard.object( + forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue + ) as? Date + + // If no previous check or last check was > minimum time frame, should check now + return lastCheckTime == nil || lastCheckTime! < minimumTimeAgo + } + + /** + Schedule a timer to perform an update check after the specified interval. + */ + private func scheduleUpdateCheckTimer() { + let interval = Constants.AutomaticUpdateCheckInterval + + Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in + Task { + Log.info("Performing scheduled update check after \(interval) seconds.") + await self.performAutomaticUpdateCheck() + } + } + + Log.info("A new update check will occur in \(interval) seconds from now.") + } } diff --git a/phpmon/Domain/Preferences/PreferenceName.swift b/phpmon/Domain/Preferences/PreferenceName.swift index b18f678..ef0a043 100644 --- a/phpmon/Domain/Preferences/PreferenceName.swift +++ b/phpmon/Domain/Preferences/PreferenceName.swift @@ -10,9 +10,6 @@ These are the keys used for every preference in the app. */ enum PreferenceName: String, Codable { - // FIRST-TIME LAUNCH - case wasLaunchedBefore = "launched_before" - // GENERAL case autoServiceRestartAfterExtensionToggle = "auto_restart_after_extension_toggle" case autoComposerGlobalUpdateAfterSwitch = "auto_composer_global_update_after_switch" @@ -104,6 +101,17 @@ enum RetiredPreferenceName: String { case shouldDisplayPhpHintInIcon = "add_php_to_icon" } +/** + Persistent internal application state keys for UserDefaults. + These track internal app state and behavior that persists across launches, + but are not user preferences or statistics. + */ +enum PersistentAppState: String { + case wasLaunchedBefore = "launched_before" + case lastAutomaticUpdateCheck = "last_automatic_update_check" + case userFavorites = "user_favorites" +} + /** These are internal stats. They NEVER get shared. */ diff --git a/phpmon/Domain/Preferences/Preferences.swift b/phpmon/Domain/Preferences/Preferences.swift index 63b0707..d4099ce 100644 --- a/phpmon/Domain/Preferences/Preferences.swift +++ b/phpmon/Domain/Preferences/Preferences.swift @@ -83,6 +83,9 @@ class Preferences { PreferenceName.displayPresets.rawValue: true, PreferenceName.displayMisc.rawValue: true, + /// Persistent App State + PersistentAppState.lastAutomaticUpdateCheck.rawValue: 0, + /// Stats InternalStats.switchCount.rawValue: 0, InternalStats.launchCount.rawValue: 0, @@ -90,13 +93,13 @@ class Preferences { InternalStats.lastGlobalPhpVersion.rawValue: "" ]) - if UserDefaults.standard.bool(forKey: PreferenceName.wasLaunchedBefore.rawValue) { + if UserDefaults.standard.bool(forKey: PersistentAppState.wasLaunchedBefore.rawValue) { handleMigration() return } Log.info("Saving first-time preferences!") - UserDefaults.standard.setValue(true, forKey: PreferenceName.wasLaunchedBefore.rawValue) + UserDefaults.standard.setValue(true, forKey: PersistentAppState.wasLaunchedBefore.rawValue) UserDefaults.standard.synchronize() } diff --git a/phpmon/Modules/Domain List/Favorites.swift b/phpmon/Modules/Domain List/Favorites.swift index 8a5bc59..d3d9653 100644 --- a/phpmon/Modules/Domain List/Favorites.swift +++ b/phpmon/Modules/Domain List/Favorites.swift @@ -14,7 +14,7 @@ class Favorites { var items: [String] init() { - if let items = UserDefaults.standard.array(forKey: "user_favorites") as? [String] { + if let items = UserDefaults.standard.array(forKey: PersistentAppState.userFavorites.rawValue) as? [String] { self.items = items } else { self.items = [] @@ -32,7 +32,7 @@ class Favorites { items.append(domain) } - UserDefaults.standard.setValue(items, forKey: "user_favorites") + UserDefaults.standard.setValue(items, forKey: PersistentAppState.userFavorites.rawValue) UserDefaults.standard.synchronize() } } From 13013f2513c1564211bb741b8de7a8df1df9b43c Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Fri, 26 Sep 2025 17:00:43 +0200 Subject: [PATCH 15/33] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Lint=20tests,=20add?= =?UTF-8?q?=20background=20update=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .swiftlint.yml | 16 ++- PHP Monitor.xcodeproj/project.pbxproj | 10 ++ phpmon/Common/Core/Constants.swift | 15 ++- phpmon/Domain/App/AppUpdater.swift | 19 ++- phpmon/Domain/App/UpdateScheduler.swift | 113 ++++++++++++++++ phpmon/Domain/Menu/MainMenu+Startup.swift | 58 +------- .../Domain/Preferences/PreferenceName.swift | 1 + .../Domain/Preferences/PreferencesTabs.swift | 2 - .../Services/AutomaticUpdateService.swift | 126 ++++++++++++++++++ tests/Shared/TestableConfigurations.swift | 8 +- tests/feature/FeatureTestCase.swift | 4 +- tests/ui/DomainsListTest.swift | 2 +- tests/ui/StartupTest.swift | 2 +- .../Parsers/ExtensionEnumeratorTest.swift | 3 +- tests/unit/Parsers/ValetRcTest.swift | 1 - .../Filesystem/RealFileSystemTest.swift | 2 - .../Testables/Shell/TestableShellTest.swift | 2 +- .../Testables/TestableConfigurationTest.swift | 3 +- .../unit/Versions/PhpVersionNumberTest.swift | 4 +- tests/unit/_ST/Commands/CommandTest.swift | 2 +- .../_ST/Parsers/ValetConfigurationTest.swift | 2 +- 21 files changed, 309 insertions(+), 86 deletions(-) create mode 100644 phpmon/Domain/App/UpdateScheduler.swift create mode 100644 phpmon/Domain/Services/AutomaticUpdateService.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 85b7f3c..719c6af 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -3,13 +3,25 @@ disabled_rules: - identifier_name - force_try - force_cast - + - private_over_fileprivate + opt_in_rules: - empty_count included: - phpmon - - phpmon-tests + - phpmon-updater + - tests excluded: - phpmon/Vendor + +line_length: + ignores_function_declarations: true + ignores_comments: true + ignores_urls: true + warning: 120 + error: 200 + +analyzer_rules: + - unused_import diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index a2c6d25..4911492 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -12,6 +12,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 */; }; + 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 */; }; + 03263A3B2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; }; 033D45982B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; }; 033D45992B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; }; 033D459A2B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; }; @@ -957,6 +961,7 @@ /* Begin PBXFileReference section */ 0309E6662B0D4B2F002AC007 /* BrewExtensionsObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewExtensionsObservable.swift; sourceTree = ""; }; 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewPhpExtension.swift; sourceTree = ""; }; + 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateScheduler.swift; sourceTree = ""; }; 0336CAAF2B0D0CDA009A1034 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallPhpExtensionCommand.swift; sourceTree = ""; }; 033D459D2B0D513900070080 /* RemovePhpExtensionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemovePhpExtensionCommand.swift; sourceTree = ""; }; @@ -2058,6 +2063,7 @@ C495F5AE28A42E080087F70A /* EnvironmentCheck.swift */, C40FE736282ABA4F00A302C2 /* AppVersion.swift */, C409349C298EE8E900D25014 /* AppUpdater.swift */, + 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */, ); path = App; sourceTree = ""; @@ -2679,6 +2685,7 @@ C48DDD0D29C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */, C45B91532956123A00F4EC78 /* FakeServicesManager.swift in Sources */, C41C708D28AA7F7900E8D498 /* NoWarningsView.swift in Sources */, + 03263A382E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */, 0309E6672B0D4B2F002AC007 /* BrewExtensionsObservable.swift in Sources */, C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */, C4205A7E27F4D21800191A39 /* ValetProxy.swift in Sources */, @@ -3006,6 +3013,7 @@ C471E81A28F9BAE80021E251 /* TimeIntervalExtension.swift in Sources */, C471E7E128F9BAAB0021E251 /* RealCommand.swift in Sources */, C471E7E228F9BAAB0021E251 /* ActiveCommand.swift in Sources */, + 03263A392E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */, C471E80A28F9BADC0021E251 /* CreatedFromFile.swift in Sources */, C471E80528F9BAD40021E251 /* ActivePhpInstallation.swift in Sources */, C471E80628F9BAD40021E251 /* PhpInstallation.swift in Sources */, @@ -3286,6 +3294,7 @@ C471E80F28F9BAE80021E251 /* NSMenuExtension.swift in Sources */, C471E80B28F9BAE80021E251 /* XibLoadable.swift in Sources */, C471E7F428F9BAC80021E251 /* VersionNumber.swift in Sources */, + 03263A3B2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */, C471E7CB28F9BA5B0021E251 /* TestableCommand.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3344,6 +3353,7 @@ 54D9E0B527E4F51E003B9AD9 /* Key.swift in Sources */, C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */, C4C1019C27C65C6F001FACC2 /* Process.swift in Sources */, + 03263A3A2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */, C451AFF72969E40F0078E617 /* HelpButton.swift in Sources */, C47DF1B0299D5A3B0007055D /* LoginItemManager.swift in Sources */, C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */, diff --git a/phpmon/Common/Core/Constants.swift b/phpmon/Common/Core/Constants.swift index b984224..ead208c 100644 --- a/phpmon/Common/Core/Constants.swift +++ b/phpmon/Common/Core/Constants.swift @@ -31,14 +31,25 @@ struct Constants { /** The interval between automatic background update checks. */ - static let AutomaticUpdateCheckInterval: TimeInterval = 60 // 60.0 * 60 * 24 // 24 hours + static let AutomaticUpdateCheckInterval: TimeInterval = 15.0 // 60.0 * 60 * 24 // 24 hours /** The minimum interval that must pass before allowing another automatic update check. This prevents excessive checking on frequent app restarts (due to crashes or bad config). */ - static let MinimumUpdateCheckInterval: TimeInterval = 60 // 60.0 * 60 // 60 minutes + static let MinimumUpdateCheckInterval: TimeInterval = 5.0 // 60.0 * 60 // 60 minutes + + /** + Retry intervals for failed automatic update checks. + Uses exponential backoff: 5 min → 15 min → 1 hr → 3 hrs before falling back to normal schedule. + */ + static let UpdateCheckRetryIntervals: [TimeInterval] = [ + 300, // 5 minutes + 900, // 15 minutes + 3600, // 1 hour + 10800 // 3 hours (final attempt) + ] /** PHP Monitor supplies a hardcoded list of PHP packages in its own diff --git a/phpmon/Domain/App/AppUpdater.swift b/phpmon/Domain/App/AppUpdater.swift index 1ae8a5e..30afdda 100644 --- a/phpmon/Domain/App/AppUpdater.swift +++ b/phpmon/Domain/App/AppUpdater.swift @@ -10,17 +10,24 @@ import Foundation import Cocoa import NVAlert +enum UpdateCheckResult { + case success + case networkError + case parseError + case disabled +} + class AppUpdater { var caskFile: CaskFile! var latestVersionOnline: AppVersion! var interactive: Bool = false - public func checkForUpdates(userInitiated: Bool) async { + public func checkForUpdates(userInitiated: Bool) async -> UpdateCheckResult { self.interactive = userInitiated if !interactive && !Preferences.isEnabled(.automaticBackgroundUpdateCheck) { Log.info("Skipping automatic update check due to user preference.") - return + return .disabled } Log.info("The app will search for updates...") @@ -29,7 +36,8 @@ class AppUpdater { guard let caskFile = await CaskFile.from(url: caskUrl) else { Log.err("The contents of the CaskFile at '\(caskUrl.absoluteString)' could not be retrieved.") - return presentCouldNotRetrieveUpdateIfInteractive() + presentCouldNotRetrieveUpdateIfInteractive() + return .networkError } self.caskFile = caskFile @@ -38,7 +46,8 @@ class AppUpdater { guard let onlineVersion = AppVersion.from(caskFile.version) else { Log.err("The version string from the CaskFile could not be read.") - return presentCouldNotRetrieveUpdateIfInteractive() + presentCouldNotRetrieveUpdateIfInteractive() + return .parseError } latestVersionOnline = onlineVersion @@ -49,6 +58,8 @@ class AppUpdater { } else if interactive { presentNoNewerVersionAvailableAlert() } + + return .success } private func presentCouldNotRetrieveUpdateIfInteractive() { diff --git a/phpmon/Domain/App/UpdateScheduler.swift b/phpmon/Domain/App/UpdateScheduler.swift new file mode 100644 index 0000000..961d087 --- /dev/null +++ b/phpmon/Domain/App/UpdateScheduler.swift @@ -0,0 +1,113 @@ +// +// UpdateScheduler.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 26/09/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +import Foundation + +@MainActor +class UpdateScheduler { + static let shared = UpdateScheduler() + + private init() {} + + /** + Start the automatic update checking process. + This should be called once during app startup. + */ + func startAutomaticUpdateChecking() async { + await performUpdateCheck() + } + + /** + Perform an automatic update check and schedule the next one. + */ + private func performUpdateCheck() async { + guard isNotThrottled() else { + // If we are throttled, just schedule a regular check the regular time from now! + scheduleTimer() + return + } + + // This check will be aborted if the preference disallows it in AppUpdater + let result = await AppUpdater().checkForUpdates(userInitiated: false) + + switch result { + case .success: + // Reset failure count and record successful check + UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue) + UserDefaults.standard.set(Date(), forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue) + scheduleTimer() + Log.info("Update check completed successfully. Next check scheduled in \(Constants.AutomaticUpdateCheckInterval) seconds.") + + case .disabled: + // User disabled automatic checks, don't schedule another + Log.info("Automatic update checks are disabled. No further checks will be scheduled.") + + case .networkError, .parseError: + // Handle failures with exponential backoff + handleFailure(result: result) + } + } + + /** + Handle update check failures with exponential backoff retry logic. + */ + private func handleFailure(result: UpdateCheckResult) { + let currentFailureCount = UserDefaults.standard.integer( + forKey: PersistentAppState.updateCheckFailureCount.rawValue + ) + let newFailureCount = currentFailureCount + 1 + + UserDefaults.standard.set(newFailureCount, forKey: PersistentAppState.updateCheckFailureCount.rawValue) + + let retryInterval: TimeInterval + if newFailureCount <= Constants.UpdateCheckRetryIntervals.count { + // Use exponential backoff + retryInterval = Constants.UpdateCheckRetryIntervals[newFailureCount - 1] + Log.info("Update check failed (\(result)). Retry attempt \(newFailureCount) scheduled in \(retryInterval) seconds.") + } else { + // Exceeded max retries, fall back to normal schedule and reset counter + retryInterval = Constants.AutomaticUpdateCheckInterval + UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue) + Log.info("Update check failed (\(result)). Max retries exceeded. Falling back to normal schedule in \(retryInterval) seconds.") + } + + scheduleTimer(after: retryInterval) + } + + /** + Determine whether another automatic update check should occur based on the last check timestamp. + Returns true if a check should happen, false otherwise. + */ + private func isNotThrottled() -> Bool { + guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else { + return false + } + + let minimumTimeAgo = Date().addingTimeInterval(-Constants.MinimumUpdateCheckInterval) + let lastCheckTime = UserDefaults.standard.object( + forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue + ) as? Date + + // If no previous check or last check was > minimum time frame, should check now + return lastCheckTime == nil || lastCheckTime! < minimumTimeAgo + } + + /** + Schedule a timer to perform an update check after the specified interval. + */ + private func scheduleTimer(after interval: TimeInterval = Constants.AutomaticUpdateCheckInterval) { + Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in + Task { + Log.info("Performing scheduled update check after \(interval) seconds.") + await self.performUpdateCheck() + } + } + + Log.info("A new update check will occur in \(interval) seconds from now.") + } +} diff --git a/phpmon/Domain/Menu/MainMenu+Startup.swift b/phpmon/Domain/Menu/MainMenu+Startup.swift index 3e12f50..6641650 100644 --- a/phpmon/Domain/Menu/MainMenu+Startup.swift +++ b/phpmon/Domain/Menu/MainMenu+Startup.swift @@ -142,7 +142,7 @@ extension MainMenu { } } else { // Check for updates - await performAutomaticUpdateCheck() + await UpdateScheduler.shared.startAutomaticUpdateChecking() // Check if the linked version has changed between launches of phpmon await PhpGuard().compareToLastGlobalVersion() @@ -205,60 +205,4 @@ extension MainMenu { Log.info("Detected applications: \(appNames)") } - - /** - Perform an automatic update check and schedule the next one. - */ - private func performAutomaticUpdateCheck() async { - guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else { - // The user has chosen not to receive update notifications - return - } - - guard automaticUpdateCheckIsNotThrottled() else { - // If we are throttled, just schedule a regular check 24 hours from now - scheduleUpdateCheckTimer() - return - } - - await AppUpdater().checkForUpdates(userInitiated: false) - - UserDefaults.standard.set(Date(), forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue) - - scheduleUpdateCheckTimer() - } - - /** - Determine whether another automatic update check should occur based on the last check timestamp. - Returns true if a check should happen, false otherwise. - */ - private func automaticUpdateCheckIsNotThrottled() -> Bool { - guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else { - return false - } - - let minimumTimeAgo = Date().addingTimeInterval(-Constants.MinimumUpdateCheckInterval) - let lastCheckTime = UserDefaults.standard.object( - forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue - ) as? Date - - // If no previous check or last check was > minimum time frame, should check now - return lastCheckTime == nil || lastCheckTime! < minimumTimeAgo - } - - /** - Schedule a timer to perform an update check after the specified interval. - */ - private func scheduleUpdateCheckTimer() { - let interval = Constants.AutomaticUpdateCheckInterval - - Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in - Task { - Log.info("Performing scheduled update check after \(interval) seconds.") - await self.performAutomaticUpdateCheck() - } - } - - Log.info("A new update check will occur in \(interval) seconds from now.") - } } diff --git a/phpmon/Domain/Preferences/PreferenceName.swift b/phpmon/Domain/Preferences/PreferenceName.swift index ef0a043..9689004 100644 --- a/phpmon/Domain/Preferences/PreferenceName.swift +++ b/phpmon/Domain/Preferences/PreferenceName.swift @@ -110,6 +110,7 @@ enum PersistentAppState: String { case wasLaunchedBefore = "launched_before" case lastAutomaticUpdateCheck = "last_automatic_update_check" case userFavorites = "user_favorites" + case updateCheckFailureCount = "update_check_failure_count" } /** diff --git a/phpmon/Domain/Preferences/PreferencesTabs.swift b/phpmon/Domain/Preferences/PreferencesTabs.swift index 4b18952..affc379 100644 --- a/phpmon/Domain/Preferences/PreferencesTabs.swift +++ b/phpmon/Domain/Preferences/PreferencesTabs.swift @@ -50,7 +50,6 @@ class AppearancePreferencesVC: GenericPreferenceVC { class MenuStructurePreferencesVC: GenericPreferenceVC { - // swiftlint:disable line_length public static func fromStoryboard() -> GenericPreferenceVC { let vc = NSStoryboard(name: "Main", bundle: nil) .instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC @@ -67,7 +66,6 @@ class MenuStructurePreferencesVC: GenericPreferenceVC { .addView(when: true, vc.displayFeature("prefs.display_misc", .displayMisc)) .addView(when: true, vc.displayFeature("prefs.display_driver", .displayDriver)) } - // swiftlint:enable line_length } class NotificationPreferencesVC: GenericPreferenceVC { diff --git a/phpmon/Domain/Services/AutomaticUpdateService.swift b/phpmon/Domain/Services/AutomaticUpdateService.swift new file mode 100644 index 0000000..2f71e9d --- /dev/null +++ b/phpmon/Domain/Services/AutomaticUpdateService.swift @@ -0,0 +1,126 @@ +// +// AutomaticUpdateService.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 26/09/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +import Foundation + +actor UpdateScheduler { + static let shared = UpdateScheduler() + + private var currentTimer: Timer? + + private init() {} + + /** + Start the automatic update checking process. This should be called once during app startup. + */ + func startAutomaticUpdateChecking() async { + await performUpdateCheck() + } + + /** + Perform an automatic update check and schedule the next one. + */ + private func performUpdateCheck() async { + guard isNotThrottled() else { + // If we are throttled, just schedule a regular check 24 hours from now. + scheduleTimer() + return + } + + let result = await AppUpdater().checkForUpdates(userInitiated: false) + + switch result { + case .success: + // Reset failure count and record successful check + UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue) + UserDefaults.standard.set(Date(), forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue) + scheduleTimer() + Log.info("Update check succeeded. Next check in \(Constants.AutomaticUpdateCheckInterval)s.") + + case .disabled: + // User disabled automatic checks, don't schedule another + Log.info("Automatic update checks disabled. No further checks scheduled.") + + case .networkError, .parseError: + // Handle failures with exponential backoff + handleFailure(result: result) + } + } + + /** + Handle update check failures with exponential backoff retry logic. + */ + private func handleFailure(result: UpdateCheckResult) { + let currentFailureCount = UserDefaults.standard.integer(forKey: PersistentAppState.updateCheckFailureCount.rawValue) + let newFailureCount = currentFailureCount + 1 + + UserDefaults.standard.set(newFailureCount, forKey: PersistentAppState.updateCheckFailureCount.rawValue) + + let retryInterval: TimeInterval + if newFailureCount <= Constants.UpdateCheckRetryIntervals.count { + // Use exponential backoff + retryInterval = Constants.UpdateCheckRetryIntervals[newFailureCount - 1] + Log.info("Update check failed (\(result)). Retry \(newFailureCount) in \(retryInterval)s.") + } else { + // Exceeded max retries, fall back to normal schedule and reset counter + retryInterval = Constants.AutomaticUpdateCheckInterval + UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue) + Log.info("Update check failed (\(result)). Max retries exceeded. Normal schedule in \(retryInterval)s.") + } + + scheduleTimer(after: retryInterval) + } + + /** + Determine whether another automatic update check should occur based on the last check timestamp. + Returns true if a check should happen, false otherwise. + */ + private func isNotThrottled() -> Bool { + guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else { + return false + } + + let minimumTimeAgo = Date().addingTimeInterval(-Constants.MinimumUpdateCheckInterval) + let lastCheckTime = UserDefaults.standard.object( + forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue + ) as? Date + + // If no previous check or last check was > minimum time frame, should check now + return lastCheckTime == nil || lastCheckTime! < minimumTimeAgo + } + + /** + Schedule a timer to perform an update check after the specified interval. + */ + private func scheduleTimer(after interval: TimeInterval = Constants.AutomaticUpdateCheckInterval) { + // Invalidate any existing timer + currentTimer?.invalidate() + + // Ensure timer is scheduled on main run loop since actors run on background threads + Task { @MainActor in + let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in + Task { + Log.info("Performing scheduled update check after \(interval)s.") + await self.performUpdateCheck() + } + } + + // Store timer reference back in actor + await self.setCurrentTimer(timer) + } + + Log.info("Next update check scheduled in \(interval)s.") + } + + /** + Set the current timer reference. Used to store timer from main thread back to actor. + */ + private func setCurrentTimer(_ timer: Timer) { + currentTimer = timer + } +} diff --git a/tests/Shared/TestableConfigurations.swift b/tests/Shared/TestableConfigurations.swift index 9f6c2de..4b5bdcf 100644 --- a/tests/Shared/TestableConfigurations.swift +++ b/tests/Shared/TestableConfigurations.swift @@ -8,6 +8,7 @@ import Foundation +// swiftlint:disable colon class TestableConfigurations { /** A functional, working system setup that is compatible with PHP Monitor. */ static var working: TestableConfiguration { @@ -179,7 +180,9 @@ class TestableConfigurations { : .delayed(0.2, "OK"), "ln -sF ~/.config/valet/valet84.sock ~/.config/valet/valet.sock" : .instant("OK"), - "/opt/homebrew/bin/brew update >/dev/null && /opt/homebrew/bin/brew outdated --json --formulae": .delayed(2.0, """ + "/opt/homebrew/bin/brew update >/dev/null && /opt/homebrew/bin/brew outdated --json --formulae" + : .delayed(2.0, + """ { "formulae": [ { @@ -199,7 +202,7 @@ class TestableConfigurations { commandOutput: [ "/opt/homebrew/bin/php -r echo ini_get('memory_limit');": "512M", "/opt/homebrew/bin/php -r echo ini_get('upload_max_filesize');": "512M", - "/opt/homebrew/bin/php -r echo ini_get('post_max_size');": "512M", + "/opt/homebrew/bin/php -r echo ini_get('post_max_size');": "512M" ], preferenceOverrides: [ .automaticBackgroundUpdateCheck: false @@ -223,6 +226,7 @@ class TestableConfigurations { return configuration } } +// swiftlint:enable colon class ShellStrings { static var shared = ShellStrings() diff --git a/tests/feature/FeatureTestCase.swift b/tests/feature/FeatureTestCase.swift index a7282af..0d09890 100644 --- a/tests/feature/FeatureTestCase.swift +++ b/tests/feature/FeatureTestCase.swift @@ -17,7 +17,7 @@ class FeatureTestCase: XCTestCase { return fs as! TestableFileSystem } - fatalError("The active filesystem is not a TestableFileSystem. Please use `ActiveFileSystem` to use the fake filesystem.") + fatalError("The active filesystem is not a TestableFileSystem. Please use `ActiveFileSystem`.") } public func assertFileSystemHas( @@ -44,6 +44,4 @@ class FeatureTestCase: XCTestCase { ) { XCTAssertEqual(contents, fakeFileSystem.files[path]?.content, file: file, line: line) } - } - diff --git a/tests/ui/DomainsListTest.swift b/tests/ui/DomainsListTest.swift index b1d79fe..2fe2610 100644 --- a/tests/ui/DomainsListTest.swift +++ b/tests/ui/DomainsListTest.swift @@ -35,7 +35,7 @@ final class DomainsListTest: UITestCase { searchField.click() searchField.typeText("non-existent thing") Thread.sleep(forTimeInterval: 0.2) - XCTAssertTrue(window.tables.tableRows.count == 0) + XCTAssertTrue(window.tables.tableRows.count == 0) // swiftlint:disable:this empty_count searchField.clearText() searchField.click() diff --git a/tests/ui/StartupTest.swift b/tests/ui/StartupTest.swift index 2fd4d30..2f4b3c2 100644 --- a/tests/ui/StartupTest.swift +++ b/tests/ui/StartupTest.swift @@ -26,7 +26,7 @@ final class StartupTest: UITestCase { assertAllExist([ app.dialogs["generic.notice".localized], app.staticTexts["startup.errors.php_binary.title".localized], - app.buttons["generic.ok".localized], + app.buttons["generic.ok".localized] ]) click(app.buttons["generic.ok".localized]) diff --git a/tests/unit/Parsers/ExtensionEnumeratorTest.swift b/tests/unit/Parsers/ExtensionEnumeratorTest.swift index 8ea8111..774d9a3 100644 --- a/tests/unit/Parsers/ExtensionEnumeratorTest.swift +++ b/tests/unit/Parsers/ExtensionEnumeratorTest.swift @@ -15,7 +15,7 @@ final class ExtensionEnumeratorTest: XCTestCase { "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.1.rb": .fake(.text, ""), "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.2.rb": .fake(.text, ""), "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.3.rb": .fake(.text, ""), - "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.4.rb": .fake(.text, ""), + "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.4.rb": .fake(.text, "") ]) } @@ -37,5 +37,4 @@ final class ExtensionEnumeratorTest: XCTestCase { XCTAssertEqual(formulae["8.3"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.3")]) XCTAssertEqual(formulae["8.4"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.4")]) } - } diff --git a/tests/unit/Parsers/ValetRcTest.swift b/tests/unit/Parsers/ValetRcTest.swift index a89c028..3a4428f 100644 --- a/tests/unit/Parsers/ValetRcTest.swift +++ b/tests/unit/Parsers/ValetRcTest.swift @@ -22,7 +22,6 @@ class ValetRcTest: XCTestCase { .url(forResource: "valetrc", withExtension: "broken")! } - // MARK: - Tests func test_can_extract_fields_from_valetrc_file() throws { diff --git a/tests/unit/Testables/Filesystem/RealFileSystemTest.swift b/tests/unit/Testables/Filesystem/RealFileSystemTest.swift index 8831dc2..b43dc0f 100644 --- a/tests/unit/Testables/Filesystem/RealFileSystemTest.swift +++ b/tests/unit/Testables/Filesystem/RealFileSystemTest.swift @@ -71,8 +71,6 @@ class RealFileSystemTest: XCTestCase { return executablePath } - - func test_can_read_file_as_text() { let temporaryDirectory = self.createUniqueTemporaryDirectory() let executable = self.createTestBinaryFile(temporaryDirectory) diff --git a/tests/unit/Testables/Shell/TestableShellTest.swift b/tests/unit/Testables/Shell/TestableShellTest.swift index 1df17af..d61ad15 100644 --- a/tests/unit/Testables/Shell/TestableShellTest.swift +++ b/tests/unit/Testables/Shell/TestableShellTest.swift @@ -30,7 +30,7 @@ class TestableShellTest: XCTestCase { XCTAssertEqual("Hello world\nGoodbye world", output.out) } - + func test_fake_shell_synchronous_output() { let greeting = BatchFakeShellOutput(items: [ .instant("Hello world\n"), diff --git a/tests/unit/Testables/TestableConfigurationTest.swift b/tests/unit/Testables/TestableConfigurationTest.swift index 511cde4..1550544 100644 --- a/tests/unit/Testables/TestableConfigurationTest.swift +++ b/tests/unit/Testables/TestableConfigurationTest.swift @@ -12,7 +12,7 @@ class TestableConfigurationTest: XCTestCase { func test_configuration_can_be_saved_as_json() async { // WORKING var configuration = TestableConfigurations.working - + try! configuration.toJson().write( toFile: NSHomeDirectory() + "/.phpmon_fconf_working.json", atomically: true, @@ -38,4 +38,3 @@ class TestableConfigurationTest: XCTestCase { ) } } - diff --git a/tests/unit/Versions/PhpVersionNumberTest.swift b/tests/unit/Versions/PhpVersionNumberTest.swift index d12f3dc..34b6893 100644 --- a/tests/unit/Versions/PhpVersionNumberTest.swift +++ b/tests/unit/Versions/PhpVersionNumberTest.swift @@ -8,7 +8,7 @@ import XCTest -// swiftlint:disable type_body_length +// swiftlint:disable type_body_length file_length class PhpVersionNumberTest: XCTestCase { func test_can_deconstruct_php_version() throws { @@ -51,7 +51,6 @@ class PhpVersionNumberTest: XCTestCase { XCTAssertEqual(version!.minor, 0) } - func test_can_check_wildcard_version_constraint() throws { // Wildcard for patch only XCTAssertEqual( @@ -408,3 +407,4 @@ class PhpVersionNumberTest: XCTestCase { ) } } +// swiftlint:enable type_body_length file_length diff --git a/tests/unit/_ST/Commands/CommandTest.swift b/tests/unit/_ST/Commands/CommandTest.swift index 90e6f2d..5b7feee 100644 --- a/tests/unit/_ST/Commands/CommandTest.swift +++ b/tests/unit/_ST/Commands/CommandTest.swift @@ -8,7 +8,7 @@ import Testing -@Suite("Commands") +@Suite("Commands") struct CommandTest { @Test diff --git a/tests/unit/_ST/Parsers/ValetConfigurationTest.swift b/tests/unit/_ST/Parsers/ValetConfigurationTest.swift index c4168e3..d251cc8 100644 --- a/tests/unit/_ST/Parsers/ValetConfigurationTest.swift +++ b/tests/unit/_ST/Parsers/ValetConfigurationTest.swift @@ -18,7 +18,7 @@ struct ValetConfigurationTest { )! } - @Test("Can load config file") + @Test("Can load config file") func can_load_config_file() throws { let json = try? String( contentsOf: Self.jsonConfigFileUrl, From b39e9a172bc1c784c327d79fa4dc3140ffb5309a Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Fri, 26 Sep 2025 17:03:18 +0200 Subject: [PATCH 16/33] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Use=20actor=20approa?= =?UTF-8?q?ch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpmon/Domain/App/UpdateScheduler.swift | 39 ++++++++++++++++++------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/phpmon/Domain/App/UpdateScheduler.swift b/phpmon/Domain/App/UpdateScheduler.swift index 961d087..934bb43 100644 --- a/phpmon/Domain/App/UpdateScheduler.swift +++ b/phpmon/Domain/App/UpdateScheduler.swift @@ -8,10 +8,11 @@ import Foundation -@MainActor -class UpdateScheduler { +actor UpdateScheduler { static let shared = UpdateScheduler() + private var currentTimer: Timer? + private init() {} /** @@ -41,11 +42,11 @@ class UpdateScheduler { UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue) UserDefaults.standard.set(Date(), forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue) scheduleTimer() - Log.info("Update check completed successfully. Next check scheduled in \(Constants.AutomaticUpdateCheckInterval) seconds.") + Log.info("Update check succeeded. Next check in \(Constants.AutomaticUpdateCheckInterval)s.") case .disabled: // User disabled automatic checks, don't schedule another - Log.info("Automatic update checks are disabled. No further checks will be scheduled.") + Log.info("Automatic update checks disabled. No further checks scheduled.") case .networkError, .parseError: // Handle failures with exponential backoff @@ -68,12 +69,12 @@ class UpdateScheduler { if newFailureCount <= Constants.UpdateCheckRetryIntervals.count { // Use exponential backoff retryInterval = Constants.UpdateCheckRetryIntervals[newFailureCount - 1] - Log.info("Update check failed (\(result)). Retry attempt \(newFailureCount) scheduled in \(retryInterval) seconds.") + Log.info("Update check failed (\(result)). Retry \(newFailureCount) in \(retryInterval)s.") } else { // Exceeded max retries, fall back to normal schedule and reset counter retryInterval = Constants.AutomaticUpdateCheckInterval UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue) - Log.info("Update check failed (\(result)). Max retries exceeded. Falling back to normal schedule in \(retryInterval) seconds.") + Log.info("Update check failed (\(result)). Max retries exceeded. Normal schedule in \(retryInterval)s.") } scheduleTimer(after: retryInterval) @@ -101,13 +102,29 @@ class UpdateScheduler { Schedule a timer to perform an update check after the specified interval. */ private func scheduleTimer(after interval: TimeInterval = Constants.AutomaticUpdateCheckInterval) { - Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in - Task { - Log.info("Performing scheduled update check after \(interval) seconds.") - await self.performUpdateCheck() + // Invalidate any existing timer + currentTimer?.invalidate() + + // Ensure timer is scheduled on main run loop since actors run on background threads + Task { @MainActor in + let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in + Task { + Log.info("Performing scheduled update check after \(interval)s.") + await self.performUpdateCheck() + } } + + // Store timer reference back in actor + await self.setCurrentTimer(timer) } - Log.info("A new update check will occur in \(interval) seconds from now.") + Log.info("Next update check scheduled in \(interval)s.") + } + + /** + Set the current timer reference. Used to store timer from main thread back to actor. + */ + private func setCurrentTimer(_ timer: Timer) { + currentTimer = timer } } From 8366a2127d539c24dc5aadec86607d991bc93508 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Fri, 26 Sep 2025 17:11:10 +0200 Subject: [PATCH 17/33] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Further=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpmon/Common/Core/Constants.swift | 14 +- phpmon/Domain/App/AppUpdater.swift | 6 - phpmon/Domain/App/UpdateScheduler.swift | 19 ++- phpmon/Domain/Preferences/Preferences.swift | 1 + .../Services/AutomaticUpdateService.swift | 126 ------------------ 5 files changed, 16 insertions(+), 150 deletions(-) delete mode 100644 phpmon/Domain/Services/AutomaticUpdateService.swift diff --git a/phpmon/Common/Core/Constants.swift b/phpmon/Common/Core/Constants.swift index ead208c..430c2a9 100644 --- a/phpmon/Common/Core/Constants.swift +++ b/phpmon/Common/Core/Constants.swift @@ -31,24 +31,24 @@ struct Constants { /** The interval between automatic background update checks. */ - static let AutomaticUpdateCheckInterval: TimeInterval = 15.0 // 60.0 * 60 * 24 // 24 hours + static let AutomaticUpdateCheckInterval: TimeInterval = 60.0 * 60 * 24 // 24 hours /** The minimum interval that must pass before allowing another automatic update check. This prevents excessive checking on frequent app restarts (due to crashes or bad config). */ - static let MinimumUpdateCheckInterval: TimeInterval = 5.0 // 60.0 * 60 // 60 minutes + static let MinimumUpdateCheckInterval: TimeInterval = 60.0 * 60 // 60 minutes /** Retry intervals for failed automatic update checks. - Uses exponential backoff: 5 min → 15 min → 1 hr → 3 hrs before falling back to normal schedule. + Uses exponential backoff before falling back to normal schedule. */ static let UpdateCheckRetryIntervals: [TimeInterval] = [ - 300, // 5 minutes - 900, // 15 minutes - 3600, // 1 hour - 10800 // 3 hours (final attempt) + 60 * 5, // 5 minutes + 60 * 15, // 15 minutes + 60 * 60, // 1 hour + 60 * 60 * 3 // 3 hours (final attempt) ] /** diff --git a/phpmon/Domain/App/AppUpdater.swift b/phpmon/Domain/App/AppUpdater.swift index 30afdda..a5cecfa 100644 --- a/phpmon/Domain/App/AppUpdater.swift +++ b/phpmon/Domain/App/AppUpdater.swift @@ -14,7 +14,6 @@ enum UpdateCheckResult { case success case networkError case parseError - case disabled } class AppUpdater { @@ -25,11 +24,6 @@ class AppUpdater { public func checkForUpdates(userInitiated: Bool) async -> UpdateCheckResult { self.interactive = userInitiated - if !interactive && !Preferences.isEnabled(.automaticBackgroundUpdateCheck) { - Log.info("Skipping automatic update check due to user preference.") - return .disabled - } - Log.info("The app will search for updates...") let caskUrl = Constants.Urls.UpdateCheckEndpoint diff --git a/phpmon/Domain/App/UpdateScheduler.swift b/phpmon/Domain/App/UpdateScheduler.swift index 934bb43..11dbd18 100644 --- a/phpmon/Domain/App/UpdateScheduler.swift +++ b/phpmon/Domain/App/UpdateScheduler.swift @@ -27,13 +27,18 @@ actor UpdateScheduler { Perform an automatic update check and schedule the next one. */ private func performUpdateCheck() async { - guard isNotThrottled() else { - // If we are throttled, just schedule a regular check the regular time from now! + guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else { + Log.info("Automatic update checks disabled. Skipping check but maintaining schedule.") + scheduleTimer() + return + } + + guard isNotThrottled() else { + Log.info("Last check was too recent. Skipping check but maintaining schedule.") scheduleTimer() return } - // This check will be aborted if the preference disallows it in AppUpdater let result = await AppUpdater().checkForUpdates(userInitiated: false) switch result { @@ -44,10 +49,6 @@ actor UpdateScheduler { scheduleTimer() Log.info("Update check succeeded. Next check in \(Constants.AutomaticUpdateCheckInterval)s.") - case .disabled: - // User disabled automatic checks, don't schedule another - Log.info("Automatic update checks disabled. No further checks scheduled.") - case .networkError, .parseError: // Handle failures with exponential backoff handleFailure(result: result) @@ -85,10 +86,6 @@ actor UpdateScheduler { Returns true if a check should happen, false otherwise. */ private func isNotThrottled() -> Bool { - guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else { - return false - } - let minimumTimeAgo = Date().addingTimeInterval(-Constants.MinimumUpdateCheckInterval) let lastCheckTime = UserDefaults.standard.object( forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue diff --git a/phpmon/Domain/Preferences/Preferences.swift b/phpmon/Domain/Preferences/Preferences.swift index d4099ce..cc4c47c 100644 --- a/phpmon/Domain/Preferences/Preferences.swift +++ b/phpmon/Domain/Preferences/Preferences.swift @@ -85,6 +85,7 @@ class Preferences { /// Persistent App State PersistentAppState.lastAutomaticUpdateCheck.rawValue: 0, + PersistentAppState.updateCheckFailureCount.rawValue: 0, /// Stats InternalStats.switchCount.rawValue: 0, diff --git a/phpmon/Domain/Services/AutomaticUpdateService.swift b/phpmon/Domain/Services/AutomaticUpdateService.swift deleted file mode 100644 index 2f71e9d..0000000 --- a/phpmon/Domain/Services/AutomaticUpdateService.swift +++ /dev/null @@ -1,126 +0,0 @@ -// -// AutomaticUpdateService.swift -// PHP Monitor -// -// Created by Nico Verbruggen on 26/09/2025. -// Copyright © 2025 Nico Verbruggen. All rights reserved. -// - -import Foundation - -actor UpdateScheduler { - static let shared = UpdateScheduler() - - private var currentTimer: Timer? - - private init() {} - - /** - Start the automatic update checking process. This should be called once during app startup. - */ - func startAutomaticUpdateChecking() async { - await performUpdateCheck() - } - - /** - Perform an automatic update check and schedule the next one. - */ - private func performUpdateCheck() async { - guard isNotThrottled() else { - // If we are throttled, just schedule a regular check 24 hours from now. - scheduleTimer() - return - } - - let result = await AppUpdater().checkForUpdates(userInitiated: false) - - switch result { - case .success: - // Reset failure count and record successful check - UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue) - UserDefaults.standard.set(Date(), forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue) - scheduleTimer() - Log.info("Update check succeeded. Next check in \(Constants.AutomaticUpdateCheckInterval)s.") - - case .disabled: - // User disabled automatic checks, don't schedule another - Log.info("Automatic update checks disabled. No further checks scheduled.") - - case .networkError, .parseError: - // Handle failures with exponential backoff - handleFailure(result: result) - } - } - - /** - Handle update check failures with exponential backoff retry logic. - */ - private func handleFailure(result: UpdateCheckResult) { - let currentFailureCount = UserDefaults.standard.integer(forKey: PersistentAppState.updateCheckFailureCount.rawValue) - let newFailureCount = currentFailureCount + 1 - - UserDefaults.standard.set(newFailureCount, forKey: PersistentAppState.updateCheckFailureCount.rawValue) - - let retryInterval: TimeInterval - if newFailureCount <= Constants.UpdateCheckRetryIntervals.count { - // Use exponential backoff - retryInterval = Constants.UpdateCheckRetryIntervals[newFailureCount - 1] - Log.info("Update check failed (\(result)). Retry \(newFailureCount) in \(retryInterval)s.") - } else { - // Exceeded max retries, fall back to normal schedule and reset counter - retryInterval = Constants.AutomaticUpdateCheckInterval - UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue) - Log.info("Update check failed (\(result)). Max retries exceeded. Normal schedule in \(retryInterval)s.") - } - - scheduleTimer(after: retryInterval) - } - - /** - Determine whether another automatic update check should occur based on the last check timestamp. - Returns true if a check should happen, false otherwise. - */ - private func isNotThrottled() -> Bool { - guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else { - return false - } - - let minimumTimeAgo = Date().addingTimeInterval(-Constants.MinimumUpdateCheckInterval) - let lastCheckTime = UserDefaults.standard.object( - forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue - ) as? Date - - // If no previous check or last check was > minimum time frame, should check now - return lastCheckTime == nil || lastCheckTime! < minimumTimeAgo - } - - /** - Schedule a timer to perform an update check after the specified interval. - */ - private func scheduleTimer(after interval: TimeInterval = Constants.AutomaticUpdateCheckInterval) { - // Invalidate any existing timer - currentTimer?.invalidate() - - // Ensure timer is scheduled on main run loop since actors run on background threads - Task { @MainActor in - let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in - Task { - Log.info("Performing scheduled update check after \(interval)s.") - await self.performUpdateCheck() - } - } - - // Store timer reference back in actor - await self.setCurrentTimer(timer) - } - - Log.info("Next update check scheduled in \(interval)s.") - } - - /** - Set the current timer reference. Used to store timer from main thread back to actor. - */ - private func setCurrentTimer(_ timer: Timer) { - currentTimer = timer - } -} From aa9c50302889903bd05e6297d15a9effe99afad1 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Fri, 26 Sep 2025 17:23:27 +0200 Subject: [PATCH 18/33] =?UTF-8?q?=F0=9F=94=A7=20Bump=20build=20for=20EAP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 4911492..afe2ba4 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -3860,7 +3860,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1595; + CURRENT_PROJECT_VERSION = 1600; DEAD_CODE_STRIPPING = YES; DEBUG = YES; ENABLE_APP_SANDBOX = NO; @@ -3891,7 +3891,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1595; + CURRENT_PROJECT_VERSION = 1600; DEAD_CODE_STRIPPING = YES; DEBUG = NO; ENABLE_APP_SANDBOX = NO; @@ -4125,7 +4125,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1595; + CURRENT_PROJECT_VERSION = 1600; DEAD_CODE_STRIPPING = YES; DEBUG = NO; ENABLE_APP_SANDBOX = NO; @@ -4242,7 +4242,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1595; + CURRENT_PROJECT_VERSION = 1600; DEAD_CODE_STRIPPING = YES; DEBUG = YES; ENABLE_APP_SANDBOX = NO; @@ -4359,7 +4359,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1595; + CURRENT_PROJECT_VERSION = 1600; DEAD_CODE_STRIPPING = YES; DEBUG = YES; ENABLE_APP_SANDBOX = NO; @@ -4539,7 +4539,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1595; + CURRENT_PROJECT_VERSION = 1600; DEAD_CODE_STRIPPING = YES; DEBUG = NO; ENABLE_APP_SANDBOX = NO; From 21296d09aa4cdfb4b4db66fae89dc9394c4e8767 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Fri, 26 Sep 2025 20:25:49 +0200 Subject: [PATCH 19/33] =?UTF-8?q?=F0=9F=94=A7=20Get=20rid=20of=20DEV=20bui?= =?UTF-8?q?lds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 447 +++--------------- .../xcschemes/PHP Monitor DEV.xcscheme | 146 ------ phpmon/Common/Core/Paths.swift | 3 +- .../Common/Extensions/StringExtension.swift | 3 +- phpmon/Domain/App/AppUpdater.swift | 6 +- phpmon/Domain/Preferences/Stats.swift | 2 +- 6 files changed, 65 insertions(+), 542 deletions(-) delete mode 100644 PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor DEV.xcscheme diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index afe2ba4..ed9e564 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -3624,40 +3624,6 @@ }; name = Debug; }; - C406A5FE298AD2CF00B5B85A /* Debug.Dev */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIconUD; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CODE_SIGN_ENTITLEMENTS = "phpmon-updater/phpmon-updater.entitlements"; - CODE_SIGN_IDENTITY = "-"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 30; - DEAD_CODE_STRIPPING = YES; - ENABLE_HARDENED_RUNTIME = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_CFBundleDisplayName = "PHP Monitor Self-Updater"; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; - INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023-2025 Nico Verbruggen. All rights reserved."; - INFOPLIST_KEY_NSPrincipalClass = NSApplication; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 1.3; - PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-updater"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - }; - name = Debug.Dev; - }; C406A5FF298AD2CF00B5B85A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -3692,40 +3658,6 @@ }; name = Release; }; - C406A600298AD2CF00B5B85A /* Release.Dev */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIconUD; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CODE_SIGN_ENTITLEMENTS = "phpmon-updater/phpmon-updater.entitlements"; - CODE_SIGN_IDENTITY = "-"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 30; - DEAD_CODE_STRIPPING = YES; - ENABLE_HARDENED_RUNTIME = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_CFBundleDisplayName = "PHP Monitor Self-Updater"; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; - INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023-2025 Nico Verbruggen. All rights reserved."; - INFOPLIST_KEY_NSPrincipalClass = NSApplication; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 1.3; - PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-updater"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - }; - name = Release.Dev; - }; C41C1B4122B0098000E7CF16 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -3856,15 +3788,22 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppColor; + AUTOMATION_APPLE_EVENTS = NO; CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1600; + CURRENT_PROJECT_VERSION = 1601; DEAD_CODE_STRIPPING = YES; DEBUG = YES; ENABLE_APP_SANDBOX = NO; - ENABLE_HARDENED_RUNTIME = YES; + ENABLE_HARDENED_RUNTIME = NO; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; INFOPLIST_FILE = phpmon/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "PHP Monitor"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; @@ -3878,6 +3817,12 @@ PRODUCT_MODULE_NAME = PHP_Monitor; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; + RUNTIME_EXCEPTION_ALLOW_JIT = NO; + RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO; + RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO; + RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO; + RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO; SWIFT_VERSION = 5.0; }; name = Debug; @@ -3887,15 +3832,22 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppColor; + AUTOMATION_APPLE_EVENTS = NO; CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1600; + CURRENT_PROJECT_VERSION = 1601; DEAD_CODE_STRIPPING = YES; DEBUG = NO; ENABLE_APP_SANDBOX = NO; - ENABLE_HARDENED_RUNTIME = YES; + ENABLE_HARDENED_RUNTIME = NO; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; INFOPLIST_FILE = phpmon/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "PHP Monitor"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; @@ -3909,6 +3861,12 @@ PRODUCT_MODULE_NAME = PHP_Monitor; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; + RUNTIME_EXCEPTION_ALLOW_JIT = NO; + RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO; + RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO; + RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO; + RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO; SWIFT_VERSION = 5.0; }; name = Release; @@ -3931,24 +3889,6 @@ }; name = Debug; }; - C471E7B528F9B4940021E251 /* Debug.Dev */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEAD_CODE_STRIPPING = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = ""; - MACOSX_DEPLOYMENT_TARGET = 12.4; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.feature-tests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - }; - name = Debug.Dev; - }; C471E7B628F9B4940021E251 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -3967,24 +3907,6 @@ }; name = Release; }; - C471E7B728F9B4940021E251 /* Release.Dev */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEAD_CODE_STRIPPING = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = ""; - MACOSX_DEPLOYMENT_TARGET = 12.4; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.feature-tests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - }; - name = Release.Dev; - }; C471E7C528F9B90F0021E251 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -4003,24 +3925,6 @@ }; name = Debug; }; - C471E7C628F9B90F0021E251 /* Debug.Dev */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEAD_CODE_STRIPPING = YES; - GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.4; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.ui-tests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TEST_TARGET_NAME = "PHP Monitor"; - }; - name = Debug.Dev; - }; C471E7C728F9B90F0021E251 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -4039,251 +3943,6 @@ }; name = Release; }; - C471E7C828F9B90F0021E251 /* Release.Dev */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEAD_CODE_STRIPPING = YES; - GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.4; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.ui-tests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TEST_TARGET_NAME = "PHP Monitor"; - }; - name = Release.Dev; - }; - C4975D0728CD190C00FFB4E8 /* Release.Dev */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "Mac Developer"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = 8M54J5J787; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 13.5; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release.Dev; - }; - C4975D0828CD190C00FFB4E8 /* Release.Dev */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDev; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppColor; - CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1600; - DEAD_CODE_STRIPPING = YES; - DEBUG = NO; - ENABLE_APP_SANDBOX = NO; - ENABLE_HARDENED_RUNTIME = YES; - INFOPLIST_FILE = phpmon/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "PHP Monitor DEV"; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 25.09; - PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.dev; - PRODUCT_MODULE_NAME = PHP_Monitor; - PRODUCT_NAME = "$(TARGET_NAME) DEV"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release.Dev; - }; - C4975D0928CD190C00FFB4E8 /* Release.Dev */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEAD_CODE_STRIPPING = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = ""; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 12.4; - PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.unit-tests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - }; - name = Release.Dev; - }; - C4975D0A28CD193A00FFB4E8 /* Debug.Dev */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "Mac Developer"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 8M54J5J787; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 13.5; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug.Dev; - }; - C4975D0B28CD193A00FFB4E8 /* Debug.Dev */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDev; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppColor; - CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1600; - DEAD_CODE_STRIPPING = YES; - DEBUG = YES; - ENABLE_APP_SANDBOX = NO; - ENABLE_HARDENED_RUNTIME = YES; - INFOPLIST_FILE = phpmon/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "PHP Monitor DEV"; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 13.5; - MARKETING_VERSION = 25.09; - PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.dev; - PRODUCT_MODULE_NAME = PHP_Monitor; - PRODUCT_NAME = "$(TARGET_NAME) DEV"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Debug.Dev; - }; - C4975D0C28CD193A00FFB4E8 /* Debug.Dev */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEAD_CODE_STRIPPING = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = ""; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 12.4; - PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.unit-tests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - }; - name = Debug.Dev; - }; C4E9D8EF29CB9A6400BD28D4 /* Debug.EA */ = { isa = XCBuildConfiguration; buildSettings = { @@ -4355,15 +4014,22 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIconEAP; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppColor; + AUTOMATION_APPLE_EVENTS = NO; CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1600; + CURRENT_PROJECT_VERSION = 1601; DEAD_CODE_STRIPPING = YES; DEBUG = YES; ENABLE_APP_SANDBOX = NO; - ENABLE_HARDENED_RUNTIME = YES; + ENABLE_HARDENED_RUNTIME = NO; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; INFOPLIST_FILE = phpmon/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "PHP Monitor EAP"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; @@ -4377,6 +4043,12 @@ PRODUCT_MODULE_NAME = PHP_Monitor; PRODUCT_NAME = "$(TARGET_NAME) EAP"; PROVISIONING_PROFILE_SPECIFIER = ""; + RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; + RUNTIME_EXCEPTION_ALLOW_JIT = NO; + RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO; + RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO; + RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO; + RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO; SWIFT_VERSION = 5.0; }; name = Debug.EA; @@ -4535,15 +4207,22 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIconEAP; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppColor; + AUTOMATION_APPLE_EVENTS = NO; CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1600; + CURRENT_PROJECT_VERSION = 1601; DEAD_CODE_STRIPPING = YES; DEBUG = NO; ENABLE_APP_SANDBOX = NO; - ENABLE_HARDENED_RUNTIME = YES; + ENABLE_HARDENED_RUNTIME = NO; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; INFOPLIST_FILE = phpmon/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "PHP Monitor EAP"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; @@ -4557,6 +4236,12 @@ PRODUCT_MODULE_NAME = PHP_Monitor; PRODUCT_NAME = "$(TARGET_NAME) EAP"; PROVISIONING_PROFILE_SPECIFIER = ""; + RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; + RUNTIME_EXCEPTION_ALLOW_JIT = NO; + RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO; + RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO; + RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO; + RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO; SWIFT_VERSION = 5.0; }; name = Release.EA; @@ -4698,10 +4383,8 @@ isa = XCConfigurationList; buildConfigurations = ( C406A5FD298AD2CF00B5B85A /* Debug */, - C406A5FE298AD2CF00B5B85A /* Debug.Dev */, C4E9D8F129CB9A6400BD28D4 /* Debug.EA */, C406A5FF298AD2CF00B5B85A /* Release */, - C406A600298AD2CF00B5B85A /* Release.Dev */, C4E9D8F729CB9A7200BD28D4 /* Release.EA */, ); defaultConfigurationIsVisible = 0; @@ -4711,10 +4394,8 @@ isa = XCConfigurationList; buildConfigurations = ( C41C1B4122B0098000E7CF16 /* Debug */, - C4975D0A28CD193A00FFB4E8 /* Debug.Dev */, C4E9D8EF29CB9A6400BD28D4 /* Debug.EA */, C41C1B4222B0098000E7CF16 /* Release */, - C4975D0728CD190C00FFB4E8 /* Release.Dev */, C4E9D8F529CB9A7200BD28D4 /* Release.EA */, ); defaultConfigurationIsVisible = 0; @@ -4724,10 +4405,8 @@ isa = XCConfigurationList; buildConfigurations = ( C41C1B4422B0098000E7CF16 /* Debug */, - C4975D0B28CD193A00FFB4E8 /* Debug.Dev */, C4E9D8F029CB9A6400BD28D4 /* Debug.EA */, C41C1B4522B0098000E7CF16 /* Release */, - C4975D0828CD190C00FFB4E8 /* Release.Dev */, C4E9D8F629CB9A7200BD28D4 /* Release.EA */, ); defaultConfigurationIsVisible = 0; @@ -4737,10 +4416,8 @@ isa = XCConfigurationList; buildConfigurations = ( C471E7B428F9B4940021E251 /* Debug */, - C471E7B528F9B4940021E251 /* Debug.Dev */, C4E9D8F329CB9A6400BD28D4 /* Debug.EA */, C471E7B628F9B4940021E251 /* Release */, - C471E7B728F9B4940021E251 /* Release.Dev */, C4E9D8F929CB9A7200BD28D4 /* Release.EA */, ); defaultConfigurationIsVisible = 0; @@ -4750,10 +4427,8 @@ isa = XCConfigurationList; buildConfigurations = ( C471E7C528F9B90F0021E251 /* Debug */, - C471E7C628F9B90F0021E251 /* Debug.Dev */, C4E9D8F429CB9A6400BD28D4 /* Debug.EA */, C471E7C728F9B90F0021E251 /* Release */, - C471E7C828F9B90F0021E251 /* Release.Dev */, C4E9D8FA29CB9A7200BD28D4 /* Release.EA */, ); defaultConfigurationIsVisible = 0; @@ -4763,10 +4438,8 @@ isa = XCConfigurationList; buildConfigurations = ( C4F7808125D7F84B000DBC97 /* Debug */, - C4975D0C28CD193A00FFB4E8 /* Debug.Dev */, C4E9D8F229CB9A6400BD28D4 /* Debug.EA */, C4F7808225D7F84B000DBC97 /* Release */, - C4975D0928CD190C00FFB4E8 /* Release.Dev */, C4E9D8F829CB9A7200BD28D4 /* Release.EA */, ); defaultConfigurationIsVisible = 0; diff --git a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor DEV.xcscheme b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor DEV.xcscheme deleted file mode 100644 index 2591575..0000000 --- a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor DEV.xcscheme +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/phpmon/Common/Core/Paths.swift b/phpmon/Common/Core/Paths.swift index ca1f796..56d23a7 100644 --- a/phpmon/Common/Core/Paths.swift +++ b/phpmon/Common/Core/Paths.swift @@ -111,8 +111,7 @@ public class Paths { } public static var caskroomPath: String { - return "\(shared.baseDir.rawValue)/Caskroom/" - + (App.identifier.contains(".dev") ? "phpmon-dev" : "phpmon") + return "\(shared.baseDir.rawValue)/Caskroom/phpmon" } public static var shell: String { diff --git a/phpmon/Common/Extensions/StringExtension.swift b/phpmon/Common/Extensions/StringExtension.swift index 27b24e2..97cf696 100644 --- a/phpmon/Common/Extensions/StringExtension.swift +++ b/phpmon/Common/Extensions/StringExtension.swift @@ -25,8 +25,7 @@ struct Localization { return Bundle.main } - let foundBundle = Bundle(identifier: "com.nicoverbruggen.phpmon.dev") - ?? Bundle(identifier: "com.nicoverbruggen.phpmon") + let foundBundle = Bundle(identifier: "com.nicoverbruggen.phpmon") ?? Bundle(identifier: "com.nicoverbruggen.phpmon.ui-tests") if foundBundle == nil { diff --git a/phpmon/Domain/App/AppUpdater.swift b/phpmon/Domain/App/AppUpdater.swift index a5cecfa..f567500 100644 --- a/phpmon/Domain/App/AppUpdater.swift +++ b/phpmon/Domain/App/AppUpdater.swift @@ -67,9 +67,7 @@ class AppUpdater { // MARK: - Alerts public func presentNewerVersionAvailableAlert() { - let command = App.identifier.contains(".dev") - ? "brew upgrade phpmon-dev" - : "brew upgrade phpmon" + let command = "brew upgrade phpmon" Task { @MainActor in NVAlert().withInformation( @@ -187,7 +185,7 @@ class AppUpdater { // Cleanup the upgrade.success file if FileSystem.fileExists("~/.config/phpmon/updater/upgrade.success") { Task { @MainActor in - if App.identifier.contains(".phpmon.eap") || App.identifier.contains(".phpmon.dev") { + if App.identifier.contains(".phpmon.eap") { LocalNotification.send( title: "notification.phpmon_updated.title".localized, subtitle: "notification.phpmon_updated_dev.desc".localized(App.shortVersion, App.bundleVersion), diff --git a/phpmon/Domain/Preferences/Stats.swift b/phpmon/Domain/Preferences/Stats.swift index fb2d3a5..42342b5 100644 --- a/phpmon/Domain/Preferences/Stats.swift +++ b/phpmon/Domain/Preferences/Stats.swift @@ -110,7 +110,7 @@ class Stats { return Log.info("A fake shell is in use, skipping sponsor alert.") } - if App.identifier.contains(".dev") || App.identifier.contains(".eap") { + if App.identifier.contains(".eap") { return Log.info("Sponsor messages never apply to beta builds.") } From af13913d9e16017413c520034ff381c1217aea7d Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Fri, 26 Sep 2025 21:13:06 +0200 Subject: [PATCH 20/33] =?UTF-8?q?=E2=9C=85=20Improve=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 24 ++++++------- .../Testables/TestableConfiguration.swift | 6 ++++ phpmon/Domain/App/App.swift | 3 ++ .../Integrations/Homebrew/CaskFile.swift | 20 +++++++---- tests/Shared/TestableConfigurations.swift | 9 +---- tests/ui/UpdateCheckTest.swift | 36 ++++--------------- 6 files changed, 42 insertions(+), 56 deletions(-) diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index ed9e564..05a12a4 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -3880,7 +3880,7 @@ DEAD_CODE_STRIPPING = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ""; - MACOSX_DEPLOYMENT_TARGET = 12.4; + MACOSX_DEPLOYMENT_TARGET = 13.5; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.feature-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3898,7 +3898,7 @@ DEAD_CODE_STRIPPING = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ""; - MACOSX_DEPLOYMENT_TARGET = 12.4; + MACOSX_DEPLOYMENT_TARGET = 13.5; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.feature-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3915,7 +3915,7 @@ CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.4; + MACOSX_DEPLOYMENT_TARGET = 13.5; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.ui-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3933,7 +3933,7 @@ CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.4; + MACOSX_DEPLOYMENT_TARGET = 13.5; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.ui-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4100,7 +4100,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.4; + MACOSX_DEPLOYMENT_TARGET = 13.5; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.unit-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -4116,7 +4116,7 @@ DEAD_CODE_STRIPPING = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ""; - MACOSX_DEPLOYMENT_TARGET = 12.4; + MACOSX_DEPLOYMENT_TARGET = 13.5; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.feature-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4133,7 +4133,7 @@ CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.4; + MACOSX_DEPLOYMENT_TARGET = 13.5; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.ui-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4293,7 +4293,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.4; + MACOSX_DEPLOYMENT_TARGET = 13.5; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.unit-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -4309,7 +4309,7 @@ DEAD_CODE_STRIPPING = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ""; - MACOSX_DEPLOYMENT_TARGET = 12.4; + MACOSX_DEPLOYMENT_TARGET = 13.5; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.feature-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4326,7 +4326,7 @@ CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.4; + MACOSX_DEPLOYMENT_TARGET = 13.5; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.ui-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4349,7 +4349,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.4; + MACOSX_DEPLOYMENT_TARGET = 13.5; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.unit-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -4369,7 +4369,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.4; + MACOSX_DEPLOYMENT_TARGET = 13.5; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.unit-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; diff --git a/phpmon/Common/Testables/TestableConfiguration.swift b/phpmon/Common/Testables/TestableConfiguration.swift index b0ac035..5b0fb99 100644 --- a/phpmon/Common/Testables/TestableConfiguration.swift +++ b/phpmon/Common/Testables/TestableConfiguration.swift @@ -140,6 +140,12 @@ public struct TestableConfiguration: Codable { Log.info("Applying fake Valet domain interactor...") ValetInteractor.useFake() } + + // Clear volatile app state for tests + UserDefaults.standard.removeObject(forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue) + + // Set variable to tell app we're testin' + App.hasLoadedTestableConfiguration = true } // MARK: Persist and load diff --git a/phpmon/Domain/App/App.swift b/phpmon/Domain/App/App.swift index 038c435..52a8d99 100644 --- a/phpmon/Domain/App/App.swift +++ b/phpmon/Domain/App/App.swift @@ -14,6 +14,9 @@ class App { /** The static app instance. Accessible at any time. */ static let shared = App() + /** Use to determine whether a loaded testable configuration is being used. */ + static var hasLoadedTestableConfiguration: Bool = false + /** Retrieve the version number from the main info dictionary, Info.plist. */ static var version: String { let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String diff --git a/phpmon/Domain/Integrations/Homebrew/CaskFile.swift b/phpmon/Domain/Integrations/Homebrew/CaskFile.swift index 2bd5c9d..666bc00 100644 --- a/phpmon/Domain/Integrations/Homebrew/CaskFile.swift +++ b/phpmon/Domain/Integrations/Homebrew/CaskFile.swift @@ -24,13 +24,11 @@ struct CaskFile { return self.properties["version"]! } - public static func from(url: URL) async -> CaskFile? { - var string: String? - - if url.scheme == "file" { - string = try? String(contentsOf: url) + private static func loadFromApi(_ url: URL) async -> String { + if App.hasLoadedTestableConfiguration { + return await Shell.pipe("curl -s --max-time 10 '\(url.absoluteString)'").out } else { - string = await Shell.pipe(""" + return await Shell.pipe(""" curl -s --max-time 10 \ -H "User-Agent: phpmon \(App.shortVersion)" \ -H "X-phpmon-version: \(App.bundleVersion)" \ @@ -39,6 +37,16 @@ struct CaskFile { '\(url.absoluteString)' """).out } + } + + public static func from(url: URL) async -> CaskFile? { + var string: String? + + if url.scheme == "file" { + string = try? String(contentsOf: url) + } else { + string = await CaskFile.loadFromApi(url) + } guard let string else { Log.err("The content of the URL for the CaskFile could not be retrieved") diff --git a/tests/Shared/TestableConfigurations.swift b/tests/Shared/TestableConfigurations.swift index 4b5bdcf..6961704 100644 --- a/tests/Shared/TestableConfigurations.swift +++ b/tests/Shared/TestableConfigurations.swift @@ -138,14 +138,7 @@ class TestableConfigurations { : .instant(ShellStrings.shared.brewServicesAsRoot), "/opt/homebrew/bin/brew services info --all --json" : .instant(ShellStrings.shared.brewServicesAsUser), - """ - curl -s --max-time 10 \ - -H "User-Agent: phpmon \(App.shortVersion)" \ - -H "X-phpmon-version: \(App.bundleVersion)" \ - -H "X-phpmon-os-version: \(App.macVersion)" \ - -H "X-phpmon-bundle-id: \(App.identifier)" \ - '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)' - """ + "curl -s --max-time 10 '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)'" : .delayed(0.5, """ cask 'phpmon-dev' do depends_on formula: 'gnu-sed' diff --git a/tests/ui/UpdateCheckTest.swift b/tests/ui/UpdateCheckTest.swift index 5bc936d..5b0ef2f 100644 --- a/tests/ui/UpdateCheckTest.swift +++ b/tests/ui/UpdateCheckTest.swift @@ -32,14 +32,7 @@ final class UpdateCheckTest: UITestCase { // Ensure an update is available configuration.shellOutput[ - """ - curl -s --max-time 10 \ - -H "User-Agent: phpmon \(App.shortVersion)" \ - -H "X-phpmon-version: \(App.bundleVersion)" \ - -H "X-phpmon-os-version: \(App.macVersion)" \ - -H "X-phpmon-bundle-id: \(App.identifier)" \ - '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)' - """ + "curl -s --max-time 10 '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)'" ] = .delayed(0.5, """ cask 'phpmon-dev' do depends_on formula: 'gnu-sed' @@ -57,8 +50,9 @@ final class UpdateCheckTest: UITestCase { let app = launch(openMenu: false, with: configuration) - // Expect to see the content of the appropriate alert box - assertExists(app.staticTexts["updater.alerts.newer_version_available.title".localized("99.0.0 (9999)")], 3.0) + // Expect to see the content of the appropriate alert box, but this may take a while; if this test fails try increasing the timeout + let timeout: TimeInterval = 10.0 + assertExists(app.staticTexts["updater.alerts.newer_version_available.title".localized("99.0.0 (9999)")], timeout) assertExists(app.buttons["updater.alerts.buttons.install".localized]) assertExists(app.buttons["updater.alerts.buttons.dismiss".localized]) } @@ -70,16 +64,7 @@ final class UpdateCheckTest: UITestCase { configuration.preferenceOverrides[.automaticBackgroundUpdateCheck] = false // Ensure an update is available - configuration.shellOutput[ - """ - curl -s --max-time 10 \ - -H "User-Agent: phpmon \(App.shortVersion)" \ - -H "X-phpmon-version: \(App.bundleVersion)" \ - -H "X-phpmon-os-version: \(App.macVersion)" \ - -H "X-phpmon-bundle-id: \(App.identifier)" \ - '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)' - """ - ] = .delayed(0.5, """ + configuration.shellOutput["curl -s --max-time 10 '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)'"] = .delayed(0.5, """ cask 'phpmon-dev' do depends_on formula: 'gnu-sed' @@ -117,16 +102,7 @@ final class UpdateCheckTest: UITestCase { configuration.preferenceOverrides[.automaticBackgroundUpdateCheck] = false // Ensure an update is available - configuration.shellOutput[ - """ - curl -s --max-time 10 \ - -H "User-Agent: phpmon \(App.shortVersion)" \ - -H "X-phpmon-version: \(App.bundleVersion)" \ - -H "X-phpmon-os-version: \(App.macVersion)" \ - -H "X-phpmon-bundle-id: \(App.identifier)" \ - '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)' - """ - ] = .delayed(0.5, "404 PAGE NOT FOUND") + configuration.shellOutput["curl -s --max-time 10 '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)'"] = .delayed(0.5, "404 PAGE NOT FOUND") // Wait for the menu to open and search for updates let app = launch(openMenu: true, with: configuration) From 7973f125312ba254f79bf2d7d5947a401588726c Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Fri, 26 Sep 2025 21:46:08 +0200 Subject: [PATCH 21/33] =?UTF-8?q?=E2=9C=85=20Adjust=20UI=20tests=20because?= =?UTF-8?q?=20Xcode=20is=20weird?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/ui/UpdateCheckTest.swift | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/tests/ui/UpdateCheckTest.swift b/tests/ui/UpdateCheckTest.swift index 5b0ef2f..07c8cca 100644 --- a/tests/ui/UpdateCheckTest.swift +++ b/tests/ui/UpdateCheckTest.swift @@ -57,7 +57,7 @@ final class UpdateCheckTest: UITestCase { assertExists(app.buttons["updater.alerts.buttons.dismiss".localized]) } - final func test_will_require_manual_search_for_update() throws { + final func test_does_not_do_automatic_background_check() throws { var configuration = TestableConfigurations.working // Ensure automatic check is disabled @@ -84,9 +84,32 @@ final class UpdateCheckTest: UITestCase { // The check should not happen if the preference is disabled assertNotExists(app.staticTexts["updater.alerts.newer_version_available.title".localized("99.0.0 (9999)")], 2) + } - // Open the menu and check manually - app.statusItems.firstMatch.click() + final func test_will_require_manual_search_for_update() throws { + var configuration = TestableConfigurations.working + + // Ensure automatic check is disabled + configuration.preferenceOverrides[.automaticBackgroundUpdateCheck] = false + + // Ensure an update is available + configuration.shellOutput["curl -s --max-time 10 '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)'"] = .delayed(0.5, """ + cask 'phpmon-dev' do + depends_on formula: 'gnu-sed' + + version '99.0.0_9999' + sha256 '1cb147bd1b1fbd52971d90dff577465b644aee7c878f15ede57f46e8f217067a' + + url 'https://github.com/nicoverbruggen/phpmon/releases/download/v99.0/phpmon-dev.zip' + name 'PHP Monitor DEV' + homepage 'https://phpmon.app' + + app 'PHP Monitor DEV.app', target: "PHP Monitor DEV.app" + end + """) + + // Wait for the menu to open and search for updates + let app = launch(openMenu: true, with: configuration) app.menuItems["mi_check_for_updates".localized].click() // Expect to see the content of the appropriate alert box From cfb72d178130bddd488439e903de37be228a86d0 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Fri, 26 Sep 2025 21:58:19 +0200 Subject: [PATCH 22/33] =?UTF-8?q?=E2=9C=85=20All=20tests=20pass=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/ui/MainMenuTest.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/ui/MainMenuTest.swift b/tests/ui/MainMenuTest.swift index e268c99..4d205ff 100644 --- a/tests/ui/MainMenuTest.swift +++ b/tests/ui/MainMenuTest.swift @@ -108,10 +108,10 @@ final class MainMenuTest: UITestCase { // But not PHP 8.6 (yet) assertNotExists(app.staticTexts["PHP 8.6"]) - // Also, PHP 8.2 should have an update available + // Also, PHP 8.4 should have an update available assertExists(app.staticTexts["phpman.version.has_update".localized( - "8.2.6", - "8.2.11" + "8.4.5", + "8.4.11" )], 5) } From fad180b6ba19122e9e26ec658d87853873117e5e Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Sat, 27 Sep 2025 19:10:55 +0200 Subject: [PATCH 23/33] =?UTF-8?q?=F0=9F=94=A7=20Add=20hardened=20runtime?= =?UTF-8?q?=20again=20for=20notarization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 05a12a4..20cd7c9 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -3797,7 +3797,7 @@ DEAD_CODE_STRIPPING = YES; DEBUG = YES; ENABLE_APP_SANDBOX = NO; - ENABLE_HARDENED_RUNTIME = NO; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; ENABLE_RESOURCE_ACCESS_CALENDARS = NO; ENABLE_RESOURCE_ACCESS_CAMERA = NO; @@ -3841,7 +3841,7 @@ DEAD_CODE_STRIPPING = YES; DEBUG = NO; ENABLE_APP_SANDBOX = NO; - ENABLE_HARDENED_RUNTIME = NO; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; ENABLE_RESOURCE_ACCESS_CALENDARS = NO; ENABLE_RESOURCE_ACCESS_CAMERA = NO; @@ -4023,7 +4023,7 @@ DEAD_CODE_STRIPPING = YES; DEBUG = YES; ENABLE_APP_SANDBOX = NO; - ENABLE_HARDENED_RUNTIME = NO; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; ENABLE_RESOURCE_ACCESS_CALENDARS = NO; ENABLE_RESOURCE_ACCESS_CAMERA = NO; @@ -4216,7 +4216,7 @@ DEAD_CODE_STRIPPING = YES; DEBUG = NO; ENABLE_APP_SANDBOX = NO; - ENABLE_HARDENED_RUNTIME = NO; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; ENABLE_RESOURCE_ACCESS_CALENDARS = NO; ENABLE_RESOURCE_ACCESS_CAMERA = NO; From 17b002f0ab355573c5c75e35ca9c34cc37a9e614 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Mon, 29 Sep 2025 12:06:51 +0200 Subject: [PATCH 24/33] =?UTF-8?q?=F0=9F=93=9D=20Update=20internet=20access?= =?UTF-8?q?=20policy,=20clarify=20update=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpmon/IAP/InternetAccessPolicy.plist | 16 ++++++++++++++++ phpmon/en.lproj/Localizable.strings | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/phpmon/IAP/InternetAccessPolicy.plist b/phpmon/IAP/InternetAccessPolicy.plist index bdcfa16..265d9e3 100644 --- a/phpmon/IAP/InternetAccessPolicy.plist +++ b/phpmon/IAP/InternetAccessPolicy.plist @@ -10,6 +10,22 @@ https://github.com/nicoverbruggen/phpmon Connections + + IsIncoming + + Host + phpmon.app, api.phpmon.app + NetworkProtocol + TCP + Port + 80, 443 + Relevance + Essential + Purpose + PHP Monitor contacts the official phpmon domain to check for updates, and visit aliased links that point to documentation. + DenyConsequences + If you deny these connections, PHP Monitor will not be able to determine if a newer version is available, and certain documentation links in the app may not function as desired. + IsIncoming diff --git a/phpmon/en.lproj/Localizable.strings b/phpmon/en.lproj/Localizable.strings index 738d602..f5455c8 100644 --- a/phpmon/en.lproj/Localizable.strings +++ b/phpmon/en.lproj/Localizable.strings @@ -439,7 +439,7 @@ This has no effect on other terminals, only for the particular terminal session "prefs.open_protocol_desc" = "When checked, this will allow the interaction with third party utilities to work (e.g. Alfred, Raycast). If you disable this, PHP Monitor will still receive the commands, but will not act upon them."; "prefs.automatic_update_check_title" = "Automatically check for updates"; -"prefs.automatic_update_check_desc" = "When checked, PHP Monitor will automatically check if there is a newer version available, and notify you if that is the case."; +"prefs.automatic_update_check_desc" = "When checked, PHP Monitor will automatically check daily if there is a newer version available, and notify you if that is the case."; "prefs.php_doctor_suggestions_title" = "Always show suggestions"; "prefs.php_doctor_suggestions_desc" = "If you uncheck this item, no PHP Doctor suggestions will appear in PHP Monitor's menu. Keep in mind that PHP Doctor will not appear if there are no recommendations."; From 9c9720de42484870d4d9c7c1e606e13bfe6dbba7 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Mon, 29 Sep 2025 13:40:01 +0200 Subject: [PATCH 25/33] =?UTF-8?q?=F0=9F=9A=A7=20Add=20fake=20API=20abstrac?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 40 +++++++++++++++++++++++ phpmon/Common/Core/Helpers.swift | 7 ++++ phpmon/Common/Http/ActiveApi.swift | 29 +++++++++++++++++ phpmon/Common/Http/TestableApi.swift | 41 ++++++++++++++++++++++++ tests/unit/_ST/Api/TestableApiTest.swift | 29 +++++++++++++++++ 5 files changed, 146 insertions(+) create mode 100644 phpmon/Common/Http/ActiveApi.swift create mode 100644 phpmon/Common/Http/TestableApi.swift create mode 100644 tests/unit/_ST/Api/TestableApiTest.swift diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 20cd7c9..82088d5 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -44,6 +44,15 @@ 0396160E2E74A61E002DD7F6 /* LoggableEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */; }; 0396160F2E74A61E002DD7F6 /* LoggableEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */; }; 039616102E74A61E002DD7F6 /* LoggableEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */; }; + 039C29132E8AA163007F5FAB /* ActiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039C29122E8AA15F007F5FAB /* ActiveApi.swift */; }; + 039C29142E8AA163007F5FAB /* ActiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039C29122E8AA15F007F5FAB /* ActiveApi.swift */; }; + 039C29152E8AA163007F5FAB /* ActiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039C29122E8AA15F007F5FAB /* ActiveApi.swift */; }; + 039C29162E8AA163007F5FAB /* ActiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039C29122E8AA15F007F5FAB /* ActiveApi.swift */; }; + 039C29182E8AA314007F5FAB /* TestableApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039C29172E8AA311007F5FAB /* TestableApi.swift */; }; + 039C29192E8AA314007F5FAB /* TestableApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039C29172E8AA311007F5FAB /* TestableApi.swift */; }; + 039C291A2E8AA314007F5FAB /* TestableApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039C29172E8AA311007F5FAB /* TestableApi.swift */; }; + 039C291B2E8AA314007F5FAB /* TestableApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039C29172E8AA311007F5FAB /* TestableApi.swift */; }; + 039C291D2E8AA39A007F5FAB /* TestableApiTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039C291C2E8AA399007F5FAB /* TestableApiTest.swift */; }; 039E1D792E5F0F300072D13D /* ValetUpgrader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039E1D782E5F0F2C0072D13D /* ValetUpgrader.swift */; }; 039E1D7A2E5F0F300072D13D /* ValetUpgrader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039E1D782E5F0F2C0072D13D /* ValetUpgrader.swift */; }; 039E1D7B2E5F0F300072D13D /* ValetUpgrader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039E1D782E5F0F2C0072D13D /* ValetUpgrader.swift */; }; @@ -972,6 +981,9 @@ 036C390E2E5C8D3B008DAEDF /* PackagistError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackagistError.swift; sourceTree = ""; }; 036C39132E5CB820008DAEDF /* TestBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestBundle.swift; sourceTree = ""; }; 0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggableEvent.swift; sourceTree = ""; }; + 039C29122E8AA15F007F5FAB /* ActiveApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveApi.swift; sourceTree = ""; }; + 039C29172E8AA311007F5FAB /* TestableApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableApi.swift; sourceTree = ""; }; + 039C291C2E8AA399007F5FAB /* TestableApiTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableApiTest.swift; sourceTree = ""; }; 039E1D782E5F0F2C0072D13D /* ValetUpgrader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetUpgrader.swift; sourceTree = ""; }; 03BFF5262E312C39007F96FA /* Startup+Timers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Startup+Timers.swift"; sourceTree = ""; }; 03BFF52B2E313240007F96FA /* StatusMenu+Driver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusMenu+Driver.swift"; sourceTree = ""; }; @@ -1337,6 +1349,7 @@ 036C3A222E5CBC33008DAEDF /* _ST */ = { isa = PBXGroup; children = ( + 039C291E2E8AA39B007F5FAB /* Api */, C4C1019927C65A4D001FACC2 /* Commands */, 036C3A232E5CBC57008DAEDF /* Parsers */, 036C39062E5C8890008DAEDF /* Integration */, @@ -1360,6 +1373,23 @@ path = Analytics; sourceTree = ""; }; + 039C29112E8AA159007F5FAB /* Http */ = { + isa = PBXGroup; + children = ( + 039C29172E8AA311007F5FAB /* TestableApi.swift */, + 039C29122E8AA15F007F5FAB /* ActiveApi.swift */, + ); + path = Http; + sourceTree = ""; + }; + 039C291E2E8AA39B007F5FAB /* Api */ = { + isa = PBXGroup; + children = ( + 039C291C2E8AA399007F5FAB /* TestableApiTest.swift */, + ); + path = Api; + sourceTree = ""; + }; 03BFF1D12E3CF4F2004C56A9 /* Provision */ = { isa = PBXGroup; children = ( @@ -2071,6 +2101,7 @@ C4B5853A2770FE2500DA4FBE /* Common */ = { isa = PBXGroup; children = ( + 039C29112E8AA159007F5FAB /* Http */, C4F787A728EF812600790735 /* Testables */, C4F787A628EF811000790735 /* Shell */, C4C8900128F0E27900CE5E97 /* Filesystem */, @@ -2831,6 +2862,7 @@ C4BF56AB2949381100379603 /* FakeValetInteractor.swift in Sources */, C4B5635E276AB09000F12CCB /* VersionExtractor.swift in Sources */, C451AFF62969E40F0078E617 /* HelpButton.swift in Sources */, + 039C29132E8AA163007F5FAB /* ActiveApi.swift in Sources */, C46DC7A42C7B55DC00F19D17 /* Favorites.swift in Sources */, 54D9E0B627E4F51E003B9AD9 /* HotKey.swift in Sources */, C4AFC4AE29C4F32F00BF4E0D /* BrewPhpFormula.swift in Sources */, @@ -2857,6 +2889,7 @@ C4EE188422D3386B00E126E5 /* Constants.swift in Sources */, C493084A279F331F009C240B /* AddSiteVC.swift in Sources */, C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */, + 039C29182E8AA314007F5FAB /* TestableApi.swift in Sources */, C47015022C46D6910069AAE7 /* NVAlertExtension.swift in Sources */, C4E49DEA28F7643D0026AC4E /* CommandProtocol.swift in Sources */, ); @@ -2884,6 +2917,7 @@ C471E83728F9BB650021E251 /* DomainScanner.swift in Sources */, C456A0C82AA614BD0080144F /* PhpPreference.swift in Sources */, C490E3BE29BCA375006D2DE6 /* Measurements.swift in Sources */, + 039C29192E8AA314007F5FAB /* TestableApi.swift in Sources */, C471E83928F9BB650021E251 /* ValetSite.swift in Sources */, C471E83A28F9BB650021E251 /* FakeValetSite.swift in Sources */, C471E83C28F9BB650021E251 /* ValetDomainScanner.swift in Sources */, @@ -2947,6 +2981,7 @@ C4463FCE29804BCB007B93D5 /* RCFile.swift in Sources */, C45B9150295608E300F4EC78 /* ValetServicesManager.swift in Sources */, C471E86528F9BB650021E251 /* WarningManager.swift in Sources */, + 039C29142E8AA163007F5FAB /* ActiveApi.swift in Sources */, C471E86628F9BB650021E251 /* PhpDoctorWindowController.swift in Sources */, C471E86728F9BB650021E251 /* OnboardingWindowController.swift in Sources */, C471E86828F9BB650021E251 /* PreferencesWindowController.swift in Sources */, @@ -3171,6 +3206,7 @@ C471E8CD28F9BB8F0021E251 /* PreferencesVC.swift in Sources */, C471E8CE28F9BB8F0021E251 /* PreferenceName.swift in Sources */, C471E8CF28F9BB8F0021E251 /* Preferences.swift in Sources */, + 039C291B2E8AA314007F5FAB /* TestableApi.swift in Sources */, 03CC1FF62E3D23130050FC18 /* ZshRunCommand.swift in Sources */, C4415E902B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */, C4E2E84D28FC1E70003B070C /* DataExtension.swift in Sources */, @@ -3178,6 +3214,7 @@ C471E8D128F9BB8F0021E251 /* MenuBarIcons.swift in Sources */, C471E8D228F9BB8F0021E251 /* Stats.swift in Sources */, C471E8D328F9BB8F0021E251 /* GlobalKeybindPreference.swift in Sources */, + 039C29152E8AA163007F5FAB /* ActiveApi.swift in Sources */, C471E8D528F9BB8F0021E251 /* CheckboxPreferenceView.swift in Sources */, C471E8D728F9BB8F0021E251 /* SelectPreferenceView.swift in Sources */, C471E8D928F9BB8F0021E251 /* HotkeyPreferenceView.swift in Sources */, @@ -3320,6 +3357,7 @@ 54FCFD2B276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */, C415D3B82770F294005EF286 /* Actions.swift in Sources */, 54B48B60275F66AE006D90C5 /* Application.swift in Sources */, + 039C291A2E8AA314007F5FAB /* TestableApi.swift in Sources */, C4FE011228084FC200D1DE6D /* SelectionVC.swift in Sources */, C4D3661B291173EA006BD146 /* DictionaryExtension.swift in Sources */, C45D654D29F52F74004C28F9 /* BrewPermissionFixer.swift in Sources */, @@ -3478,6 +3516,7 @@ C42CFB1A27DFE8BD00862737 /* NginxConfigurationTest.swift in Sources */, C4BF56AC2949381100379603 /* FakeValetInteractor.swift in Sources */, C471E79428F9B23B0021E251 /* FileSystemProtocol.swift in Sources */, + 039C29162E8AA163007F5FAB /* ActiveApi.swift in Sources */, C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */, C485707C28BF459500539B36 /* NoWarningsView.swift in Sources */, 033D45992B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */, @@ -3493,6 +3532,7 @@ C409349E298EE8E900D25014 /* AppUpdater.swift in Sources */, C4AF9F7D275454A900D44ED0 /* ValetVersionExtractorTest.swift in Sources */, C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */, + 039C291D2E8AA39A007F5FAB /* TestableApiTest.swift in Sources */, C4B585452770FE3900DA4FBE /* RealCommand.swift in Sources */, C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */, C4F780B725D80B5D000DBC97 /* App.swift in Sources */, diff --git a/phpmon/Common/Core/Helpers.swift b/phpmon/Common/Core/Helpers.swift index 21de8c7..ef5a0b1 100644 --- a/phpmon/Common/Core/Helpers.swift +++ b/phpmon/Common/Core/Helpers.swift @@ -8,6 +8,8 @@ // MARK: Common Shell Commands +import Foundation + /** Runs a `brew` command. Can run as superuser. */ @@ -49,3 +51,8 @@ func grepContains(file: String, query: String) async -> Bool { func delay(seconds: Double) async { try! await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) } + +/** A simpler way to initialize a fixed, valid URL. */ +func url(_ string: String) -> URL { + return URL(string: string)! +} diff --git a/phpmon/Common/Http/ActiveApi.swift b/phpmon/Common/Http/ActiveApi.swift new file mode 100644 index 0000000..245b5c5 --- /dev/null +++ b/phpmon/Common/Http/ActiveApi.swift @@ -0,0 +1,29 @@ +// +// ActiveApi.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 29/09/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +import Foundation + +var Api: ApiProtocol { + return ActiveApi.shared +} + +class ActiveApi { + static var shared: ApiProtocol = RealApi() + + public static func useTestable(_ responses: [URL: FakeApiResponse]) { + Self.shared = TestableApi(responses: responses) + } + + public static func useReal() { + Self.shared = RealApi() + } +} + +protocol ApiProtocol {} + +class RealApi: ApiProtocol {} diff --git a/phpmon/Common/Http/TestableApi.swift b/phpmon/Common/Http/TestableApi.swift new file mode 100644 index 0000000..e9eaea3 --- /dev/null +++ b/phpmon/Common/Http/TestableApi.swift @@ -0,0 +1,41 @@ +// +// TestableApi.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 29/09/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class TestableApi: ApiProtocol { + private var fakeResponses: [URL: FakeApiResponse] = [:] + + init(responses: [URL: FakeApiResponse]) { + self.fakeResponses = responses + } + + public func hasResponse(for url: URL) -> Bool { + return fakeResponses.keys.contains(url) + } + + public func getResponse(for url: URL) -> FakeApiResponse { + return fakeResponses[url]! + } +} + +struct FakeApiResponse { + let statusCode: Int + let headers: [String: String] + let data: Data? + + init(statusCode: Int, headers: [String: String], text: String) { + self.statusCode = statusCode + self.headers = headers + self.data = text.data(using: .utf8) + } + + var text: String { + return String(data: self.data!, encoding: .utf8) ?? "" + } +} diff --git a/tests/unit/_ST/Api/TestableApiTest.swift b/tests/unit/_ST/Api/TestableApiTest.swift new file mode 100644 index 0000000..5ac9584 --- /dev/null +++ b/tests/unit/_ST/Api/TestableApiTest.swift @@ -0,0 +1,29 @@ +// +// Untitled.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 29/09/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +import Testing +import Foundation + +@Suite("Api") +struct TestableApiTest { + + @Test + func createFakeApi() { + let api = TestableApi(responses: [ + url("https://api.phpmon.test"): FakeApiResponse(statusCode: 200, headers: [:], text: "{\"success\": true}") + ]) + + #expect(api.hasResponse(for: url("https://api.phpmon.test")) == true) + + let response = api.getResponse(for: url("https://api.phpmon.test")) + + #expect(response.statusCode == 200) + #expect(response.text.contains("success")) + } + +} From 5b27d9f0ea2e3b6f0f9e0df6b94e8b317508d364 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Mon, 29 Sep 2025 16:31:30 +0200 Subject: [PATCH 26/33] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Migrate=20more=20tes?= =?UTF-8?q?ts=20to=20Swift=20Testing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 10 +-- phpmon/Common/Core/Constants.swift | 64 ++++++--------- phpmon/Common/Core/Helpers.swift | 4 +- .../Extensions/TimeIntervalExtension.swift | 11 ++- .../Config/BytePhpPreferenceTest.swift | 42 ---------- .../unit/Parsers/NginxConfigurationTest.swift | 81 ------------------- tests/unit/Parsers/PhpExtensionTest.swift | 72 ----------------- tests/unit/Parsers/ValetRcTest.swift | 49 ----------- tests/unit/_ST/Api/TestableApiTest.swift | 12 +-- tests/unit/_ST/Commands/CommandTest.swift | 6 +- .../unit/_ST/Integration/PackagistTest.swift | 1 - .../Config/BytePhpPreferenceTest.swift | 41 ++++++++++ .../Config/PhpConfigurationFileTest.swift | 60 +++++++------- .../Parsers/HomebrewPackageTest.swift | 50 ++++++------ .../_ST/Parsers/NginxConfigurationTest.swift | 70 ++++++++++++++++ tests/unit/_ST/Parsers/PhpExtensionTest.swift | 71 ++++++++++++++++ .../_ST/Parsers/ValetConfigurationTest.swift | 5 +- tests/unit/_ST/Parsers/ValetRcTest.swift | 45 +++++++++++ 18 files changed, 330 insertions(+), 364 deletions(-) delete mode 100644 tests/unit/Parsers/Config/BytePhpPreferenceTest.swift delete mode 100644 tests/unit/Parsers/NginxConfigurationTest.swift delete mode 100644 tests/unit/Parsers/PhpExtensionTest.swift delete mode 100644 tests/unit/Parsers/ValetRcTest.swift create mode 100644 tests/unit/_ST/Parsers/Config/BytePhpPreferenceTest.swift rename tests/unit/{ => _ST}/Parsers/Config/PhpConfigurationFileTest.swift (51%) rename tests/unit/{ => _ST}/Parsers/HomebrewPackageTest.swift (70%) create mode 100644 tests/unit/_ST/Parsers/NginxConfigurationTest.swift create mode 100644 tests/unit/_ST/Parsers/PhpExtensionTest.swift create mode 100644 tests/unit/_ST/Parsers/ValetRcTest.swift diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 82088d5..67b0ccb 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -1361,6 +1361,11 @@ isa = PBXGroup; children = ( C4AF9F76275447F100D44ED0 /* ValetConfigurationTest.swift */, + C4F780AD25D80B37000DBC97 /* PhpExtensionTest.swift */, + C43A8A2325D9D20D00591B77 /* HomebrewPackageTest.swift */, + C42CFB1927DFE8BD00862737 /* NginxConfigurationTest.swift */, + C4551656297AED18009B8466 /* ValetRcTest.swift */, + C456A0D02AA6175D0080144F /* Config */, ); path = Parsers; sourceTree = ""; @@ -2188,11 +2193,6 @@ C4C1019727C65A11001FACC2 /* Parsers */ = { isa = PBXGroup; children = ( - C456A0D02AA6175D0080144F /* Config */, - C4F780AD25D80B37000DBC97 /* PhpExtensionTest.swift */, - C43A8A2325D9D20D00591B77 /* HomebrewPackageTest.swift */, - C42CFB1927DFE8BD00862737 /* NginxConfigurationTest.swift */, - C4551656297AED18009B8466 /* ValetRcTest.swift */, C40934AA298EEDA900D25014 /* CaskFileParserTest.swift */, C4AFC4B229C4F43300BF4E0D /* HomebrewUpgradableTest.swift */, C4F520662AF03791006787F2 /* ExtensionEnumeratorTest.swift */, diff --git a/phpmon/Common/Core/Constants.swift b/phpmon/Common/Core/Constants.swift index 430c2a9..155099d 100644 --- a/phpmon/Common/Core/Constants.swift +++ b/phpmon/Common/Core/Constants.swift @@ -26,34 +26,34 @@ struct Constants { be displayed. This is based on an appropriate launch time on a basic M1 Apple chip, with some margin for slower Intel chips. */ - static let SlowBootThresholdInterval: TimeInterval = 30.0 + static let SlowBootThresholdInterval: TimeInterval = .seconds(30) /** The interval between automatic background update checks. */ - static let AutomaticUpdateCheckInterval: TimeInterval = 60.0 * 60 * 24 // 24 hours + static let AutomaticUpdateCheckInterval: TimeInterval = .hours(24) /** The minimum interval that must pass before allowing another automatic update check. This prevents excessive checking on frequent app restarts (due to crashes or bad config). */ - static let MinimumUpdateCheckInterval: TimeInterval = 60.0 * 60 // 60 minutes + static let MinimumUpdateCheckInterval: TimeInterval = .minutes(60) /** Retry intervals for failed automatic update checks. Uses exponential backoff before falling back to normal schedule. */ static let UpdateCheckRetryIntervals: [TimeInterval] = [ - 60 * 5, // 5 minutes - 60 * 15, // 15 minutes - 60 * 60, // 1 hour - 60 * 60 * 3 // 3 hours (final attempt) + .minutes(5), + .minutes(15), + .hours(1), + .hours(3) ] /** PHP Monitor supplies a hardcoded list of PHP packages in its own - PHP Version Manager. + PHP Version Manager. This hardcoded list will expire and will need to be modified when the cutoff date occurs, which is when the `php` formula will @@ -62,8 +62,12 @@ struct Constants { If users launch an older version of the app, then a warning will be displayed to let them know that certain operations will not work correctly and that they need to update their app. + + The cutoff date is always a few days after GA of the latest + release, as it often takes a while for Homebrew to make the + new release available and not everyone uses a separate tap. */ - static let PhpFormulaeCutoffDate = "2025-11-30" // YYYY-MM-DD + static let PhpFormulaeCutoffDate = "2025-11-20" // YYYY-MM-DD /** * The PHP versions that are considered pre-release versions. @@ -72,7 +76,8 @@ struct Constants { */ static var ExperimentalPhpVersions: Set { let releaseDates = [ - "8.5": Date.fromString(Self.PhpFormulaeCutoffDate), + // "8.6": Date.fromString("2026-11-30"), // TBD + "8.5": Date.fromString(PhpFormulaeCutoffDate), "8.4": Date.fromString("2024-11-22") ] @@ -108,6 +113,7 @@ struct Constants { "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4", "8.5" // DEV + // "8.6" // TBD ] /** @@ -130,48 +136,28 @@ struct Constants { "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4", "8.5" // DEV + // "8.6" // TBD ] ] struct Urls { - // phpmon.app URLs (these are aliased to redirect correctly) + static let DonationPage = url("https://phpmon.app/sponsor") - static let DonationPage = URL( - string: "https://phpmon.app/sponsor" - )! + static let FrequentlyAskedQuestions = url("https://phpmon.app/faq") - static let FrequentlyAskedQuestions = URL( - string: "https://phpmon.app/faq" - )! + static let WikiPhpUnavailable = url("https://phpmon.app/php-unavailable") - static let WikiPhpUnavailable = URL( - string: "https://phpmon.app/php-unavailable" - )! + static let WikiPhpUpgrade = url("https://phpmon.app/php-upgrade") - static let WikiPhpUpgrade = URL( - string: "https://phpmon.app/php-upgrade" - )! + static let DonationPayment = url("https://phpmon.app/sponsor/now") - static let DonationPayment = URL( - string: "https://phpmon.app/sponsor/now" - )! - - static let EarlyAccessChangelog = URL( - string: "https://phpmon.app/early-access/release-notes" - )! + static let EarlyAccessChangelog = url("https://phpmon.app/early-access/release-notes") // API endpoints (via api.phpmon.app) - - static let UpdateCheckEndpoint = URL( - string: "https://api.phpmon.app/api/v1/update-check" - )! + static let UpdateCheckEndpoint = url("https://api.phpmon.app/api/v1/update-check") // GitHub URLs (do not alias these) - - static let GitHubReleases = URL( - string: "https://github.com/nicoverbruggen/phpmon/releases" - )! + static let GitHubReleases = url("https://github.com/nicoverbruggen/phpmon/releases") } - } diff --git a/phpmon/Common/Core/Helpers.swift b/phpmon/Common/Core/Helpers.swift index ef5a0b1..64ea165 100644 --- a/phpmon/Common/Core/Helpers.swift +++ b/phpmon/Common/Core/Helpers.swift @@ -52,7 +52,9 @@ func delay(seconds: Double) async { try! await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) } -/** A simpler way to initialize a fixed, valid URL. */ +/** + A simpler way to initialize a fixed, valid URL. + */ func url(_ string: String) -> URL { return URL(string: string)! } diff --git a/phpmon/Common/Extensions/TimeIntervalExtension.swift b/phpmon/Common/Extensions/TimeIntervalExtension.swift index bbf8990..8fb1e51 100644 --- a/phpmon/Common/Extensions/TimeIntervalExtension.swift +++ b/phpmon/Common/Extensions/TimeIntervalExtension.swift @@ -9,7 +9,14 @@ import Foundation extension TimeInterval { - public static func minutes(_ amount: Int) -> TimeInterval { - return Double(amount * 60) + static func seconds(_ value: Double) -> TimeInterval { value } + static func minutes(_ value: Double) -> TimeInterval { value * 60 } + static func hours(_ value: Double) -> TimeInterval { value * 3600 } + static func days(_ value: Double) -> TimeInterval { value * 86400 } +} + +extension Date { + func adding(_ interval: TimeInterval) -> Date { + return self.addingTimeInterval(interval) } } diff --git a/tests/unit/Parsers/Config/BytePhpPreferenceTest.swift b/tests/unit/Parsers/Config/BytePhpPreferenceTest.swift deleted file mode 100644 index 4e48c15..0000000 --- a/tests/unit/Parsers/Config/BytePhpPreferenceTest.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// BytePhpPreferenceTest.swift -// Unit Tests -// -// Created by Nico Verbruggen on 04/09/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. -// - -import XCTest - -class BytePhpPreferenceTest: XCTestCase { - - func test_can_extract_memory_value() throws { - let pref = BytePhpPreference(key: "memory_limit") - - XCTAssertEqual(pref.internalValue, "512M") - XCTAssertEqual(pref.unit, .megabyte) - XCTAssertEqual(pref.value, 512) - } - - func test_can_parse_all_kinds_of_values() throws { - var (unit, value) = BytePhpPreference.readFrom(internalValue: "1G")! - XCTAssertEqual(unit, .gigabyte) - XCTAssertEqual(value, 1) - - (unit, value) = BytePhpPreference.readFrom(internalValue: "256M")! - XCTAssertEqual(unit, .megabyte) - XCTAssertEqual(value, 256) - - (unit, value) = BytePhpPreference.readFrom(internalValue: "512K")! - XCTAssertEqual(unit, .kilobyte) - XCTAssertEqual(value, 512) - - (unit, value) = BytePhpPreference.readFrom(internalValue: "1024")! - XCTAssertEqual(unit, .kilobyte) - XCTAssertEqual(value, 1024) - - (unit, value) = BytePhpPreference.readFrom(internalValue: "-1")! - XCTAssertEqual(unit, .kilobyte) - XCTAssertEqual(value, -1) - } -} diff --git a/tests/unit/Parsers/NginxConfigurationTest.swift b/tests/unit/Parsers/NginxConfigurationTest.swift deleted file mode 100644 index 5306072..0000000 --- a/tests/unit/Parsers/NginxConfigurationTest.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// NginxConfigurationTest.swift -// PHP Monitor -// -// Created by Nico Verbruggen on 29/11/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. -// - -import XCTest - -class NginxConfigurationTest: XCTestCase { - - // MARK: - Test Files - - static var regularUrl: URL { - return Bundle(for: Self.self).url(forResource: "nginx-site", withExtension: "test")! - } - - static var isolatedUrl: URL { - return Bundle(for: Self.self).url(forResource: "nginx-site-isolated", withExtension: "test")! - } - - static var proxyUrl: URL { - return Bundle(for: Self.self).url(forResource: "nginx-proxy", withExtension: "test")! - } - - static var secureProxyUrl: URL { - return Bundle(for: Self.self).url(forResource: "nginx-secure-proxy", withExtension: "test")! - } - - static var customTldProxyUrl: URL { - return Bundle(for: Self.self).url(forResource: "nginx-secure-proxy-custom-tld", withExtension: "test")! - } - - // MARK: - Tests - - func test_can_determine_site_name_and_tld() throws { - XCTAssertEqual( - "nginx-site", - NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.domain - ) - XCTAssertEqual( - "test", - NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.tld - ) - } - - func test_can_determine_isolation() throws { - XCTAssertNil( - NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.isolatedVersion - ) - - XCTAssertEqual( - "8.1", - NginxConfigurationFile.from(filePath: NginxConfigurationTest.isolatedUrl.path)?.isolatedVersion - ) - } - - func test_can_determine_proxy() throws { - let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.proxyUrl.path)! - XCTAssertTrue(proxied.contents.contains("# valet stub: proxy.valet.conf")) - XCTAssertEqual("http://127.0.0.1:90", proxied.proxy) - - let normal = NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)! - XCTAssertFalse(normal.contents.contains("# valet stub: proxy.valet.conf")) - XCTAssertEqual(nil, normal.proxy) - } - - func test_can_determine_secured_proxy() throws { - let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.secureProxyUrl.path)! - XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf")) - XCTAssertEqual("http://127.0.0.1:90", proxied.proxy) - } - - func test_can_determine_proxy_with_custom_tld() throws { - let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.customTldProxyUrl.path)! - XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf")) - XCTAssertEqual("http://localhost:8080", proxied.proxy) - } - -} diff --git a/tests/unit/Parsers/PhpExtensionTest.swift b/tests/unit/Parsers/PhpExtensionTest.swift deleted file mode 100644 index 94f7f04..0000000 --- a/tests/unit/Parsers/PhpExtensionTest.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// ExtensionParserTest.swift -// PHP Monitor -// -// Created by Nico Verbruggen on 13/02/2021. -// Copyright © 2023 Nico Verbruggen. All rights reserved. -// - -import XCTest - -class PhpExtensionTest: XCTestCase { - - static var phpIniFileUrl: URL { - return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")! - } - - func test_can_load_extension() throws { - let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path) - - XCTAssertGreaterThan(extensions.count, 0) - } - - func test_extension_name_is_correct() throws { - let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path) - - let extensionNames = extensions.map { (ext) -> String in - return ext.name - } - - // These 6 should be found - XCTAssertTrue(extensionNames.contains("xdebug")) - XCTAssertTrue(extensionNames.contains("imagick")) - XCTAssertTrue(extensionNames.contains("sodium-next")) - XCTAssertTrue(extensionNames.contains("opcache")) - XCTAssertTrue(extensionNames.contains("yaml")) - XCTAssertTrue(extensionNames.contains("custom")) - - XCTAssertFalse(extensionNames.contains("fake")) - XCTAssertFalse(extensionNames.contains("nice")) - } - - func test_extension_status_is_correct() throws { - let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path) - - // xdebug should be enabled - XCTAssertEqual(extensions[0].enabled, true) - - // imagick should be disabled - XCTAssertEqual(extensions[1].enabled, false) - } - - func test_toggle_works_as_expected() async throws { - let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")! - let extensions = PhpExtension.from(filePath: destination.path) - XCTAssertEqual(extensions.count, 6) - - // Try to disable xdebug (should be detected first)! - let xdebug = extensions.first! - XCTAssertTrue(xdebug.name == "xdebug") - XCTAssertEqual(xdebug.enabled, true) - await xdebug.toggle() - XCTAssertEqual(xdebug.enabled, false) - - // Check if the file contains the appropriate data - let file = try! String(contentsOf: destination, encoding: .utf8) - XCTAssertTrue(file.contains("; zend_extension=\"xdebug.so\"")) - - // Make sure if we load the data again, it's disabled - XCTAssertEqual(PhpExtension.from(filePath: destination.path).first!.enabled, false) - } - -} diff --git a/tests/unit/Parsers/ValetRcTest.swift b/tests/unit/Parsers/ValetRcTest.swift deleted file mode 100644 index 3a4428f..0000000 --- a/tests/unit/Parsers/ValetRcTest.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// ValetRcTest.swift -// Unit Tests -// -// Created by Nico Verbruggen on 20/01/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. -// - -import XCTest - -class ValetRcTest: XCTestCase { - - // MARK: - Test Files - - static var validPath: URL { - return Bundle(for: Self.self) - .url(forResource: "valetrc", withExtension: "valid")! - } - - static var brokenPath: URL { - return Bundle(for: Self.self) - .url(forResource: "valetrc", withExtension: "broken")! - } - - // MARK: - Tests - - func test_can_extract_fields_from_valetrc_file() throws { - let fakeFile = RCFile.fromPath("/Users/fake/file.rc") - XCTAssertNil(fakeFile) - - // Can parse the file - let validFile = RCFile.fromPath(ValetRcTest.validPath.path) - XCTAssertNotNil(validFile) - - let fields = validFile!.fields - - // Correctly parses and trims (and omits double quotes) per line - XCTAssertEqual(fields["PHP"], "php@8.2") - XCTAssertEqual(fields["OTHER"], "thing") - XCTAssertEqual(fields["PHPMON_WATCH"], "true") - XCTAssertEqual(fields["SYNTAX"], "variable") - - // Ignores entries prefixed with # - XCTAssertTrue(!fields.keys.contains("#PHP")) - - // Ignores invalid lines - XCTAssertTrue(!fields.keys.contains("OOF")) - } -} diff --git a/tests/unit/_ST/Api/TestableApiTest.swift b/tests/unit/_ST/Api/TestableApiTest.swift index 5ac9584..e7326ef 100644 --- a/tests/unit/_ST/Api/TestableApiTest.swift +++ b/tests/unit/_ST/Api/TestableApiTest.swift @@ -9,13 +9,14 @@ import Testing import Foundation -@Suite("Api") struct TestableApiTest { - - @Test - func createFakeApi() { + @Test func createFakeApi() { let api = TestableApi(responses: [ - url("https://api.phpmon.test"): FakeApiResponse(statusCode: 200, headers: [:], text: "{\"success\": true}") + url("https://api.phpmon.test"): FakeApiResponse( + statusCode: 200, + headers: [:], + text: "{\"success\": true}" + ) ]) #expect(api.hasResponse(for: url("https://api.phpmon.test")) == true) @@ -25,5 +26,4 @@ struct TestableApiTest { #expect(response.statusCode == 200) #expect(response.text.contains("success")) } - } diff --git a/tests/unit/_ST/Commands/CommandTest.swift b/tests/unit/_ST/Commands/CommandTest.swift index 5b7feee..8dfe410 100644 --- a/tests/unit/_ST/Commands/CommandTest.swift +++ b/tests/unit/_ST/Commands/CommandTest.swift @@ -8,11 +8,8 @@ import Testing -@Suite("Commands") struct CommandTest { - - @Test - func determinePhpVersion() { + @Test func determinePhpVersion() { let version = Command.execute( path: Paths.php, arguments: ["-v"], @@ -24,5 +21,4 @@ struct CommandTest { #expect(version.contains("built")) #expect(version.contains("Zend")) } - } diff --git a/tests/unit/_ST/Integration/PackagistTest.swift b/tests/unit/_ST/Integration/PackagistTest.swift index 045e17a..3ed6450 100644 --- a/tests/unit/_ST/Integration/PackagistTest.swift +++ b/tests/unit/_ST/Integration/PackagistTest.swift @@ -8,7 +8,6 @@ import Testing -@Suite("Integration") struct PackagistTest { @Test func canRetrieveLaravelValetVersion() async { let packageToCheck = "laravel/valet" diff --git a/tests/unit/_ST/Parsers/Config/BytePhpPreferenceTest.swift b/tests/unit/_ST/Parsers/Config/BytePhpPreferenceTest.swift new file mode 100644 index 0000000..319c0e9 --- /dev/null +++ b/tests/unit/_ST/Parsers/Config/BytePhpPreferenceTest.swift @@ -0,0 +1,41 @@ +// +// BytePhpPreferenceTest.swift +// Unit Tests +// +// Created by Nico Verbruggen on 04/09/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Testing + +struct BytePhpPreferenceTest { + @Test func test_can_extract_memory_value() throws { + let pref = BytePhpPreference(key: "memory_limit") + + #expect(pref.internalValue == "512M") + #expect(pref.unit == .megabyte) + #expect(pref.value == 512) + } + + @Test func test_can_parse_all_kinds_of_values() throws { + var (unit, value) = BytePhpPreference.readFrom(internalValue: "1G")! + #expect(unit == .gigabyte) + #expect(value == 1) + + (unit, value) = BytePhpPreference.readFrom(internalValue: "256M")! + #expect(unit == .megabyte) + #expect(value == 256) + + (unit, value) = BytePhpPreference.readFrom(internalValue: "512K")! + #expect(unit == .kilobyte) + #expect(value == 512) + + (unit, value) = BytePhpPreference.readFrom(internalValue: "1024")! + #expect(unit == .kilobyte) + #expect(value == 1024) + + (unit, value) = BytePhpPreference.readFrom(internalValue: "-1")! + #expect(unit == .kilobyte) + #expect(value == -1) + } +} diff --git a/tests/unit/Parsers/Config/PhpConfigurationFileTest.swift b/tests/unit/_ST/Parsers/Config/PhpConfigurationFileTest.swift similarity index 51% rename from tests/unit/Parsers/Config/PhpConfigurationFileTest.swift rename to tests/unit/_ST/Parsers/Config/PhpConfigurationFileTest.swift index 036c5a9..6e2cc49 100644 --- a/tests/unit/Parsers/Config/PhpConfigurationFileTest.swift +++ b/tests/unit/_ST/Parsers/Config/PhpConfigurationFileTest.swift @@ -6,41 +6,41 @@ // Copyright © 2023 Nico Verbruggen. All rights reserved. // -import XCTest +import Testing +import Foundation -class PhpConfigurationFileTest: XCTestCase { +class PhpConfigurationFileTest { static var phpIniFileUrl: URL { - return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")! + return TestBundle.url(forResource: "php", withExtension: "ini")! } - func test_can_load_extension() throws { + @Test func test_can_load_extension() throws { + let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path) + + #expect(iniFile != nil) + #expect(!iniFile!.extensions.isEmpty) + } + + @Test func test_can_check_key_existence() throws { let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)! - XCTAssertNotNil(iniFile) - - XCTAssertGreaterThan(iniFile.extensions.count, 0) + #expect(iniFile.has(key: "error_reporting")) + #expect(iniFile.has(key: "display_errors")) + #expect(false == iniFile.has(key: "my_unknown_key")) } - func test_can_check_key_existence() throws { + @Test func test_can_check_key_value() throws { let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)! - XCTAssertTrue(iniFile.has(key: "error_reporting")) - XCTAssertTrue(iniFile.has(key: "display_errors")) - XCTAssertFalse(iniFile.has(key: "my_unknown_key")) + #expect(iniFile.get(for: "error_reporting") != nil) + #expect(iniFile.get(for: "error_reporting") == "E_ALL") + + #expect(iniFile.get(for: "display_errors") != nil) + #expect(iniFile.get(for: "display_errors") == "On") } - func test_can_check_key_value() throws { - let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)! - - XCTAssertNotNil(iniFile.get(for: "error_reporting")) - XCTAssert(iniFile.get(for: "error_reporting") == "E_ALL") - - XCTAssertNotNil(iniFile.get(for: "display_errors")) - XCTAssert(iniFile.get(for: "display_errors") == "On") - } - - func test_can_customize_configuration_value() throws { + @Test func test_can_customize_configuration_value() throws { let destination = Utility .copyToTemporaryFile(resourceName: "php", fileExtension: "ini")! @@ -48,15 +48,15 @@ class PhpConfigurationFileTest: XCTestCase { .from(filePath: destination.path)! // 0. Verify the original value - XCTAssertEqual(configurationFile.get(for: "error_reporting"), "E_ALL") + #expect(configurationFile.get(for: "error_reporting") == "E_ALL") // 1. Change the value try! configurationFile.replace( key: "error_reporting", value: "E_ALL & ~E_DEPRECATED & ~E_STRICT" ) - XCTAssertEqual( - configurationFile.get(for: "error_reporting"), + #expect( + configurationFile.get(for: "error_reporting") == "E_ALL & ~E_DEPRECATED & ~E_STRICT" ) @@ -65,20 +65,14 @@ class PhpConfigurationFileTest: XCTestCase { key: "error_reporting", value: "error_reporting" ) - XCTAssertEqual( - configurationFile.get(for: "error_reporting"), - "error_reporting" - ) + #expect(configurationFile.get(for: "error_reporting") == "error_reporting") // 3. Verify subsequent saves weren't broken try! configurationFile.replace( key: "error_reporting", value: "E_ALL" ) - XCTAssertEqual( - configurationFile.get(for: "error_reporting"), - "E_ALL" - ) + #expect(configurationFile.get(for: "error_reporting") == "E_ALL") } } diff --git a/tests/unit/Parsers/HomebrewPackageTest.swift b/tests/unit/_ST/Parsers/HomebrewPackageTest.swift similarity index 70% rename from tests/unit/Parsers/HomebrewPackageTest.swift rename to tests/unit/_ST/Parsers/HomebrewPackageTest.swift index e6a5f8a..eb5627a 100644 --- a/tests/unit/Parsers/HomebrewPackageTest.swift +++ b/tests/unit/_ST/Parsers/HomebrewPackageTest.swift @@ -6,53 +6,54 @@ // Copyright © 2023 Nico Verbruggen. All rights reserved. // -import XCTest +import Testing +import Foundation -class HomebrewPackageTest: XCTestCase { +struct HomebrewPackageTest { // - MARK: SYNTHETIC TESTS static var jsonBrewFile: URL { - return Bundle(for: Self.self) + return TestBundle .url(forResource: "brew-formula", withExtension: "json")! } - func test_can_load_extension_json() throws { + static var jsonBrewServicesFile: URL { + return TestBundle + .url(forResource: "brew-services", withExtension: "json")! + } + + @Test func test_can_load_extension_json() throws { let json = try! String(contentsOf: Self.jsonBrewFile, encoding: .utf8) let package = try! JSONDecoder().decode( [HomebrewPackage].self, from: json.data(using: .utf8)! ).first! - XCTAssertEqual(package.full_name, "php") - XCTAssertEqual(package.aliases.first!, "php@8.4") - XCTAssertEqual(package.installed.contains(where: { installed in + #expect(package.full_name == "php") + #expect(package.aliases.first! == "php@8.4") + #expect(package.installed.contains(where: { installed in installed.version.starts(with: "8.4") - }), true) + }) == true) } - static var jsonBrewServicesFile: URL { - return Bundle(for: Self.self) - .url(forResource: "brew-services", withExtension: "json")! - } - - func test_can_parse_services_json() throws { + @Test func test_can_parse_services_json() throws { let json = try! String(contentsOf: Self.jsonBrewServicesFile, encoding: .utf8) let services = try! JSONDecoder().decode( [HomebrewService].self, from: json.data(using: .utf8)! ) - XCTAssertGreaterThan(services.count, 0) - XCTAssertEqual(services.first?.name, "dnsmasq") - XCTAssertEqual(services.first?.service_name, "homebrew.mxcl.dnsmasq") + #expect(!services.isEmpty) + #expect(services.first?.name == "dnsmasq") + #expect(services.first?.service_name == "homebrew.mxcl.dnsmasq") } - /* // - MARK: LIVE TESTS /// This test requires that you have a valid Homebrew installation set up, /// and requires the Valet services to be installed: php, nginx and dnsmasq. /// If this test fails, there is an issue with your Homebrew installation /// or the JSON API of the Homebrew output may have changed. + @Test(.disabled("Uses system command; enable at your own risk")) func test_can_parse_services_json_from_cli_output() async throws { ActiveShell.useSystem() @@ -65,17 +66,19 @@ class HomebrewPackageTest: XCTestCase { return ["php", "nginx", "dnsmasq"].contains(service.name) }) - XCTAssertTrue(services.contains(where: {$0.name == "php"})) - XCTAssertTrue(services.contains(where: {$0.name == "nginx"})) - XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"})) - XCTAssertEqual(services.count, 3) + #expect(services.contains(where: {$0.name == "php"})) + #expect(services.contains(where: {$0.name == "nginx"})) + #expect(services.contains(where: {$0.name == "dnsmasq"})) + #expect(services.count == 3) } /// This test requires that you have a valid Homebrew installation set up, /// and requires the `php` formula to be installed. /// If this test fails, there is an issue with your Homebrew installation /// or the JSON API of the Homebrew output may have changed. + @Test(.disabled("Uses system command; enable at your own risk")) func test_can_load_extension_json_from_cli_output() async throws { + ActiveShell.useSystem() let package = try! JSONDecoder().decode( @@ -83,7 +86,6 @@ class HomebrewPackageTest: XCTestCase { from: await Shell.pipe("\(Paths.brew) info php --json").out.data(using: .utf8)! ).first! - XCTAssertTrue(package.name == "php") + #expect(package.full_name == "php") } - */ } diff --git a/tests/unit/_ST/Parsers/NginxConfigurationTest.swift b/tests/unit/_ST/Parsers/NginxConfigurationTest.swift new file mode 100644 index 0000000..a188638 --- /dev/null +++ b/tests/unit/_ST/Parsers/NginxConfigurationTest.swift @@ -0,0 +1,70 @@ +// +// NginxConfigurationTest.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 29/11/2021. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Testing +import Foundation + +struct NginxConfigurationTest { + + // MARK: - Test Files + + static var regularUrl: URL { + TestBundle.url(forResource: "nginx-site", withExtension: "test")! + } + + static var isolatedUrl: URL { + TestBundle.url(forResource: "nginx-site-isolated", withExtension: "test")! + } + + static var proxyUrl: URL { + TestBundle.url(forResource: "nginx-proxy", withExtension: "test")! + } + + static var secureProxyUrl: URL { + TestBundle.url(forResource: "nginx-secure-proxy", withExtension: "test")! + } + + static var customTldProxyUrl: URL { + TestBundle.url(forResource: "nginx-secure-proxy-custom-tld", withExtension: "test")! + } + + // MARK: - Tests + + @Test func test_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) + } + + @Test func test_can_determine_isolation() throws { + #expect(nil == NginxConfigurationFile.from(filePath: Self.regularUrl.path)?.isolatedVersion) + #expect("8.1" == NginxConfigurationFile.from(filePath: Self.isolatedUrl.path)?.isolatedVersion) + } + + @Test func test_can_determine_proxy() throws { + let proxied = NginxConfigurationFile.from(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)! + #expect(false == normal.contents.contains("# valet stub: proxy.valet.conf")) + #expect(nil == normal.proxy) + } + + @Test func test_can_determine_secured_proxy() throws { + let proxied = NginxConfigurationFile.from(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 test_can_determine_proxy_with_custom_tld() throws { + let proxied = NginxConfigurationFile.from(filePath: Self.customTldProxyUrl.path)! + #expect(proxied.contents.contains("# valet stub: secure.proxy.valet.conf")) + #expect("http://localhost:8080" == proxied.proxy) + } + +} diff --git a/tests/unit/_ST/Parsers/PhpExtensionTest.swift b/tests/unit/_ST/Parsers/PhpExtensionTest.swift new file mode 100644 index 0000000..c81b296 --- /dev/null +++ b/tests/unit/_ST/Parsers/PhpExtensionTest.swift @@ -0,0 +1,71 @@ +// +// ExtensionParserTest.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 13/02/2021. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Testing +import Foundation + +struct PhpExtensionTest { + static var phpIniFileUrl: URL { + return TestBundle.url(forResource: "php", withExtension: "ini")! + } + + @Test func test_can_load_extension() throws { + let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path) + + #expect(!extensions.isEmpty) + } + + @Test func test_extension_name_is_correct() throws { + let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path) + + let extensionNames = extensions.map { (ext) -> String in + return ext.name + } + + // These 6 should be found + #expect(extensionNames.contains("xdebug")) + #expect(extensionNames.contains("imagick")) + #expect(extensionNames.contains("sodium-next")) + #expect(extensionNames.contains("opcache")) + #expect(extensionNames.contains("yaml")) + #expect(extensionNames.contains("custom")) + + #expect(extensionNames.contains("fake") == false) + #expect(extensionNames.contains("nice") == false) + } + + @Test func test_extension_status_is_correct() throws { + let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path) + + // xdebug should be enabled + #expect(extensions[0].enabled == true) + + // imagick should be disabled + #expect(extensions[1].enabled == false) + } + + @Test func test_toggle_works_as_expected() async throws { + let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")! + let extensions = PhpExtension.from(filePath: destination.path) + #expect(extensions.count == 6) + + // Try to disable xdebug (should be detected first)! + let xdebug = extensions.first! + #expect(xdebug.name == "xdebug") + #expect(xdebug.enabled == true) + await xdebug.toggle() + #expect(xdebug.enabled == false) + + // Check if the file contains the appropriate data + let file = try! String(contentsOf: destination, encoding: .utf8) + #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) + } +} diff --git a/tests/unit/_ST/Parsers/ValetConfigurationTest.swift b/tests/unit/_ST/Parsers/ValetConfigurationTest.swift index d251cc8..a14904f 100644 --- a/tests/unit/_ST/Parsers/ValetConfigurationTest.swift +++ b/tests/unit/_ST/Parsers/ValetConfigurationTest.swift @@ -9,7 +9,6 @@ import Testing import Foundation -@Suite("Parsers") struct ValetConfigurationTest { static var jsonConfigFileUrl: URL { return TestBundle.url( @@ -18,8 +17,7 @@ struct ValetConfigurationTest { )! } - @Test("Can load config file") - func can_load_config_file() throws { + @Test func can_load_config_file() throws { let json = try? String( contentsOf: Self.jsonConfigFileUrl, encoding: .utf8 @@ -37,5 +35,4 @@ struct ValetConfigurationTest { #expect(config.defaultSite == "/Users/username/default-site") #expect(config.loopback == "127.0.0.1") } - } diff --git a/tests/unit/_ST/Parsers/ValetRcTest.swift b/tests/unit/_ST/Parsers/ValetRcTest.swift new file mode 100644 index 0000000..ac0b981 --- /dev/null +++ b/tests/unit/_ST/Parsers/ValetRcTest.swift @@ -0,0 +1,45 @@ +// +// ValetRcTest.swift +// Unit Tests +// +// Created by Nico Verbruggen on 20/01/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Testing +import Foundation + +struct ValetRcTest { + // MARK: - Test Files + static var validPath: URL { + return TestBundle.url(forResource: "valetrc", withExtension: "valid")! + } + + static var brokenPath: URL { + return TestBundle.url(forResource: "valetrc", withExtension: "broken")! + } + + // MARK: - Tests + @Test func test_can_extract_fields_from_valet_rc_file() throws { + let fakeFile = RCFile.fromPath("/Users/fake/file.rc") + #expect(nil == fakeFile) + + // Can parse the file + let validFile = RCFile.fromPath(ValetRcTest.validPath.path) + #expect(nil != validFile) + + let fields = validFile!.fields + + // Correctly parses and trims (and omits double quotes) per line + #expect(fields["PHP"] == "php@8.2") + #expect(fields["OTHER"] == "thing") + #expect(fields["PHPMON_WATCH"] == "true") + #expect(fields["SYNTAX"] == "variable") + + // Ignores entries prefixed with # + #expect(!fields.keys.contains("#PHP")) + + // Ignores invalid lines + #expect(!fields.keys.contains("OOF")) + } +} From ceff52ed11c4c500cf95328cdae35db78e82f429 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Mon, 29 Sep 2025 17:12:43 +0200 Subject: [PATCH 27/33] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Migrate=20more=20tes?= =?UTF-8?q?ts=20to=20Swift=20Testing=20(2/=3F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 30 +++------- tests/unit/Parsers/CaskFileParserTest.swift | 53 ------------------ .../{Config => }/BytePhpPreferenceTest.swift | 4 +- .../unit/_ST/Parsers/CaskFileParserTest.swift | 55 +++++++++++++++++++ .../Parsers/ExtensionEnumeratorTest.swift | 25 ++++----- .../_ST/Parsers/HomebrewPackageTest.swift | 14 ++--- .../Parsers/HomebrewUpgradableTest.swift | 32 +++++++---- .../_ST/Parsers/NginxConfigurationTest.swift | 10 ++-- .../PhpConfigurationFileTest.swift | 8 +-- tests/unit/_ST/Parsers/PhpExtensionTest.swift | 10 ++-- .../_ST/Parsers/ValetConfigurationTest.swift | 5 +- tests/unit/_ST/Parsers/ValetRcTest.swift | 6 +- 12 files changed, 119 insertions(+), 133 deletions(-) delete mode 100644 tests/unit/Parsers/CaskFileParserTest.swift rename tests/unit/_ST/Parsers/{Config => }/BytePhpPreferenceTest.swift (90%) create mode 100644 tests/unit/_ST/Parsers/CaskFileParserTest.swift rename tests/unit/{ => _ST}/Parsers/ExtensionEnumeratorTest.swift (54%) rename tests/unit/{ => _ST}/Parsers/HomebrewUpgradableTest.swift (53%) rename tests/unit/_ST/Parsers/{Config => }/PhpConfigurationFileTest.swift (91%) diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 67b0ccb..9ac1db4 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -1360,12 +1360,16 @@ 036C3A232E5CBC57008DAEDF /* Parsers */ = { isa = PBXGroup; children = ( - C4AF9F76275447F100D44ED0 /* ValetConfigurationTest.swift */, - C4F780AD25D80B37000DBC97 /* PhpExtensionTest.swift */, + C456A0D12AA6179D0080144F /* BytePhpPreferenceTest.swift */, + C40934AA298EEDA900D25014 /* CaskFileParserTest.swift */, + C4F520662AF03791006787F2 /* ExtensionEnumeratorTest.swift */, C43A8A2325D9D20D00591B77 /* HomebrewPackageTest.swift */, + C4AFC4B229C4F43300BF4E0D /* HomebrewUpgradableTest.swift */, C42CFB1927DFE8BD00862737 /* NginxConfigurationTest.swift */, + C46FA98A2822F08F00D78807 /* PhpConfigurationFileTest.swift */, + C4F780AD25D80B37000DBC97 /* PhpExtensionTest.swift */, + C4AF9F76275447F100D44ED0 /* ValetConfigurationTest.swift */, C4551656297AED18009B8466 /* ValetRcTest.swift */, - C456A0D02AA6175D0080144F /* Config */, ); path = Parsers; sourceTree = ""; @@ -1852,15 +1856,6 @@ path = "PHP Extensions"; sourceTree = ""; }; - C456A0D02AA6175D0080144F /* Config */ = { - isa = PBXGroup; - children = ( - C46FA98A2822F08F00D78807 /* PhpConfigurationFileTest.swift */, - C456A0D12AA6179D0080144F /* BytePhpPreferenceTest.swift */, - ); - path = Config; - sourceTree = ""; - }; C459B4BE27F6093A00E9B4B4 /* nginx */ = { isa = PBXGroup; children = ( @@ -2190,16 +2185,6 @@ path = Nginx; sourceTree = ""; }; - C4C1019727C65A11001FACC2 /* Parsers */ = { - isa = PBXGroup; - children = ( - C40934AA298EEDA900D25014 /* CaskFileParserTest.swift */, - C4AFC4B229C4F43300BF4E0D /* HomebrewUpgradableTest.swift */, - C4F520662AF03791006787F2 /* ExtensionEnumeratorTest.swift */, - ); - path = Parsers; - sourceTree = ""; - }; C4C1019827C65A1A001FACC2 /* Versions */ = { isa = PBXGroup; children = ( @@ -2344,7 +2329,6 @@ C471E6D928F9AFC20021E251 /* Testables */, C40C7F1C27720E1400DDDCDC /* Test Files */, C4C1019827C65A1A001FACC2 /* Versions */, - C4C1019727C65A11001FACC2 /* Parsers */, ); path = unit; sourceTree = ""; diff --git a/tests/unit/Parsers/CaskFileParserTest.swift b/tests/unit/Parsers/CaskFileParserTest.swift deleted file mode 100644 index bd783dd..0000000 --- a/tests/unit/Parsers/CaskFileParserTest.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// CaskFileParserTest.swift -// Unit Tests -// -// Created by Nico Verbruggen on 04/02/2023. -// Copyright © 2023 Nico Verbruggen. All rights reserved. -// - -import XCTest - -class CaskFileParserTest: XCTestCase { - - // MARK: - Test Files - static var exampleFilePath: URL { - return Bundle(for: Self.self) - .url(forResource: "phpmon-dev", withExtension: "rb")! - } - - func test_can_extract_fields_from_cask_file() async throws { - guard let caskFile = await CaskFile.from(url: CaskFileParserTest.exampleFilePath) else { - return XCTFail("The CaskFile could not be parsed, check the log for more info") - } - - XCTAssertEqual( - caskFile.version, - "5.7.2_1035" - ) - XCTAssertEqual( - caskFile.sha256, - "1cb147bd1b1fbd52971d90dff577465b644aee7c878f15ede57f46e8f217067a" - ) - XCTAssertEqual( - caskFile.name, - "PHP Monitor DEV" - ) - XCTAssertEqual( - caskFile.url, - "https://github.com/nicoverbruggen/phpmon/releases/download/v5.7.2/phpmon-dev.zip" - ) - } - - func test_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 { - return XCTFail("The remote CaskFile could not be parsed, check the log for more info") - } - - XCTAssertTrue(caskFile.properties.keys.contains("version")) - XCTAssertTrue(caskFile.properties.keys.contains("homepage")) - XCTAssertTrue(caskFile.properties.keys.contains("url")) - } -} diff --git a/tests/unit/_ST/Parsers/Config/BytePhpPreferenceTest.swift b/tests/unit/_ST/Parsers/BytePhpPreferenceTest.swift similarity index 90% rename from tests/unit/_ST/Parsers/Config/BytePhpPreferenceTest.swift rename to tests/unit/_ST/Parsers/BytePhpPreferenceTest.swift index 319c0e9..0fe1499 100644 --- a/tests/unit/_ST/Parsers/Config/BytePhpPreferenceTest.swift +++ b/tests/unit/_ST/Parsers/BytePhpPreferenceTest.swift @@ -9,7 +9,7 @@ import Testing struct BytePhpPreferenceTest { - @Test func test_can_extract_memory_value() throws { + @Test func can_extract_memory_value() throws { let pref = BytePhpPreference(key: "memory_limit") #expect(pref.internalValue == "512M") @@ -17,7 +17,7 @@ struct BytePhpPreferenceTest { #expect(pref.value == 512) } - @Test func test_can_parse_all_kinds_of_values() throws { + @Test func can_parse_all_kinds_of_values() throws { var (unit, value) = BytePhpPreference.readFrom(internalValue: "1G")! #expect(unit == .gigabyte) #expect(value == 1) diff --git a/tests/unit/_ST/Parsers/CaskFileParserTest.swift b/tests/unit/_ST/Parsers/CaskFileParserTest.swift new file mode 100644 index 0000000..e24581f --- /dev/null +++ b/tests/unit/_ST/Parsers/CaskFileParserTest.swift @@ -0,0 +1,55 @@ +// +// CaskFileParserTest.swift +// Unit Tests +// +// Created by Nico Verbruggen on 04/02/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Testing +import Foundation + +struct CaskFileParserTest { + + // MARK: - Test Files + static var exampleFilePath: URL { + TestBundle.url(forResource: "phpmon-dev", withExtension: "rb")! + } + + @Test func can_extract_fields_from_cask_file() async throws { + guard let caskFile = await CaskFile.from(url: CaskFileParserTest.exampleFilePath) else { + Issue.record("The CaskFile could not be parsed, check the log for more info") + return + } + + #expect( + caskFile.version == + "5.7.2_1035" + ) + #expect( + caskFile.sha256 == + "1cb147bd1b1fbd52971d90dff577465b644aee7c878f15ede57f46e8f217067a" + ) + #expect( + caskFile.name == + "PHP Monitor DEV" + ) + #expect( + caskFile.url == + "https://github.com/nicoverbruggen/phpmon/releases/download/v5.7.2/phpmon-dev.zip" + ) + } + + @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 { + Issue.record("The remote CaskFile could not be parsed, check the log for more info") + return + } + + #expect(caskFile.properties.keys.contains("version")) + #expect(caskFile.properties.keys.contains("homepage")) + #expect(caskFile.properties.keys.contains("url")) + } +} diff --git a/tests/unit/Parsers/ExtensionEnumeratorTest.swift b/tests/unit/_ST/Parsers/ExtensionEnumeratorTest.swift similarity index 54% rename from tests/unit/Parsers/ExtensionEnumeratorTest.swift rename to tests/unit/_ST/Parsers/ExtensionEnumeratorTest.swift index 774d9a3..2408cb4 100644 --- a/tests/unit/Parsers/ExtensionEnumeratorTest.swift +++ b/tests/unit/_ST/Parsers/ExtensionEnumeratorTest.swift @@ -6,11 +6,11 @@ // Copyright © 2023 Nico Verbruggen. All rights reserved. // -import XCTest +import Testing +import Foundation -final class ExtensionEnumeratorTest: XCTestCase { - - override func setUp() async throws { +struct ExtensionEnumeratorTest { + init() async throws { ActiveFileSystem.useTestable([ "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.1.rb": .fake(.text, ""), "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.2.rb": .fake(.text, ""), @@ -19,22 +19,19 @@ final class ExtensionEnumeratorTest: XCTestCase { ]) } - func testCanReadFormulae() throws { + @Test func can_read_formulae() throws { let directory = "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula" let files = try FileSystem.getShallowContentsOfDirectory(directory) - XCTAssertEqual( - Set(["xdebug@8.1.rb", "xdebug@8.2.rb", "xdebug@8.3.rb", "xdebug@8.4.rb"]), - Set(files) - ) + #expect(Set(files) == Set(["xdebug@8.1.rb", "xdebug@8.2.rb", "xdebug@8.3.rb", "xdebug@8.4.rb"])) } - func testCanParseFormulaeBasedOnSyntax() throws { + @Test func can_parse_formulae_based_on_syntax() throws { let formulae = BrewTapFormulae.from(tap: "shivammathur/homebrew-extensions") - XCTAssertEqual(formulae["8.1"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.1")]) - XCTAssertEqual(formulae["8.2"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.2")]) - XCTAssertEqual(formulae["8.3"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.3")]) - XCTAssertEqual(formulae["8.4"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.4")]) + #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")]) } } diff --git a/tests/unit/_ST/Parsers/HomebrewPackageTest.swift b/tests/unit/_ST/Parsers/HomebrewPackageTest.swift index eb5627a..9f4e3ba 100644 --- a/tests/unit/_ST/Parsers/HomebrewPackageTest.swift +++ b/tests/unit/_ST/Parsers/HomebrewPackageTest.swift @@ -14,16 +14,14 @@ struct HomebrewPackageTest { // - MARK: SYNTHETIC TESTS static var jsonBrewFile: URL { - return TestBundle - .url(forResource: "brew-formula", withExtension: "json")! + TestBundle.url(forResource: "brew-formula", withExtension: "json")! } static var jsonBrewServicesFile: URL { - return TestBundle - .url(forResource: "brew-services", withExtension: "json")! + TestBundle.url(forResource: "brew-services", withExtension: "json")! } - @Test func test_can_load_extension_json() throws { + @Test func can_load_extension_json() throws { let json = try! String(contentsOf: Self.jsonBrewFile, encoding: .utf8) let package = try! JSONDecoder().decode( [HomebrewPackage].self, from: json.data(using: .utf8)! @@ -36,7 +34,7 @@ struct HomebrewPackageTest { }) == true) } - @Test func test_can_parse_services_json() throws { + @Test func can_parse_services_json() throws { let json = try! String(contentsOf: Self.jsonBrewServicesFile, encoding: .utf8) let services = try! JSONDecoder().decode( [HomebrewService].self, from: json.data(using: .utf8)! @@ -54,7 +52,7 @@ struct HomebrewPackageTest { /// If this test fails, there is an issue with your Homebrew installation /// or the JSON API of the Homebrew output may have changed. @Test(.disabled("Uses system command; enable at your own risk")) - func test_can_parse_services_json_from_cli_output() async throws { + func can_parse_services_json_from_cli_output() async throws { ActiveShell.useSystem() let services = try! JSONDecoder().decode( @@ -77,7 +75,7 @@ struct HomebrewPackageTest { /// If this test fails, there is an issue with your Homebrew installation /// or the JSON API of the Homebrew output may have changed. @Test(.disabled("Uses system command; enable at your own risk")) - func test_can_load_extension_json_from_cli_output() async throws { + func can_load_extension_json_from_cli_output() async throws { ActiveShell.useSystem() diff --git a/tests/unit/Parsers/HomebrewUpgradableTest.swift b/tests/unit/_ST/Parsers/HomebrewUpgradableTest.swift similarity index 53% rename from tests/unit/Parsers/HomebrewUpgradableTest.swift rename to tests/unit/_ST/Parsers/HomebrewUpgradableTest.swift index 6be575d..1413260 100644 --- a/tests/unit/Parsers/HomebrewUpgradableTest.swift +++ b/tests/unit/_ST/Parsers/HomebrewUpgradableTest.swift @@ -6,26 +6,34 @@ // Copyright © 2023 Nico Verbruggen. All rights reserved. // -import XCTest +import Testing +import Foundation -class HomebrewUpgradableTest: XCTestCase { +struct HomebrewUpgradableTest { static var outdatedFileUrl: URL { - return Bundle(for: Self.self) - .url(forResource: "brew-outdated", withExtension: "json")! + return TestBundle.url(forResource: "brew-outdated", withExtension: "json")! } - func test_upgradable_php_versions_can_be_parsed() 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)), + : .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"), + : .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"), + : .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"), + : .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") + : .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 @@ -39,11 +47,11 @@ class HomebrewUpgradableTest: XCTestCase { let data = await BrewPhpFormulaeHandler().loadPhpVersions(loadOutdated: true) - XCTAssertTrue(data.contains(where: { formula in + #expect(true == data.contains(where: { formula in formula.installedVersion == "8.1.16" && formula.upgradeVersion == "8.1.17" })) - XCTAssertTrue(data.contains(where: { formula in + #expect(true == data.contains(where: { formula in formula.installedVersion == "8.2.3" && formula.upgradeVersion == "8.2.4" })) } diff --git a/tests/unit/_ST/Parsers/NginxConfigurationTest.swift b/tests/unit/_ST/Parsers/NginxConfigurationTest.swift index a188638..d4a0ed7 100644 --- a/tests/unit/_ST/Parsers/NginxConfigurationTest.swift +++ b/tests/unit/_ST/Parsers/NginxConfigurationTest.swift @@ -35,17 +35,17 @@ struct NginxConfigurationTest { // MARK: - Tests - @Test func test_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("test" == NginxConfigurationFile.from(filePath: Self.regularUrl.path)?.tld) } - @Test func test_can_determine_isolation() throws { + @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) } - @Test func test_can_determine_proxy() throws { + @Test func can_determine_proxy() throws { let proxied = NginxConfigurationFile.from(filePath: Self.proxyUrl.path)! #expect(proxied.contents.contains("# valet stub: proxy.valet.conf")) #expect("http://127.0.0.1:90" == proxied.proxy) @@ -55,13 +55,13 @@ struct NginxConfigurationTest { #expect(nil == normal.proxy) } - @Test func test_can_determine_secured_proxy() throws { + @Test func can_determine_secured_proxy() throws { let proxied = NginxConfigurationFile.from(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 test_can_determine_proxy_with_custom_tld() throws { + @Test func can_determine_proxy_with_custom_tld() throws { let proxied = NginxConfigurationFile.from(filePath: Self.customTldProxyUrl.path)! #expect(proxied.contents.contains("# valet stub: secure.proxy.valet.conf")) #expect("http://localhost:8080" == proxied.proxy) diff --git a/tests/unit/_ST/Parsers/Config/PhpConfigurationFileTest.swift b/tests/unit/_ST/Parsers/PhpConfigurationFileTest.swift similarity index 91% rename from tests/unit/_ST/Parsers/Config/PhpConfigurationFileTest.swift rename to tests/unit/_ST/Parsers/PhpConfigurationFileTest.swift index 6e2cc49..0f0f8b1 100644 --- a/tests/unit/_ST/Parsers/Config/PhpConfigurationFileTest.swift +++ b/tests/unit/_ST/Parsers/PhpConfigurationFileTest.swift @@ -15,14 +15,14 @@ class PhpConfigurationFileTest { return TestBundle.url(forResource: "php", withExtension: "ini")! } - @Test func test_can_load_extension() throws { + @Test func can_load_extension() throws { let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path) #expect(iniFile != nil) #expect(!iniFile!.extensions.isEmpty) } - @Test func test_can_check_key_existence() throws { + @Test func can_check_key_existence() throws { let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)! #expect(iniFile.has(key: "error_reporting")) @@ -30,7 +30,7 @@ class PhpConfigurationFileTest { #expect(false == iniFile.has(key: "my_unknown_key")) } - @Test func test_can_check_key_value() throws { + @Test func can_check_key_value() throws { let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)! #expect(iniFile.get(for: "error_reporting") != nil) @@ -40,7 +40,7 @@ class PhpConfigurationFileTest { #expect(iniFile.get(for: "display_errors") == "On") } - @Test func test_can_customize_configuration_value() throws { + @Test func can_customize_configuration_value() throws { let destination = Utility .copyToTemporaryFile(resourceName: "php", fileExtension: "ini")! diff --git a/tests/unit/_ST/Parsers/PhpExtensionTest.swift b/tests/unit/_ST/Parsers/PhpExtensionTest.swift index c81b296..fc49bfb 100644 --- a/tests/unit/_ST/Parsers/PhpExtensionTest.swift +++ b/tests/unit/_ST/Parsers/PhpExtensionTest.swift @@ -11,16 +11,16 @@ import Foundation struct PhpExtensionTest { static var phpIniFileUrl: URL { - return TestBundle.url(forResource: "php", withExtension: "ini")! + TestBundle.url(forResource: "php", withExtension: "ini")! } - @Test func test_can_load_extension() throws { + @Test func can_load_extension() throws { let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path) #expect(!extensions.isEmpty) } - @Test func test_extension_name_is_correct() throws { + @Test func extension_name_is_correct() throws { let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path) let extensionNames = extensions.map { (ext) -> String in @@ -39,7 +39,7 @@ struct PhpExtensionTest { #expect(extensionNames.contains("nice") == false) } - @Test func test_extension_status_is_correct() throws { + @Test func extension_status_is_correct() throws { let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path) // xdebug should be enabled @@ -49,7 +49,7 @@ struct PhpExtensionTest { #expect(extensions[1].enabled == false) } - @Test func test_toggle_works_as_expected() async throws { + @Test func toggle_works_as_expected() async throws { let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")! let extensions = PhpExtension.from(filePath: destination.path) #expect(extensions.count == 6) diff --git a/tests/unit/_ST/Parsers/ValetConfigurationTest.swift b/tests/unit/_ST/Parsers/ValetConfigurationTest.swift index a14904f..5dd2d18 100644 --- a/tests/unit/_ST/Parsers/ValetConfigurationTest.swift +++ b/tests/unit/_ST/Parsers/ValetConfigurationTest.swift @@ -11,10 +11,7 @@ import Foundation struct ValetConfigurationTest { static var jsonConfigFileUrl: URL { - return TestBundle.url( - forResource: "valet-config", - withExtension: "json" - )! + TestBundle.url(forResource: "valet-config", withExtension: "json")! } @Test func can_load_config_file() throws { diff --git a/tests/unit/_ST/Parsers/ValetRcTest.swift b/tests/unit/_ST/Parsers/ValetRcTest.swift index ac0b981..675d658 100644 --- a/tests/unit/_ST/Parsers/ValetRcTest.swift +++ b/tests/unit/_ST/Parsers/ValetRcTest.swift @@ -12,15 +12,15 @@ import Foundation struct ValetRcTest { // MARK: - Test Files static var validPath: URL { - return TestBundle.url(forResource: "valetrc", withExtension: "valid")! + TestBundle.url(forResource: "valetrc", withExtension: "valid")! } static var brokenPath: URL { - return TestBundle.url(forResource: "valetrc", withExtension: "broken")! + TestBundle.url(forResource: "valetrc", withExtension: "broken")! } // MARK: - Tests - @Test func test_can_extract_fields_from_valet_rc_file() throws { + @Test func can_extract_fields_from_valet_rc_file() throws { let fakeFile = RCFile.fromPath("/Users/fake/file.rc") #expect(nil == fakeFile) From db8df8575d393c63c9a65facb3dc1728bbea25ae Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Mon, 29 Sep 2025 17:59:33 +0200 Subject: [PATCH 28/33] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Migrate=20more=20tes?= =?UTF-8?q?ts=20to=20Swift=20Testing=20(3/=3F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 30 ++--- .../Common/Testables/TestableFileSystem.swift | 8 +- .../Api/TestableApiTest.swift | 0 .../Commands/CommandTest.swift | 0 .../Integration/PackagistTest.swift | 0 .../Parsers/BytePhpPreferenceTest.swift | 0 .../Parsers/CaskFileParserTest.swift | 0 .../Parsers/ExtensionEnumeratorTest.swift | 0 .../Parsers/HomebrewPackageTest.swift | 0 .../Parsers/HomebrewUpgradableTest.swift | 0 .../Parsers/NginxConfigurationTest.swift | 0 .../Parsers/PhpConfigurationFileTest.swift | 0 .../Parsers/PhpExtensionTest.swift | 0 .../Parsers/ValetConfigurationTest.swift | 0 .../Parsers/ValetRcTest.swift | 0 .../Filesystem/RealFileSystemTest.swift | 125 +++++++++--------- .../Filesystem/TestableFileSystemTest.swift | 117 ++++++++++++++++ .../Testables/Shell/RealShellTest.swift | 81 ++++++++++++ .../Testables/Shell/TestableShellTest.swift | 28 ++-- .../Testables/TestableConfigurationTest.swift | 12 +- .../Filesystem/TestableFileSystemTest.swift | 115 ---------------- .../unit/Testables/Shell/RealShellTest.swift | 87 ------------ 22 files changed, 304 insertions(+), 299 deletions(-) rename tests/unit/{_ST => SwiftTestMigrated}/Api/TestableApiTest.swift (100%) rename tests/unit/{_ST => SwiftTestMigrated}/Commands/CommandTest.swift (100%) rename tests/unit/{_ST => SwiftTestMigrated}/Integration/PackagistTest.swift (100%) rename tests/unit/{_ST => SwiftTestMigrated}/Parsers/BytePhpPreferenceTest.swift (100%) rename tests/unit/{_ST => SwiftTestMigrated}/Parsers/CaskFileParserTest.swift (100%) rename tests/unit/{_ST => SwiftTestMigrated}/Parsers/ExtensionEnumeratorTest.swift (100%) rename tests/unit/{_ST => SwiftTestMigrated}/Parsers/HomebrewPackageTest.swift (100%) rename tests/unit/{_ST => SwiftTestMigrated}/Parsers/HomebrewUpgradableTest.swift (100%) rename tests/unit/{_ST => SwiftTestMigrated}/Parsers/NginxConfigurationTest.swift (100%) rename tests/unit/{_ST => SwiftTestMigrated}/Parsers/PhpConfigurationFileTest.swift (100%) rename tests/unit/{_ST => SwiftTestMigrated}/Parsers/PhpExtensionTest.swift (100%) rename tests/unit/{_ST => SwiftTestMigrated}/Parsers/ValetConfigurationTest.swift (100%) rename tests/unit/{_ST => SwiftTestMigrated}/Parsers/ValetRcTest.swift (100%) rename tests/unit/{ => SwiftTestMigrated}/Testables/Filesystem/RealFileSystemTest.swift (57%) create mode 100644 tests/unit/SwiftTestMigrated/Testables/Filesystem/TestableFileSystemTest.swift create mode 100644 tests/unit/SwiftTestMigrated/Testables/Shell/RealShellTest.swift rename tests/unit/{ => SwiftTestMigrated}/Testables/Shell/TestableShellTest.swift (66%) rename tests/unit/{ => SwiftTestMigrated}/Testables/TestableConfigurationTest.swift (68%) delete mode 100644 tests/unit/Testables/Filesystem/TestableFileSystemTest.swift delete mode 100644 tests/unit/Testables/Shell/RealShellTest.swift diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 9ac1db4..99fd8c0 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -1346,15 +1346,16 @@ path = Integration; sourceTree = ""; }; - 036C3A222E5CBC33008DAEDF /* _ST */ = { + 036C3A222E5CBC33008DAEDF /* SwiftTestMigrated */ = { isa = PBXGroup; children = ( 039C291E2E8AA39B007F5FAB /* Api */, C4C1019927C65A4D001FACC2 /* Commands */, - 036C3A232E5CBC57008DAEDF /* Parsers */, 036C39062E5C8890008DAEDF /* Integration */, + 036C3A232E5CBC57008DAEDF /* Parsers */, + 03D53E902E8AE089001B1671 /* Testables */, ); - path = _ST; + path = SwiftTestMigrated; sourceTree = ""; }; 036C3A232E5CBC57008DAEDF /* Parsers */ = { @@ -1408,6 +1409,16 @@ path = Provision; sourceTree = ""; }; + 03D53E902E8AE089001B1671 /* Testables */ = { + isa = PBXGroup; + children = ( + C4E2E84628FC1D8C003B070C /* TestableConfigurationTest.swift */, + C413E43328DA3E8F00AE33C7 /* Shell */, + C471E6DA28F9AFCB0021E251 /* Filesystem */, + ); + path = Testables; + sourceTree = ""; + }; 5420395726135DB800FB00FA /* Preferences */ = { isa = PBXGroup; children = ( @@ -1935,16 +1946,6 @@ path = "Domain List"; sourceTree = ""; }; - C471E6D928F9AFC20021E251 /* Testables */ = { - isa = PBXGroup; - children = ( - C471E6DA28F9AFCB0021E251 /* Filesystem */, - C413E43328DA3E8F00AE33C7 /* Shell */, - C4E2E84628FC1D8C003B070C /* TestableConfigurationTest.swift */, - ); - path = Testables; - sourceTree = ""; - }; C471E6DA28F9AFCB0021E251 /* Filesystem */ = { isa = PBXGroup; children = ( @@ -2325,9 +2326,8 @@ C4F7807A25D7F84B000DBC97 /* unit */ = { isa = PBXGroup; children = ( - 036C3A222E5CBC33008DAEDF /* _ST */, - C471E6D928F9AFC20021E251 /* Testables */, C40C7F1C27720E1400DDDCDC /* Test Files */, + 036C3A222E5CBC33008DAEDF /* SwiftTestMigrated */, C4C1019827C65A1A001FACC2 /* Versions */, ); path = unit; diff --git a/phpmon/Common/Testables/TestableFileSystem.swift b/phpmon/Common/Testables/TestableFileSystem.swift index 4cb0dca..7a24e3c 100644 --- a/phpmon/Common/Testables/TestableFileSystem.swift +++ b/phpmon/Common/Testables/TestableFileSystem.swift @@ -23,11 +23,11 @@ class TestableFileSystem: FileSystemProtocol { let adjustedKey = key.contains("~") ? key.replacingOccurrences(of: "~", with: self.homeDirectory) : key self.files[adjustedKey] = value } - } - // Ensure that intermediate directories are created - for file in self.files { - self.createIntermediateDirectories(file.key) + // Ensure that intermediate directories are created + for file in self.files { + self.createIntermediateDirectories(file.key) + } } } diff --git a/tests/unit/_ST/Api/TestableApiTest.swift b/tests/unit/SwiftTestMigrated/Api/TestableApiTest.swift similarity index 100% rename from tests/unit/_ST/Api/TestableApiTest.swift rename to tests/unit/SwiftTestMigrated/Api/TestableApiTest.swift diff --git a/tests/unit/_ST/Commands/CommandTest.swift b/tests/unit/SwiftTestMigrated/Commands/CommandTest.swift similarity index 100% rename from tests/unit/_ST/Commands/CommandTest.swift rename to tests/unit/SwiftTestMigrated/Commands/CommandTest.swift diff --git a/tests/unit/_ST/Integration/PackagistTest.swift b/tests/unit/SwiftTestMigrated/Integration/PackagistTest.swift similarity index 100% rename from tests/unit/_ST/Integration/PackagistTest.swift rename to tests/unit/SwiftTestMigrated/Integration/PackagistTest.swift diff --git a/tests/unit/_ST/Parsers/BytePhpPreferenceTest.swift b/tests/unit/SwiftTestMigrated/Parsers/BytePhpPreferenceTest.swift similarity index 100% rename from tests/unit/_ST/Parsers/BytePhpPreferenceTest.swift rename to tests/unit/SwiftTestMigrated/Parsers/BytePhpPreferenceTest.swift diff --git a/tests/unit/_ST/Parsers/CaskFileParserTest.swift b/tests/unit/SwiftTestMigrated/Parsers/CaskFileParserTest.swift similarity index 100% rename from tests/unit/_ST/Parsers/CaskFileParserTest.swift rename to tests/unit/SwiftTestMigrated/Parsers/CaskFileParserTest.swift diff --git a/tests/unit/_ST/Parsers/ExtensionEnumeratorTest.swift b/tests/unit/SwiftTestMigrated/Parsers/ExtensionEnumeratorTest.swift similarity index 100% rename from tests/unit/_ST/Parsers/ExtensionEnumeratorTest.swift rename to tests/unit/SwiftTestMigrated/Parsers/ExtensionEnumeratorTest.swift diff --git a/tests/unit/_ST/Parsers/HomebrewPackageTest.swift b/tests/unit/SwiftTestMigrated/Parsers/HomebrewPackageTest.swift similarity index 100% rename from tests/unit/_ST/Parsers/HomebrewPackageTest.swift rename to tests/unit/SwiftTestMigrated/Parsers/HomebrewPackageTest.swift diff --git a/tests/unit/_ST/Parsers/HomebrewUpgradableTest.swift b/tests/unit/SwiftTestMigrated/Parsers/HomebrewUpgradableTest.swift similarity index 100% rename from tests/unit/_ST/Parsers/HomebrewUpgradableTest.swift rename to tests/unit/SwiftTestMigrated/Parsers/HomebrewUpgradableTest.swift diff --git a/tests/unit/_ST/Parsers/NginxConfigurationTest.swift b/tests/unit/SwiftTestMigrated/Parsers/NginxConfigurationTest.swift similarity index 100% rename from tests/unit/_ST/Parsers/NginxConfigurationTest.swift rename to tests/unit/SwiftTestMigrated/Parsers/NginxConfigurationTest.swift diff --git a/tests/unit/_ST/Parsers/PhpConfigurationFileTest.swift b/tests/unit/SwiftTestMigrated/Parsers/PhpConfigurationFileTest.swift similarity index 100% rename from tests/unit/_ST/Parsers/PhpConfigurationFileTest.swift rename to tests/unit/SwiftTestMigrated/Parsers/PhpConfigurationFileTest.swift diff --git a/tests/unit/_ST/Parsers/PhpExtensionTest.swift b/tests/unit/SwiftTestMigrated/Parsers/PhpExtensionTest.swift similarity index 100% rename from tests/unit/_ST/Parsers/PhpExtensionTest.swift rename to tests/unit/SwiftTestMigrated/Parsers/PhpExtensionTest.swift diff --git a/tests/unit/_ST/Parsers/ValetConfigurationTest.swift b/tests/unit/SwiftTestMigrated/Parsers/ValetConfigurationTest.swift similarity index 100% rename from tests/unit/_ST/Parsers/ValetConfigurationTest.swift rename to tests/unit/SwiftTestMigrated/Parsers/ValetConfigurationTest.swift diff --git a/tests/unit/_ST/Parsers/ValetRcTest.swift b/tests/unit/SwiftTestMigrated/Parsers/ValetRcTest.swift similarity index 100% rename from tests/unit/_ST/Parsers/ValetRcTest.swift rename to tests/unit/SwiftTestMigrated/Parsers/ValetRcTest.swift diff --git a/tests/unit/Testables/Filesystem/RealFileSystemTest.swift b/tests/unit/SwiftTestMigrated/Testables/Filesystem/RealFileSystemTest.swift similarity index 57% rename from tests/unit/Testables/Filesystem/RealFileSystemTest.swift rename to tests/unit/SwiftTestMigrated/Testables/Filesystem/RealFileSystemTest.swift index b43dc0f..bfc354f 100644 --- a/tests/unit/Testables/Filesystem/RealFileSystemTest.swift +++ b/tests/unit/SwiftTestMigrated/Testables/Filesystem/RealFileSystemTest.swift @@ -6,10 +6,12 @@ // Copyright © 2023 Nico Verbruggen. All rights reserved. // -import XCTest +import Testing +import Foundation -class RealFileSystemTest: XCTestCase { - override func setUp() { +@Suite(.serialized) +struct RealFileSystemTest { + init() throws { ActiveFileSystem.useSystem() } @@ -21,45 +23,6 @@ class RealFileSystemTest: XCTestCase { return fullTempDirectoryPath } - func test_testable_fs_is_in_use() { - XCTAssertTrue(FileSystem is RealFileSystem) - } - - func test_temporary_path_exists() { - let temporaryDirectory = self.createUniqueTemporaryDirectory() - - // True - XCTAssertTrue(FileSystem.directoryExists(temporaryDirectory)) - XCTAssertTrue(FileSystem.anyExists(temporaryDirectory)) - - // False - XCTAssertFalse(FileSystem.fileExists(temporaryDirectory)) - } - - func test_directory_can_be_created_symlinked_and_read() { - let temporaryDirectory = self.createUniqueTemporaryDirectory() - - let folderPath = "\(temporaryDirectory)/brew/etc/lib/c" - - try! FileSystem.createDirectory(folderPath, withIntermediateDirectories: true) - - XCTAssertTrue(FileSystem.directoryExists("\(temporaryDirectory)/brew")) - XCTAssertTrue(FileSystem.directoryExists("\(temporaryDirectory)/brew/etc")) - XCTAssertTrue(FileSystem.directoryExists("\(temporaryDirectory)/brew/etc/lib")) - XCTAssertTrue(FileSystem.directoryExists("\(temporaryDirectory)/brew/etc/lib/c")) - - _ = system("ln -s \(temporaryDirectory)/brew/etc/lib/c \(temporaryDirectory)/c") - XCTAssertTrue(FileSystem.directoryExists("\(temporaryDirectory)/c")) - XCTAssertTrue(FileSystem.isSymlink("\(temporaryDirectory)/c")) - XCTAssertEqual( - try! FileSystem.getDestinationOfSymlink("\(temporaryDirectory)/c"), - "\(temporaryDirectory)/brew/etc/lib/c" - ) - - let contents = try! FileSystem.getShallowContentsOfDirectory("\(temporaryDirectory)/brew/etc/lib/c") - XCTAssertEqual([], contents) - } - private func createTestBinaryFile(_ temporaryDirectory: String) -> String { let executablePath = "\(temporaryDirectory)/exec.sh" @@ -71,12 +34,51 @@ class RealFileSystemTest: XCTestCase { return executablePath } - func test_can_read_file_as_text() { + @Test func testable_fs_is_in_use() { + #expect(FileSystem is RealFileSystem) + } + + @Test func temporary_path_exists() { + let temporaryDirectory = self.createUniqueTemporaryDirectory() + + // True + #expect(FileSystem.directoryExists(temporaryDirectory)) + #expect(FileSystem.anyExists(temporaryDirectory)) + + // False + #expect(!FileSystem.fileExists(temporaryDirectory)) + } + + @Test func directory_can_be_created_symlinked_and_read() { + let temporaryDirectory = self.createUniqueTemporaryDirectory() + + let folderPath = "\(temporaryDirectory)/brew/etc/lib/c" + + try! FileSystem.createDirectory(folderPath, withIntermediateDirectories: true) + + #expect(FileSystem.directoryExists("\(temporaryDirectory)/brew")) + #expect(FileSystem.directoryExists("\(temporaryDirectory)/brew/etc")) + #expect(FileSystem.directoryExists("\(temporaryDirectory)/brew/etc/lib")) + #expect(FileSystem.directoryExists("\(temporaryDirectory)/brew/etc/lib/c")) + + _ = system("ln -s \(temporaryDirectory)/brew/etc/lib/c \(temporaryDirectory)/c") + #expect(FileSystem.directoryExists("\(temporaryDirectory)/c")) + #expect(FileSystem.isSymlink("\(temporaryDirectory)/c")) + #expect( + try! FileSystem.getDestinationOfSymlink("\(temporaryDirectory)/c") == + "\(temporaryDirectory)/brew/etc/lib/c" + ) + + let contents = try! FileSystem.getShallowContentsOfDirectory("\(temporaryDirectory)/brew/etc/lib/c") + #expect([] == contents) + } + + @Test func can_read_file_as_text() { let temporaryDirectory = self.createUniqueTemporaryDirectory() let executable = self.createTestBinaryFile(temporaryDirectory) - XCTAssertEqual( - try! FileSystem.getStringFromFile(executable), + #expect( + try! FileSystem.getStringFromFile(executable) == """ !#/bin/bash echo 'Hello world'; @@ -84,50 +86,49 @@ class RealFileSystemTest: XCTestCase { ) } - func test_make_binary_executable() { + @Test func make_binary_executable() { let temporaryDirectory = self.createUniqueTemporaryDirectory() let executable = self.createTestBinaryFile(temporaryDirectory) - XCTAssertTrue(FileSystem.isWriteableFile(executable)) - XCTAssertFalse(FileSystem.isExecutableFile(executable)) + #expect(FileSystem.isWriteableFile(executable)) + #expect(!FileSystem.isExecutableFile(executable)) try! FileSystem.makeExecutable(executable) - XCTAssertTrue(FileSystem.isExecutableFile(executable)) - XCTAssertFalse(FileSystem.isDirectory(executable)) - XCTAssertFalse(FileSystem.isSymlink(executable)) + #expect(FileSystem.isExecutableFile(executable)) + #expect(!FileSystem.isDirectory(executable)) + #expect(!FileSystem.isSymlink(executable)) } - func test_non_existent_file_is_not_symlink_or_directory() { + @Test func non_existent_file_is_not_symlink_or_directory() { let path = "/path/that/does/not/exist" - XCTAssertFalse(FileSystem.isDirectory(path)) - XCTAssertFalse(FileSystem.isSymlink(path)) + #expect(!FileSystem.isDirectory(path)) + #expect(!FileSystem.isSymlink(path)) } - func test_moving_file() { + @Test func moving_file() { let temporaryDirectory = self.createUniqueTemporaryDirectory() let executable = self.createTestBinaryFile(temporaryDirectory) - XCTAssertTrue(FileSystem.fileExists(executable)) + #expect(FileSystem.fileExists(executable)) let newExecutable = executable.replacingOccurrences(of: "/exec.sh", with: "/file.txt") try! FileSystem.move(from: executable, to: newExecutable) - XCTAssertTrue(FileSystem.fileExists(newExecutable)) - XCTAssertFalse(FileSystem.fileExists(executable)) + #expect(FileSystem.fileExists(newExecutable)) + #expect(!FileSystem.fileExists(executable)) } - func test_deleting_file() { + @Test func deleting_file() { let temporaryDirectory = self.createUniqueTemporaryDirectory() let executable = self.createTestBinaryFile(temporaryDirectory) - XCTAssertTrue(FileSystem.fileExists(executable)) + #expect(FileSystem.fileExists(executable)) try! FileSystem.remove(executable) - XCTAssertFalse(FileSystem.fileExists(executable)) + #expect(!FileSystem.fileExists(executable)) } - } diff --git a/tests/unit/SwiftTestMigrated/Testables/Filesystem/TestableFileSystemTest.swift b/tests/unit/SwiftTestMigrated/Testables/Filesystem/TestableFileSystemTest.swift new file mode 100644 index 0000000..6155f68 --- /dev/null +++ b/tests/unit/SwiftTestMigrated/Testables/Filesystem/TestableFileSystemTest.swift @@ -0,0 +1,117 @@ +// +// TestableFileSystemTest.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 01/11/2022. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Testing +import Foundation + +@Suite(.serialized) +struct TestableFileSystemTest { + init() throws { + ActiveFileSystem.useTestable([ + "/home/user/bin/foo": .fake(.binary), + "/home/user/docs": .fake(.symlink, "/home/user/documents"), + "/home/user/documents/script.sh": .fake(.text, "echo 'cool';"), + "/home/user/documents/nice.txt": .fake(.text, "69"), + "/home/user/documents/filters/filter1.txt": .fake(.text, "F1"), + "/home/user/documents/filters/filter2.txt": .fake(.text, "F2") + ]) + } + + @Test func testable_fs_is_in_use() { + #expect(FileSystem is TestableFileSystem) + } + + @Test func intermediate_directories_are_automatically_created() { + #expect(FileSystem.directoryExists("/")) + #expect(FileSystem.directoryExists("/home")) + #expect(FileSystem.directoryExists("/home/user")) + #expect(FileSystem.directoryExists("/home/user/documents")) + #expect(FileSystem.directoryExists("/home/user/bin")) + } + + @Test func binary_directory_is_writable() { + #expect(FileSystem.isWriteableFile("/home/user/bin")) + } + + @Test func binary_exists() { + #expect(FileSystem.isExecutableFile("/home/user/bin/foo")) + } + + @Test func can_write_text_to_executable() throws { + try! FileSystem.writeAtomicallyToFile("/home/user/bin/bar", content: "bar bar bar!") + + #expect(FileSystem.fileExists("/home/user/bin/bar")) + #expect(!FileSystem.isExecutableFile("/home/user/bin/bar")) + + try! FileSystem.makeExecutable("/home/user/bin/bar") + + #expect(FileSystem.isExecutableFile("/home/user/bin/bar")) + } + + @Test func can_create_directory() throws { + try! FileSystem.createDirectory( + "/home/nico/phpmon/config", + withIntermediateDirectories: true + ) + + #expect(FileSystem + .anyExists("/home/nico/phpmon/config")) + #expect(FileSystem.directoryExists("/home/nico/phpmon/config")) + } + + @Test func can_create_nested_directories() throws { + try FileSystem.createDirectory( + "/home/user/thing/epic/nested/directories", + withIntermediateDirectories: true + ) + + #expect(FileSystem.directoryExists("/")) + #expect(FileSystem.directoryExists("/home")) + #expect(FileSystem.directoryExists("/home/user")) + #expect(FileSystem.directoryExists("/home/user/thing")) + #expect(FileSystem.directoryExists("/home/user/thing/epic/nested")) + #expect(FileSystem.directoryExists("/home/user/thing/epic/nested/directories")) + } + + @Test func can_list_directory_contents() throws { + let contents = try! FileSystem.getShallowContentsOfDirectory("/home/user/documents") + + #expect( + contents.sorted() == + [ + "script.sh", + "nice.txt", + "filters" + ].sorted() + ) + } + + @Test func can_delete_directory_recursively() { + #expect(FileSystem.directoryExists("/home/user/documents")) + #expect(FileSystem.directoryExists("/home/user/documents/filters")) + #expect(FileSystem.fileExists("/home/user/documents/filters/filter1.txt")) + + try! FileSystem.remove("/home/user/documents") + + #expect(!FileSystem.directoryExists("/home/user/documents")) + #expect(!FileSystem.directoryExists("/home/user/documents/filters")) + #expect(!FileSystem.fileExists("/home/user/documents/filters/filter1.txt")) + } + + @Test func can_move_directory() { + #expect(FileSystem.directoryExists("/home/user/documents")) + #expect(FileSystem.directoryExists("/home/user/documents/filters")) + #expect(FileSystem.fileExists("/home/user/documents/filters/filter1.txt")) + + try! FileSystem.move(from: "/home/user/documents", to: "/home/user/new") + + #expect(FileSystem.directoryExists("/home/user/new")) + #expect(FileSystem.directoryExists("/home/user/new/filters")) + #expect(FileSystem.fileExists("/home/user/new/filters/filter1.txt")) + } +} diff --git a/tests/unit/SwiftTestMigrated/Testables/Shell/RealShellTest.swift b/tests/unit/SwiftTestMigrated/Testables/Shell/RealShellTest.swift new file mode 100644 index 0000000..e6dd51f --- /dev/null +++ b/tests/unit/SwiftTestMigrated/Testables/Shell/RealShellTest.swift @@ -0,0 +1,81 @@ +// +// RealShellTest.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 28/09/2022. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Testing +import Foundation + +@Suite(.serialized) +struct RealShellTest { + + init() async throws { + // Reset to the default shell + ActiveShell.useSystem() + } + + @Test func system_shell_is_default() async { + #expect(Shell is RealShell) + + let output = await Shell.pipe("php -v") + + #expect(output.out.contains("Copyright (c) The PHP Group")) + } + + @Test func system_shell_can_be_used_synchronously() { + #expect(Shell is RealShell) + + let output = Shell.sync("php -v") + + #expect(output.out.contains("Copyright (c) The PHP Group")) + } + + @Test func system_shell_has_path() { + let systemShell = Shell as! RealShell + + #expect(systemShell.PATH.contains(":/usr/local/bin")) + #expect(systemShell.PATH.contains(":/usr/bin")) + } + + @Test func system_shell_can_buffer_output() async { + var bits: [String] = [] + + let (_, shellOutput) = try! await Shell.attach( + "php -r \"echo 'Hello world' . PHP_EOL; usleep(200); echo 'Goodbye world';\"", + didReceiveOutput: { incoming, _ in + bits.append(incoming) + }, + withTimeout: 2.0 + ) + + #expect(bits.contains("Hello world\n")) + #expect(bits.contains("Goodbye world")) + #expect("Hello world\nGoodbye world" == shellOutput.out) + } + + @Test func system_shell_can_timeout_and_throw_error() async { + await #expect(throws: ShellError.timedOut) { + try await Shell.attach( + "php -r \"sleep(1);\"", + didReceiveOutput: { _, _ in }, + withTimeout: .seconds(0.1) + ) + } + } + + @Test func can_run_multiple_shell_commands_in_parallel() async throws { + let start = ContinuousClock.now + + await withTaskGroup(of: Void.self) { group in + group.addTask { await Shell.quiet("php -r \"usleep(700000);\"") } + group.addTask { await Shell.quiet("php -r \"usleep(700000);\"") } + group.addTask { await Shell.quiet("php -r \"usleep(700000);\"") } + } + + let duration = start.duration(to: .now) + #expect(duration < .milliseconds(1500)) // Should complete in ~700ms if parallel + } +} diff --git a/tests/unit/Testables/Shell/TestableShellTest.swift b/tests/unit/SwiftTestMigrated/Testables/Shell/TestableShellTest.swift similarity index 66% rename from tests/unit/Testables/Shell/TestableShellTest.swift rename to tests/unit/SwiftTestMigrated/Testables/Shell/TestableShellTest.swift index d61ad15..e9659fa 100644 --- a/tests/unit/Testables/Shell/TestableShellTest.swift +++ b/tests/unit/SwiftTestMigrated/Testables/Shell/TestableShellTest.swift @@ -6,10 +6,12 @@ // Copyright © 2023 Nico Verbruggen. All rights reserved. // -import XCTest +import Testing +import Foundation -class TestableShellTest: XCTestCase { - func test_fake_shell_output_can_be_declared() async { +@Suite(.serialized) +struct TestableShellTest { + @Test func fake_shell_output_can_be_declared() async { let greeting = BatchFakeShellOutput(items: [ .instant("Hello world\n"), .delayed(0.3, "Goodbye world") @@ -17,10 +19,10 @@ class TestableShellTest: XCTestCase { let output = await greeting.outputInstantaneously() - XCTAssertEqual("Hello world\nGoodbye world", output.out) + #expect("Hello world\nGoodbye world" == output.out) } - func test_fake_shell_can_output_in_realtime() async { + @Test func fake_shell_can_output_in_realtime() async { let greeting = BatchFakeShellOutput(items: [ .instant("Hello world\n"), .delayed(2, "Goodbye world") @@ -28,10 +30,10 @@ class TestableShellTest: XCTestCase { let output = await greeting.output(didReceiveOutput: { _, _ in }) - XCTAssertEqual("Hello world\nGoodbye world", output.out) + #expect("Hello world\nGoodbye world" == output.out) } - func test_fake_shell_synchronous_output() { + @Test func fake_shell_synchronous_output() { let greeting = BatchFakeShellOutput(items: [ .instant("Hello world\n"), .delayed(0.2, "Goodbye world") @@ -39,10 +41,10 @@ class TestableShellTest: XCTestCase { let output = greeting.syncOutput() - XCTAssertEqual("Hello world\nGoodbye world", output.out) + #expect("Hello world\nGoodbye world" == output.out) } - func test_fake_shell_usage() { + @Test func fake_shell_usage() { let expectedOutput = """ PHP 8.3.0 (cli) (built: Nov 21 2023 14:40:35) (NTS) Copyright (c) The PHP Group @@ -56,13 +58,13 @@ class TestableShellTest: XCTestCase { "echo $PATH": .instant("/Users/user/bin:/opt/homebrew/bin") ]) - XCTAssertEqual(expectedOutput, shell.sync("php -v").out) - XCTAssertEqual("/Users/user/bin:/opt/homebrew/bin", shell.sync("echo $PATH").out) + #expect(expectedOutput == shell.sync("php -v").out) + #expect("/Users/user/bin:/opt/homebrew/bin" == shell.sync("echo $PATH").out) } - func test_fake_shell_has_path() { + @Test func fake_shell_has_path() { ActiveShell.useTestable([:]) - XCTAssertEqual(Shell.PATH, "/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin") + #expect(Shell.PATH == "/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin") } } diff --git a/tests/unit/Testables/TestableConfigurationTest.swift b/tests/unit/SwiftTestMigrated/Testables/TestableConfigurationTest.swift similarity index 68% rename from tests/unit/Testables/TestableConfigurationTest.swift rename to tests/unit/SwiftTestMigrated/Testables/TestableConfigurationTest.swift index 1550544..a57da85 100644 --- a/tests/unit/Testables/TestableConfigurationTest.swift +++ b/tests/unit/SwiftTestMigrated/Testables/TestableConfigurationTest.swift @@ -6,10 +6,11 @@ // Copyright © 2023 Nico Verbruggen. All rights reserved. // -import XCTest +import Testing +import Foundation -class TestableConfigurationTest: XCTestCase { - func test_configuration_can_be_saved_as_json() async { +struct TestableConfigurationTest { + @Test func configuration_can_be_saved_as_json() async { // WORKING var configuration = TestableConfigurations.working @@ -36,5 +37,10 @@ class TestableConfigurationTest: XCTestCase { atomically: true, encoding: .utf8 ) + + // 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")) } } diff --git a/tests/unit/Testables/Filesystem/TestableFileSystemTest.swift b/tests/unit/Testables/Filesystem/TestableFileSystemTest.swift deleted file mode 100644 index 1ec44fb..0000000 --- a/tests/unit/Testables/Filesystem/TestableFileSystemTest.swift +++ /dev/null @@ -1,115 +0,0 @@ -// -// TestableFileSystemTest.swift -// PHP Monitor -// -// Created by Nico Verbruggen on 01/11/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. -// - -import XCTest - -class TestableFileSystemTest: XCTestCase { - - override func setUp() async throws { - ActiveFileSystem.useTestable([ - "/home/user/bin/foo": .fake(.binary), - "/home/user/docs": .fake(.symlink, "/home/user/documents"), - "/home/user/documents/script.sh": .fake(.text, "echo 'cool';"), - "/home/user/documents/nice.txt": .fake(.text, "69"), - "/home/user/documents/filters/filter1.txt": .fake(.text, "F1"), - "/home/user/documents/filters/filter2.txt": .fake(.text, "F2") - ]) - } - - func test_testable_fs_is_in_use() { - XCTAssertTrue(FileSystem is TestableFileSystem) - } - - func test_intermediate_directories_are_automatically_created() { - XCTAssertTrue(FileSystem.directoryExists("/")) - XCTAssertTrue(FileSystem.directoryExists("/home")) - XCTAssertTrue(FileSystem.directoryExists("/home/user")) - XCTAssertTrue(FileSystem.directoryExists("/home/user/documents")) - XCTAssertTrue(FileSystem.directoryExists("/home/user/bin")) - } - - func test_binary_directory_is_writable() { - XCTAssertTrue(FileSystem.isWriteableFile("/home/user/bin")) - } - - func test_binary_exists() { - XCTAssertTrue(FileSystem.isExecutableFile("/home/user/bin/foo")) - } - - func test_can_write_text_to_executable() throws { - try! FileSystem.writeAtomicallyToFile("/home/user/bin/bar", content: "bar bar bar!") - - XCTAssertFalse(FileSystem.isExecutableFile("/home/user/bin/bar")) - - try! FileSystem.makeExecutable("/home/user/bin/bar") - - XCTAssertTrue(FileSystem.isExecutableFile("/home/user/bin/bar")) - } - - func test_can_create_directory() throws { - try! FileSystem.createDirectory( - "/home/nico/phpmon/config", - withIntermediateDirectories: true - ) - - XCTAssertTrue(FileSystem - .anyExists("/home/nico/phpmon/config")) - XCTAssertTrue(FileSystem.directoryExists("/home/nico/phpmon/config")) - } - - func test_can_create_nested_directories() throws { - try FileSystem.createDirectory( - "/home/user/thing/epic/nested/directories", - withIntermediateDirectories: true - ) - - XCTAssertTrue(FileSystem.directoryExists("/")) - XCTAssertTrue(FileSystem.directoryExists("/home")) - XCTAssertTrue(FileSystem.directoryExists("/home/user")) - XCTAssertTrue(FileSystem.directoryExists("/home/user/thing")) - XCTAssertTrue(FileSystem.directoryExists("/home/user/thing/epic/nested")) - XCTAssertTrue(FileSystem.directoryExists("/home/user/thing/epic/nested/directories")) - } - - func test_can_list_directory_contents() throws { - let contents = try! FileSystem.getShallowContentsOfDirectory("/home/user/documents") - - XCTAssertEqual( - contents.sorted(), - [ - "script.sh", - "nice.txt", - "filters" - ].sorted() - ) - } - - func test_can_delete_directory_recursively() { - XCTAssertTrue(FileSystem.directoryExists("/home/user/documents")) - XCTAssertTrue(FileSystem.directoryExists("/home/user/documents/filters")) - XCTAssertTrue(FileSystem.fileExists("/home/user/documents/filters/filter1.txt")) - - try! FileSystem.remove("/home/user/documents") - - XCTAssertFalse(FileSystem.directoryExists("/home/user/documents")) - XCTAssertFalse(FileSystem.directoryExists("/home/user/documents/filters")) - XCTAssertFalse(FileSystem.fileExists("/home/user/documents/filters/filter1.txt")) - } - - func test_can_move_directory() { - XCTAssertTrue(FileSystem.directoryExists("/home/user/documents")) - XCTAssertTrue(FileSystem.directoryExists("/home/user/documents/filters")) - XCTAssertTrue(FileSystem.fileExists("/home/user/documents/filters/filter1.txt")) - - try! FileSystem.move(from: "/home/user/documents", to: "/home/user/new") - - XCTAssertTrue(FileSystem.directoryExists("/home/user/new")) - XCTAssertTrue(FileSystem.directoryExists("/home/user/new/filters")) - XCTAssertTrue(FileSystem.fileExists("/home/user/new/filters/filter1.txt")) - } -} diff --git a/tests/unit/Testables/Shell/RealShellTest.swift b/tests/unit/Testables/Shell/RealShellTest.swift deleted file mode 100644 index 9a7568c..0000000 --- a/tests/unit/Testables/Shell/RealShellTest.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// RealShellTest.swift -// PHP Monitor -// -// Created by Nico Verbruggen on 28/09/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. -// - -import XCTest - -class RealShellTest: XCTestCase { - - override func setUp() async throws { - // Reset to the default shell - ActiveShell.useSystem() - } - - func test_system_shell_is_default() async { - XCTAssertTrue(Shell is RealShell) - - let output = await Shell.pipe("php -v") - - XCTAssertTrue(output.out.contains("Copyright (c) The PHP Group")) - } - - func test_system_shell_can_be_used_synchronously() { - XCTAssertTrue(Shell is RealShell) - - let output = Shell.sync("php -v") - - XCTAssertTrue(output.out.contains("Copyright (c) The PHP Group")) - } - - func test_system_shell_has_path() { - let systemShell = Shell as! RealShell - - XCTAssertTrue(systemShell.PATH.contains(":/usr/local/bin")) - XCTAssertTrue(systemShell.PATH.contains(":/usr/bin")) - } - - func test_system_shell_can_buffer_output() async { - var bits: [String] = [] - - let (_, shellOutput) = try! await Shell.attach( - "php -r \"echo 'Hello world' . PHP_EOL; usleep(200); echo 'Goodbye world';\"", - didReceiveOutput: { incoming, _ in - bits.append(incoming) - }, - withTimeout: 2.0 - ) - - XCTAssertTrue(bits.contains("Hello world\n")) - XCTAssertTrue(bits.contains("Goodbye world")) - XCTAssertEqual("Hello world\nGoodbye world", shellOutput.out) - } - - func test_system_shell_can_timeout_and_throw_error() async { - let expectation = XCTestExpectation(description: #function) - - do { - _ = try await Shell.attach( - "php -r \"sleep(1);\"", - didReceiveOutput: { _, _ in }, - withTimeout: 0.1 - ) - } catch { - XCTAssertEqual(error as? ShellError, ShellError.timedOut) - expectation.fulfill() - } - - await fulfillment(of: [expectation], timeout: 5.0) - } - - func test_system_processes_run_in_parallel() async { - let expectation = XCTestExpectation(description: #function) - - let thing = { - await Shell.quiet("php -r \"usleep(700);\"") - await Shell.quiet("php -r \"usleep(700);\"") - await Shell.quiet("php -r \"usleep(700);\"") - expectation.fulfill() - } - - await thing() - await fulfillment(of: [expectation], timeout: 5.0) - } -} From d2b172fc5263f7cb346ef0bab576699a6ee4078e Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Tue, 30 Sep 2025 11:39:05 +0200 Subject: [PATCH 29/33] =?UTF-8?q?=F0=9F=93=9D=20Update=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 ++++++---- SECURITY.md | 4 +--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 126b601..b7e5389 100644 --- a/README.md +++ b/README.md @@ -399,13 +399,15 @@ PHP Monitor is a universal app and supports both architectures, so [find out her
Why is the app doing network requests? -The app will automatically check for updates, which is the most likely culprit. +This happens for various reasons. -This happens at launch (unless disabled), and the app directly checks the Caskfile hosted on GitHub. This data is not, and will not be used for analytics (and, as far as I can tell, cannot). +PHP Monitor will connect to the `api.phpmon.app` domain to check for updates. To provide a good update experience, some information about which version of PHP Monitor and macOS you are using is transmitted to determine which updates are available for your system configuration. -I also can't prevent `brew` from doing things via the network when PHP Monitor uses the binary. +The app includes an Internet Access Policy file, so if you're using something like [Little Snitch](https://www.obdev.at/products/littlesnitch/index.html) there should be a description why these connections occur. -The app includes an Internet Access Policy file, so if you're using something like Little Snitch there should be a description why these calls occur. +Certain connections may not be documented if Homebrew functionality is being invoked via the GUI. I also can't prevent `brew` from doing things via the network when PHP Monitor invokes `brew`, obviously. + +For example: Homebrew automatically sends analytics to an `influxdata.com` endpoint (more info [here](https://docs.brew.sh/Analytics)). You can disable this by running `brew analytics off`.
diff --git a/SECURITY.md b/SECURITY.md index 162bed8..a8c1530 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,9 +6,7 @@ Generally speaking, only the latest version of **PHP Monitor** is supported, exc | Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Recommended Valet Version | | ------- | ------------- | ------------------ | ----- | ----- | ----- | ---- -| 25 | ✅ Universal binary | ✅ Yes | Ventura (13.5+)
Sonoma (14.0+)
Sequoia (15.0+)
Tahoe (26.0+)* | macOS 13.5+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)
PHP 7.0—PHP 8.4 (w/ Valet 3.x)
PHP 7.1-PHP 8.5 (w/ Valet 4.x)| 3.0 or higher recommended
2.16.2 minimum | - -(*) Denotes preliminary supported based on the app being built with the latest version of the SDK prior to the release of the latest release of macOS. Please check out the pinned issue for more information. +| 25 | ✅ Universal binary | ✅ Yes | Ventura (13.5+)
Sonoma (14.0+)
Sequoia (15.0+)
Tahoe (26.0+) | macOS 13.5+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)
PHP 7.0—PHP 8.4 (w/ Valet 3.x)
PHP 7.1-PHP 8.5 (w/ Valet 4.x)| 3.0 or higher recommended
2.16.2 minimum | ## Legacy versions From 00eed56a3d2712ed6f4ab7cdd754572a48d31489 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Tue, 30 Sep 2025 13:43:33 +0200 Subject: [PATCH 30/33] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Rework=20API=20struc?= =?UTF-8?q?ture,=20update=20check=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 22 ++++++++++++++++++- phpmon/Common/Http/ActiveApi.swift | 4 ---- phpmon/Common/Http/ApiProtocol.swift | 11 ++++++++++ phpmon/Common/Http/RealApi.swift | 9 ++++++++ .../{Http => Testables}/TestableApi.swift | 0 phpmon/Domain/App/UpdateScheduler.swift | 1 - .../Integrations/Homebrew/CaskFile.swift | 4 ++-- 7 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 phpmon/Common/Http/ApiProtocol.swift create mode 100644 phpmon/Common/Http/RealApi.swift rename phpmon/Common/{Http => Testables}/TestableApi.swift (100%) diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 99fd8c0..26d9ccd 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -16,6 +16,14 @@ 03263A392E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; }; 03263A3A2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; }; 03263A3B2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; }; + 032DAC282E8BEB5B0018E01C /* RealApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032DAC272E8BEB590018E01C /* RealApi.swift */; }; + 032DAC292E8BEB5B0018E01C /* RealApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032DAC272E8BEB590018E01C /* RealApi.swift */; }; + 032DAC2A2E8BEB5B0018E01C /* RealApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032DAC272E8BEB590018E01C /* RealApi.swift */; }; + 032DAC2B2E8BEB5B0018E01C /* RealApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032DAC272E8BEB590018E01C /* RealApi.swift */; }; + 032DAC2D2E8BEB6B0018E01C /* ApiProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032DAC2C2E8BEB690018E01C /* ApiProtocol.swift */; }; + 032DAC2E2E8BEB6B0018E01C /* ApiProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032DAC2C2E8BEB690018E01C /* ApiProtocol.swift */; }; + 032DAC2F2E8BEB6B0018E01C /* ApiProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032DAC2C2E8BEB690018E01C /* ApiProtocol.swift */; }; + 032DAC302E8BEB6B0018E01C /* ApiProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032DAC2C2E8BEB690018E01C /* ApiProtocol.swift */; }; 033D45982B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; }; 033D45992B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; }; 033D459A2B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; }; @@ -971,6 +979,8 @@ 0309E6662B0D4B2F002AC007 /* BrewExtensionsObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewExtensionsObservable.swift; sourceTree = ""; }; 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewPhpExtension.swift; sourceTree = ""; }; 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateScheduler.swift; sourceTree = ""; }; + 032DAC272E8BEB590018E01C /* RealApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealApi.swift; sourceTree = ""; }; + 032DAC2C2E8BEB690018E01C /* ApiProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiProtocol.swift; sourceTree = ""; }; 0336CAAF2B0D0CDA009A1034 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallPhpExtensionCommand.swift; sourceTree = ""; }; 033D459D2B0D513900070080 /* RemovePhpExtensionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemovePhpExtensionCommand.swift; sourceTree = ""; }; @@ -1386,8 +1396,9 @@ 039C29112E8AA159007F5FAB /* Http */ = { isa = PBXGroup; children = ( - 039C29172E8AA311007F5FAB /* TestableApi.swift */, 039C29122E8AA15F007F5FAB /* ActiveApi.swift */, + 032DAC272E8BEB590018E01C /* RealApi.swift */, + 032DAC2C2E8BEB690018E01C /* ApiProtocol.swift */, ); path = Http; sourceTree = ""; @@ -2346,6 +2357,7 @@ C4F787A728EF812600790735 /* Testables */ = { isa = PBXGroup; children = ( + 039C29172E8AA311007F5FAB /* TestableApi.swift */, C46EBC4928DB966A007ACC74 /* TestableShell.swift */, C4AD38B128ECD9D300FA8D83 /* TestableFileSystem.swift */, C4E49DEC28F764A00026AC4E /* TestableCommand.swift */, @@ -2707,6 +2719,7 @@ C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */, C4E49DE728F764050026AC4E /* ActiveCommand.swift in Sources */, C43B8FD52BA9BAD3000C02BE /* UnavailableContentView.swift in Sources */, + 032DAC2A2E8BEB5B0018E01C /* RealApi.swift in Sources */, 54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */, C450C8C628C919EC002A2B4B /* PreferenceName.swift in Sources */, C4E4404627C56F4700D225E1 /* ValetSite.swift in Sources */, @@ -2726,6 +2739,7 @@ C4463FCC29804BCB007B93D5 /* RCFile.swift in Sources */, C44264BE2850B86C007400F1 /* SwiftUIHelper.swift in Sources */, C4E9D2C02878B336008FFDAD /* OnboardingView.swift in Sources */, + 032DAC2E2E8BEB6B0018E01C /* ApiProtocol.swift in Sources */, C4F2E4372752F0870020E974 /* BrewDiagnostics.swift in Sources */, 031E2B692B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */, C4AD38B228ECD9D300FA8D83 /* TestableFileSystem.swift in Sources */, @@ -2920,6 +2934,7 @@ C471E84528F9BB650021E251 /* App+GlobalHotkey.swift in Sources */, C4513F922B13E2FB001AD760 /* PhpExtensionManagerView.swift in Sources */, C471E84628F9BB650021E251 /* InterAppHandler.swift in Sources */, + 032DAC2B2E8BEB5B0018E01C /* RealApi.swift in Sources */, C471E84728F9BB650021E251 /* Startup.swift in Sources */, C471E84828F9BB650021E251 /* EnvironmentCheck.swift in Sources */, C471E84A28F9BB650021E251 /* AppVersion.swift in Sources */, @@ -3082,6 +3097,7 @@ C45E2A77291992DA005C7CFD /* FeatureTestCase.swift in Sources */, C471E82028F9BB290021E251 /* NginxConfigurationFile.swift in Sources */, C471E7D428F9BA8F0021E251 /* ActiveFileSystem.swift in Sources */, + 032DAC2D2E8BEB6B0018E01C /* ApiProtocol.swift in Sources */, C4513F902B13E2E6001AD760 /* PhpExtensionManagerWindowController.swift in Sources */, C471E81528F9BAE80021E251 /* ArrayExtension.swift in Sources */, C471E7DA28F9BA8F0021E251 /* TestableCommand.swift in Sources */, @@ -3153,6 +3169,7 @@ C471E8B528F9BB8F0021E251 /* MainMenu+FixMyValet.swift in Sources */, C471E8B628F9BB8F0021E251 /* MainMenu+Actions.swift in Sources */, C471E8B728F9BB8F0021E251 /* StatusMenu.swift in Sources */, + 032DAC302E8BEB6B0018E01C /* ApiProtocol.swift in Sources */, C471E8B828F9BB8F0021E251 /* StatusMenu+Items.swift in Sources */, 036C390C2E5C8CC5008DAEDF /* PackagistP2Response.swift in Sources */, C471E8B928F9BB8F0021E251 /* DomainListCellProtocol.swift in Sources */, @@ -3268,6 +3285,7 @@ C471E7F828F9BACB0021E251 /* InternalSwitcher.swift in Sources */, C471E82328F9BB2E0021E251 /* ComposerJson.swift in Sources */, C471E82128F9BB2E0021E251 /* ProjectTypeDetection.swift in Sources */, + 032DAC282E8BEB5B0018E01C /* RealApi.swift in Sources */, C471E7EF28F9BAC30021E251 /* Actions.swift in Sources */, C471E82228F9BB2E0021E251 /* ComposerWindow.swift in Sources */, C4D3660E29113F20006BD146 /* System.swift in Sources */, @@ -3400,6 +3418,7 @@ C456A0C72AA614BD0080144F /* PhpPreference.swift in Sources */, C42106672AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */, C46DC7A52C7B5BC900F19D17 /* Favorites.swift in Sources */, + 032DAC2F2E8BEB6B0018E01C /* ApiProtocol.swift in Sources */, C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */, C4FC21B128391F8E00D368BB /* MainMenu+Actions.swift in Sources */, 54D9E0B927E4F51E003B9AD9 /* KeyCombo.swift in Sources */, @@ -3407,6 +3426,7 @@ C4159AF728E4D40400545349 /* RealShellTest.swift in Sources */, C450C8C728C919EC002A2B4B /* PreferenceName.swift in Sources */, C40D725B2A018ACC0054A067 /* BusyStatus.swift in Sources */, + 032DAC292E8BEB5B0018E01C /* RealApi.swift in Sources */, C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */, C40D72602A018AE30054A067 /* BrewFormula+UI.swift in Sources */, C485707B28BF458900539B36 /* VersionPopoverView.swift in Sources */, diff --git a/phpmon/Common/Http/ActiveApi.swift b/phpmon/Common/Http/ActiveApi.swift index 245b5c5..de880cf 100644 --- a/phpmon/Common/Http/ActiveApi.swift +++ b/phpmon/Common/Http/ActiveApi.swift @@ -23,7 +23,3 @@ class ActiveApi { Self.shared = RealApi() } } - -protocol ApiProtocol {} - -class RealApi: ApiProtocol {} diff --git a/phpmon/Common/Http/ApiProtocol.swift b/phpmon/Common/Http/ApiProtocol.swift new file mode 100644 index 0000000..a4b525a --- /dev/null +++ b/phpmon/Common/Http/ApiProtocol.swift @@ -0,0 +1,11 @@ +// +// ApiProtocol.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 30/09/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +protocol ApiProtocol { + +} diff --git a/phpmon/Common/Http/RealApi.swift b/phpmon/Common/Http/RealApi.swift new file mode 100644 index 0000000..2a85b03 --- /dev/null +++ b/phpmon/Common/Http/RealApi.swift @@ -0,0 +1,9 @@ +// +// RealApi.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 30/09/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +class RealApi: ApiProtocol {} diff --git a/phpmon/Common/Http/TestableApi.swift b/phpmon/Common/Testables/TestableApi.swift similarity index 100% rename from phpmon/Common/Http/TestableApi.swift rename to phpmon/Common/Testables/TestableApi.swift diff --git a/phpmon/Domain/App/UpdateScheduler.swift b/phpmon/Domain/App/UpdateScheduler.swift index 11dbd18..bbc4371 100644 --- a/phpmon/Domain/App/UpdateScheduler.swift +++ b/phpmon/Domain/App/UpdateScheduler.swift @@ -47,7 +47,6 @@ actor UpdateScheduler { UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue) UserDefaults.standard.set(Date(), forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue) scheduleTimer() - Log.info("Update check succeeded. Next check in \(Constants.AutomaticUpdateCheckInterval)s.") case .networkError, .parseError: // Handle failures with exponential backoff diff --git a/phpmon/Domain/Integrations/Homebrew/CaskFile.swift b/phpmon/Domain/Integrations/Homebrew/CaskFile.swift index 666bc00..2950f05 100644 --- a/phpmon/Domain/Integrations/Homebrew/CaskFile.swift +++ b/phpmon/Domain/Integrations/Homebrew/CaskFile.swift @@ -30,8 +30,8 @@ struct CaskFile { } else { return await Shell.pipe(""" curl -s --max-time 10 \ - -H "User-Agent: phpmon \(App.shortVersion)" \ - -H "X-phpmon-version: \(App.bundleVersion)" \ + -H "User-Agent: phpmon-curl 1.0" \ + -H "X-phpmon-version: \(App.shortVersion) (\(App.bundleVersion))" \ -H "X-phpmon-os-version: \(App.macVersion)" \ -H "X-phpmon-bundle-id: \(App.identifier)" \ '\(url.absoluteString)' From 7e77dd562da85acad86f0ec799ca47d78044156e Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Tue, 30 Sep 2025 14:52:59 +0200 Subject: [PATCH 31/33] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20User-Agent=20reports?= =?UTF-8?q?=20as=20phpmon-curl/1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpmon/Domain/Integrations/Homebrew/CaskFile.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpmon/Domain/Integrations/Homebrew/CaskFile.swift b/phpmon/Domain/Integrations/Homebrew/CaskFile.swift index 2950f05..b13f567 100644 --- a/phpmon/Domain/Integrations/Homebrew/CaskFile.swift +++ b/phpmon/Domain/Integrations/Homebrew/CaskFile.swift @@ -30,7 +30,7 @@ struct CaskFile { } else { return await Shell.pipe(""" 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-os-version: \(App.macVersion)" \ -H "X-phpmon-bundle-id: \(App.identifier)" \ From f366be865bde07b77dbbabcbf855d95a2566fe36 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Tue, 30 Sep 2025 14:53:21 +0200 Subject: [PATCH 32/33] =?UTF-8?q?=F0=9F=94=A7=20Bump=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 26d9ccd..c0a8fa8 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -3837,7 +3837,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1601; + CURRENT_PROJECT_VERSION = 1605; DEAD_CODE_STRIPPING = YES; DEBUG = YES; ENABLE_APP_SANDBOX = NO; @@ -3881,7 +3881,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1601; + CURRENT_PROJECT_VERSION = 1605; DEAD_CODE_STRIPPING = YES; DEBUG = NO; ENABLE_APP_SANDBOX = NO; @@ -4063,7 +4063,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1601; + CURRENT_PROJECT_VERSION = 1605; DEAD_CODE_STRIPPING = YES; DEBUG = YES; ENABLE_APP_SANDBOX = NO; @@ -4256,7 +4256,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1601; + CURRENT_PROJECT_VERSION = 1605; DEAD_CODE_STRIPPING = YES; DEBUG = NO; ENABLE_APP_SANDBOX = NO; From 85b12d1aecb872d8841f157f7817ae230a163501 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Tue, 30 Sep 2025 16:14:17 +0200 Subject: [PATCH 33/33] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Prevent=20unit=20tes?= =?UTF-8?q?ts=20from=20running=20concurrently?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The way the testing classes are built with the singletons currently prevents various test structs from being run at the same time. I've adjusted the tests in the PHP Monitor EAP test configuration so that they are not executed concurrently. This does slow down the test suite but prevents odd crashes from individual tests interfering with other tests. It's not an ideal solution and I would like to address this in the future, but I suspect this will be rather cumbersome. Since the app does not actually suffer from this particular issue, this is something worth investigating later. --- PHP Monitor.xcodeproj/project.pbxproj | 2 - .../xcschemes/PHP Monitor EAP.xcscheme | 8 ++- .../xcschemes/PHP Monitor.xcscheme | 9 +-- .../Integrations/Homebrew/CaskFile.swift | 2 +- tests/PHP Monitor.xctestplan | 67 ------------------- tests/Shared/TestableConfigurations.swift | 15 +++++ .../Parsers/CaskFileParserTest.swift | 5 ++ .../Parsers/PhpConfigurationFileTest.swift | 4 +- .../Parsers/PhpExtensionTest.swift | 5 ++ 9 files changed, 36 insertions(+), 81 deletions(-) delete mode 100644 tests/PHP Monitor.xctestplan diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index c0a8fa8..21c445d 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -1144,7 +1144,6 @@ C46FA98A2822F08F00D78807 /* PhpConfigurationFileTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpConfigurationFileTest.swift; sourceTree = ""; }; C47015012C46D6910069AAE7 /* NVAlertExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NVAlertExtension.swift; sourceTree = ""; }; C4709CA128524B3400088BB8 /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = ""; }; - C471E79228F9B1D30021E251 /* PHP Monitor.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "PHP Monitor.xctestplan"; sourceTree = ""; }; C471E7AD28F9B4940021E251 /* Feature Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Feature Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; C471E7AF28F9B4940021E251 /* InternalSwitcherTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalSwitcherTest.swift; sourceTree = ""; }; C471E7BC28F9B90F0021E251 /* UI Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "UI Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1970,7 +1969,6 @@ isa = PBXGroup; children = ( C4E2E86828FC2FF2003B070C /* Shared */, - C471E79228F9B1D30021E251 /* PHP Monitor.xctestplan */, C4F7807A25D7F84B000DBC97 /* unit */, C471E7AE28F9B4940021E251 /* feature */, C471E7BD28F9B90F0021E251 /* ui */, diff --git a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor EAP.xcscheme b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor EAP.xcscheme index 4635407..ab0bc59 100644 --- a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor EAP.xcscheme +++ b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor EAP.xcscheme @@ -31,7 +31,8 @@ + parallelizable = "NO" + testExecutionOrdering = "random"> + skipped = "NO" + parallelizable = "NO"> - - - - + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> diff --git a/phpmon/Domain/Integrations/Homebrew/CaskFile.swift b/phpmon/Domain/Integrations/Homebrew/CaskFile.swift index b13f567..944bfd9 100644 --- a/phpmon/Domain/Integrations/Homebrew/CaskFile.swift +++ b/phpmon/Domain/Integrations/Homebrew/CaskFile.swift @@ -25,7 +25,7 @@ struct CaskFile { } private static func loadFromApi(_ url: URL) async -> String { - if App.hasLoadedTestableConfiguration { + if App.hasLoadedTestableConfiguration || url.absoluteString.contains("https://raw.githubusercontent.com") { return await Shell.pipe("curl -s --max-time 10 '\(url.absoluteString)'").out } else { return await Shell.pipe(""" diff --git a/tests/PHP Monitor.xctestplan b/tests/PHP Monitor.xctestplan deleted file mode 100644 index cd0bc27..0000000 --- a/tests/PHP Monitor.xctestplan +++ /dev/null @@ -1,67 +0,0 @@ -{ - "configurations" : [ - { - "id" : "98F42C11-E6D2-4AD9-A5CA-40EFE44F384A", - "name" : "Configuration 1", - "options" : { - - } - } - ], - "defaultOptions" : { - "codeCoverage" : false, - "commandLineArgumentEntries" : [ - { - "argument" : "--v", - "enabled" : false - } - ], - "environmentVariableEntries" : [ - { - "enabled" : false, - "key" : "EXTREME_DOCTOR_MODE", - "value" : "" - }, - { - "enabled" : false, - "key" : "SLOW_SHELL_MODE", - "value" : "" - }, - { - "enabled" : false, - "key" : "PAINT_PHPMON_SWIFTUI_VIEWS", - "value" : "" - } - ], - "targetForVariableExpansion" : { - "containerPath" : "container:PHP Monitor.xcodeproj", - "identifier" : "C41C1B3222B0097F00E7CF16", - "name" : "PHP Monitor" - } - }, - "testTargets" : [ - { - "parallelizable" : true, - "target" : { - "containerPath" : "container:PHP Monitor.xcodeproj", - "identifier" : "C4F7807825D7F84B000DBC97", - "name" : "Unit Tests" - } - }, - { - "target" : { - "containerPath" : "container:PHP Monitor.xcodeproj", - "identifier" : "C471E7AC28F9B4940021E251", - "name" : "Feature Tests" - } - }, - { - "target" : { - "containerPath" : "container:PHP Monitor.xcodeproj", - "identifier" : "C471E7BB28F9B90F0021E251", - "name" : "UI Tests" - } - } - ], - "version" : 1 -} diff --git a/tests/Shared/TestableConfigurations.swift b/tests/Shared/TestableConfigurations.swift index 6961704..65f5ed7 100644 --- a/tests/Shared/TestableConfigurations.swift +++ b/tests/Shared/TestableConfigurations.swift @@ -153,6 +153,21 @@ class TestableConfigurations { app 'PHP Monitor DEV.app', target: "PHP Monitor DEV.app" end """), + "curl -s --max-time 10 'https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon.rb''" : + .delayed(0.5, """ + cask 'phpmon-dev' do + depends_on formula: 'gnu-sed' + + version '25.08.0_1000' + sha256 '1cb147bd1b1fbd52971d90dff577465b644aee7c878f15ede57f46e8f217067a' + + url 'https://github.com/nicoverbruggen/phpmon/releases/download/v6.0/phpmon-dev.zip' + name 'PHP Monitor DEV' + homepage 'https://phpmon.app' + + app 'PHP Monitor DEV.app', target: "PHP Monitor DEV.app" + end + """), "/opt/homebrew/bin/brew unlink php" : .delayed(0.2, "OK"), "/opt/homebrew/bin/brew unlink php@8.2" diff --git a/tests/unit/SwiftTestMigrated/Parsers/CaskFileParserTest.swift b/tests/unit/SwiftTestMigrated/Parsers/CaskFileParserTest.swift index e24581f..07e1402 100644 --- a/tests/unit/SwiftTestMigrated/Parsers/CaskFileParserTest.swift +++ b/tests/unit/SwiftTestMigrated/Parsers/CaskFileParserTest.swift @@ -9,8 +9,13 @@ import Testing import Foundation +@Suite(.serialized) struct CaskFileParserTest { + init() async throws { + ActiveShell.useSystem() + } + // MARK: - Test Files static var exampleFilePath: URL { TestBundle.url(forResource: "phpmon-dev", withExtension: "rb")! diff --git a/tests/unit/SwiftTestMigrated/Parsers/PhpConfigurationFileTest.swift b/tests/unit/SwiftTestMigrated/Parsers/PhpConfigurationFileTest.swift index 0f0f8b1..e9ea17b 100644 --- a/tests/unit/SwiftTestMigrated/Parsers/PhpConfigurationFileTest.swift +++ b/tests/unit/SwiftTestMigrated/Parsers/PhpConfigurationFileTest.swift @@ -9,13 +9,14 @@ import Testing import Foundation +@Suite(.serialized) class PhpConfigurationFileTest { - 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) #expect(iniFile != nil) @@ -23,6 +24,7 @@ class PhpConfigurationFileTest { } @Test func can_check_key_existence() throws { + print(Self.phpIniFileUrl.path) let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)! #expect(iniFile.has(key: "error_reporting")) diff --git a/tests/unit/SwiftTestMigrated/Parsers/PhpExtensionTest.swift b/tests/unit/SwiftTestMigrated/Parsers/PhpExtensionTest.swift index fc49bfb..574bea0 100644 --- a/tests/unit/SwiftTestMigrated/Parsers/PhpExtensionTest.swift +++ b/tests/unit/SwiftTestMigrated/Parsers/PhpExtensionTest.swift @@ -9,7 +9,12 @@ import Testing import Foundation +@Suite(.serialized) struct PhpExtensionTest { + init () async throws { + ActiveShell.useSystem() + } + static var phpIniFileUrl: URL { TestBundle.url(forResource: "php", withExtension: "ini")! }