🚀 Version 25.09
@@ -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
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1640"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
|
||||
BuildableName = "PHP Monitor.app"
|
||||
BlueprintName = "PHP Monitor"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug.Dev"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
codeCoverageEnabled = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C4F7807825D7F84B000DBC97"
|
||||
BuildableName = "Unit Tests.xctest"
|
||||
BlueprintName = "Unit Tests"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C471E7BB28F9B90F0021E251"
|
||||
BuildableName = "UI Tests.xctest"
|
||||
BlueprintName = "UI Tests"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C471E7AC28F9B4940021E251"
|
||||
BuildableName = "Feature Tests.xctest"
|
||||
BlueprintName = "Feature Tests"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug.Dev"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
|
||||
BuildableName = "PHP Monitor.app"
|
||||
BlueprintName = "PHP Monitor"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "--v"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--cli"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--configuration:~/.phpmon_fconf_working.json"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--configuration:~/.phpmon_fconf_working_no_valet.json"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--configuration:~/.phpmon_fconf_broken.json"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "EXTREME_DOCTOR_MODE"
|
||||
value = ""
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "PAINT_PHPMON_SWIFTUI_VIEWS"
|
||||
value = ""
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release.Dev"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
|
||||
BuildableName = "PHP Monitor.app"
|
||||
BlueprintName = "PHP Monitor"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug.Dev">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release.Dev"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1640"
|
||||
LastUpgradeVersion = "2600"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -31,7 +31,8 @@
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
parallelizable = "NO"
|
||||
testExecutionOrdering = "random">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C4F7807825D7F84B000DBC97"
|
||||
@@ -41,7 +42,7 @@
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
skipped = "YES"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
@@ -52,7 +53,8 @@
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
skipped = "NO"
|
||||
parallelizable = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C471E7AC28F9B4940021E251"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1640"
|
||||
LastUpgradeVersion = "2600"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1640"
|
||||
LastUpgradeVersion = "2600"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -26,13 +26,8 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<TestPlans>
|
||||
<TestPlanReference
|
||||
reference = "container:PHP Monitor.xcodeproj/PHP Monitor.xctestplan"
|
||||
default = "YES">
|
||||
</TestPlanReference>
|
||||
</TestPlans>
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1640"
|
||||
LastUpgradeVersion = "2600"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
10
README.md
@@ -399,13 +399,15 @@ PHP Monitor is a universal app and supports both architectures, so [find out her
|
||||
<details>
|
||||
<summary><strong>Why is the app doing network requests?</strong></summary>
|
||||
|
||||
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`.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -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+)<br/>Sonoma (14.0+)<br/>Sequoia (15.0+)<br/>Tahoe (26.0+)* | macOS 13.5+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.5 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 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+)<br/>Sonoma (14.0+)<br/>Sequoia (15.0+)<br/>Tahoe (26.0+) | macOS 13.5+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.5 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
|
||||
## Legacy versions
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
28
phpmon-updater/AppIconUD.icon/Assets/upgrade.svg
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 171 175" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g>
|
||||
<g id="Updater-Badge" serif:id="Updater Badge" transform="matrix(1,0,0,1,-319.58,-314.735)">
|
||||
<g>
|
||||
<g transform="matrix(1,0,0,1,20.6989,12.9824)">
|
||||
<ellipse cx="383.215" cy="389.494" rx="69.228" ry="71.667" style="fill:rgb(189,237,171);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.302874,0,0,0.311711,327.157,322.241)">
|
||||
<path d="M256,504C393,504 504,393 504,256C504,119 393,8 256,8C119,8 8,119 8,256C8,393 119,504 256,504ZM256,56C366.5,56 456,145.5 456,256C456,366.5 366.5,456 256,456C145.5,456 56,366.5 56,256C56,145.5 145.5,56 256,56ZM276,384L236,384C229.4,384 224,378.6 224,372L224,256L157,256C146.3,256 141,243.1 148.5,235.5L247.5,136.5C252.2,131.8 259.8,131.8 264.5,136.5L363.5,235.5C371.1,243.1 365.7,256 355,256L288,256L288,372C288,378.6 282.6,384 276,384Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g transform="matrix(1,0,0,1,20.6989,12.9824)">
|
||||
<ellipse cx="383.215" cy="389.494" rx="69.228" ry="71.667" style="fill:rgb(189,237,171);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.302874,0,0,0.311711,327.157,322.241)">
|
||||
<path d="M256,504C393,504 504,393 504,256C504,119 393,8 256,8C119,8 8,119 8,256C8,393 119,504 256,504ZM256,56C366.5,56 456,145.5 456,256C456,366.5 366.5,456 256,456C145.5,456 56,366.5 56,256C56,145.5 145.5,56 256,56ZM276,384L236,384C229.4,384 224,378.6 224,372L224,256L157,256C146.3,256 141,243.1 148.5,235.5L247.5,136.5C252.2,131.8 259.8,131.8 264.5,136.5L363.5,235.5C371.1,243.1 365.7,256 355,256L288,256L288,372C288,378.6 282.6,384 276,384Z" style="fill:url(#_Linear2);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.82822e-14,448.789,-461.884,2.74804e-14,256.903,32.5209)"><stop offset="0" style="stop-color:rgb(0,179,48);stop-opacity:1"/><stop offset="0.5" style="stop-color:rgb(0,156,42);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(0,133,37);stop-opacity:1"/></linearGradient>
|
||||
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.82822e-14,448.789,-461.884,2.74804e-14,256.903,32.5209)"><stop offset="0" style="stop-color:rgb(0,179,48);stop-opacity:1"/><stop offset="0.5" style="stop-color:rgb(0,156,42);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(0,133,37);stop-opacity:1"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
84
phpmon-updater/AppIconUD.icon/icon.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"fill" : {
|
||||
"linear-gradient" : [
|
||||
"srgb:0.27800,0.58000,0.98800,1.00000",
|
||||
"srgb:0.27800,0.58000,0.98800,1.00000"
|
||||
]
|
||||
},
|
||||
"groups" : [
|
||||
{
|
||||
"blur-material" : null,
|
||||
"layers" : [
|
||||
{
|
||||
"blend-mode" : "normal",
|
||||
"fill" : "none",
|
||||
"glass" : true,
|
||||
"image-name" : "upgrade.svg",
|
||||
"name" : "upgrade",
|
||||
"position" : {
|
||||
"scale" : 2.3,
|
||||
"translation-in-points" : [
|
||||
251.390625,
|
||||
248.5546875
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
0.609375,
|
||||
9.453125
|
||||
]
|
||||
},
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency" : {
|
||||
"enabled" : false,
|
||||
"value" : 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"blend-mode" : "screen",
|
||||
"blur-material" : null,
|
||||
"layers" : [
|
||||
{
|
||||
"blend-mode" : "normal",
|
||||
"fill" : {
|
||||
"solid" : "srgb:1.00000,0.99038,0.96423,1.00000"
|
||||
},
|
||||
"glass" : true,
|
||||
"image-name" : "phpmon.svg",
|
||||
"name" : "phpmon",
|
||||
"position" : {
|
||||
"scale" : 1.85,
|
||||
"translation-in-points" : [
|
||||
3.890625,
|
||||
2.3828125
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"circles" : [
|
||||
"watchOS"
|
||||
],
|
||||
"squares" : "shared"
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon_16x16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_16x16@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_32x32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_32x32@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_128x128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_128x128@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_256x256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_256x256@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_512x512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_512x512@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 811 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 450 KiB |
10
phpmon/AppIcon.icon/Assets/phpmon.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1.25033,-0.175723,0.175723,1.25033,14.4412,107.226)">
|
||||
<path d="M133.3,83.75L120.4,83.75L120.4,54.437C120.4,52.134 118.465,50.25 116.1,50.25L107.5,50.25C105.135,50.25 103.2,52.134 103.2,54.438L103.2,96.312C103.2,98.616 105.135,100.5 107.5,100.5L133.3,100.5C135.665,100.5 137.6,98.616 137.6,96.312L137.6,87.938C137.6,85.634 135.665,83.75 133.3,83.75ZM335.4,184.25L326.8,184.25L326.8,127.666C326.8,121.019 324.059,114.633 319.221,109.922L265.525,57.63C260.688,52.92 254.13,50.25 247.304,50.25L223.6,50.25L223.6,25.125C223.6,11.254 212.044,0 197.8,0L25.8,0C11.556,0 0,11.254 0,25.125L0,192.625C0,206.496 11.556,217.75 25.8,217.75L34.4,217.75C34.4,245.492 57.513,268 86,268C114.487,268 137.6,245.492 137.6,217.75L206.4,217.75C206.4,245.492 229.512,268 258,268C286.488,268 309.6,245.492 309.6,217.75L335.4,217.75C340.13,217.75 344,213.981 344,209.375L344,192.625C344,188.019 340.13,184.25 335.4,184.25ZM86,242.875C71.756,242.875 60.2,231.621 60.2,217.75C60.2,203.879 71.756,192.625 86,192.625C100.244,192.625 111.8,203.879 111.8,217.75C111.8,231.621 100.244,242.875 86,242.875ZM111.8,150.75C78.529,150.75 51.6,124.526 51.6,92.125C51.6,59.725 78.529,33.5 111.8,33.5C145.071,33.5 172,59.724 172,92.125C172,124.525 145.071,150.75 111.8,150.75ZM258,242.875C243.756,242.875 232.2,231.621 232.2,217.75C232.2,203.879 243.756,192.625 258,192.625C272.244,192.625 283.8,203.879 283.8,217.75C283.8,231.621 272.244,242.875 258,242.875ZM301,134L223.6,134L223.6,75.375L247.304,75.375L301,127.666L301,134Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.96882e-14,321.533,-321.533,1.96882e-14,172.079,-41.4918)"><stop offset="0" style="stop-color:rgb(81,194,251);stop-opacity:1"/><stop offset="0" style="stop-color:rgb(81,194,251);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(28,145,254);stop-opacity:1"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
26
phpmon/AppIconEAP.icon/Assets/eap.svg
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 343 138" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g id="EAP-Pill" serif:id="EAP Pill" transform="matrix(0.989959,-5.55112e-17,5.55112e-17,1,-226.512,-352.307)">
|
||||
<g transform="matrix(1.49702,2.46519e-32,-4.1157e-17,1,-175.953,-20.6096)">
|
||||
<path d="M485.35,426.302L485.35,456.577C485.35,472.795 476.465,485.962 465.522,485.962L306.401,485.962C295.458,485.962 286.573,472.795 286.573,456.577L286.573,426.302C286.573,410.084 295.458,396.917 306.401,396.917L465.522,396.917C476.465,396.917 485.35,410.084 485.35,426.302Z" style="fill:url(#_Linear1);"/>
|
||||
</g>
|
||||
<g transform="matrix(1.49702,2.46519e-32,-4.1157e-17,1,-175.953,-20.6096)">
|
||||
<path d="M485.35,426.302L485.35,456.577C485.35,472.795 476.465,485.962 465.522,485.962L306.401,485.962C295.458,485.962 286.573,472.795 286.573,456.577L286.573,426.302C286.573,410.084 295.458,396.917 306.401,396.917L465.522,396.917C476.465,396.917 485.35,410.084 485.35,426.302Z" style="fill:url(#_Linear2);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.811641,-6.24693e-17,-3.25779e-17,1,143.651,1.2882)">
|
||||
<g>
|
||||
<path d="M184.151,434.829L184.151,422.301L202.79,422.301L202.79,412.221L184.151,412.221L184.151,404.445L207.27,404.445L207.27,394.365L169.366,394.365L169.366,444.909L207.987,444.909L207.987,434.829L184.151,434.829Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M184.151,434.829L184.151,422.301L202.79,422.301L202.79,412.221L184.151,412.221L184.151,404.445L207.27,404.445L207.27,394.365L169.366,394.365L169.366,444.909L207.987,444.909L207.987,434.829L184.151,434.829Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M230.389,444.909L233.974,438.789L257.541,438.789L261.125,444.909L277.524,444.909L248.222,393.933L243.383,393.933L213.812,444.909L230.389,444.909ZM245.713,417.117L252.254,428.997L239.171,428.997L245.713,417.117Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M230.389,444.909L233.974,438.789L257.541,438.789L261.125,444.909L277.524,444.909L248.222,393.933L243.383,393.933L213.812,444.909L230.389,444.909ZM245.713,417.117L252.254,428.997L239.171,428.997L245.713,417.117Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M327.346,412.581C327.346,399.333 310.679,390.333 283.348,394.869L283.348,444.909L298.134,444.909L298.134,432.093C314.263,432.021 327.346,424.461 327.346,412.581ZM298.134,404.733C308.17,403.725 312.113,408.621 312.113,412.581C312.113,417.189 308.349,421.941 298.134,421.581L298.134,404.733Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<path d="M327.346,412.581C327.346,399.333 310.679,390.333 283.348,394.869L283.348,444.909L298.134,444.909L298.134,432.093C314.263,432.021 327.346,424.461 327.346,412.581ZM298.134,404.733C308.17,403.725 312.113,408.621 312.113,412.581C312.113,417.189 308.349,421.941 298.134,421.581L298.134,404.733Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.99972e-15,-81.6517,36.5774,2.23972e-15,387.828,484.137)"><stop offset="0" style="stop-color:rgb(240,21,150);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(246,86,109);stop-opacity:1"/></linearGradient>
|
||||
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.99972e-15,-81.6517,36.5774,2.23972e-15,387.828,484.137)"><stop offset="0" style="stop-color:rgb(240,21,150);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(246,86,109);stop-opacity:1"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
10
phpmon/AppIconEAP.icon/Assets/phpmon.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1.25033,-0.175723,0.175723,1.25033,14.4412,107.226)">
|
||||
<path d="M133.3,83.75L120.4,83.75L120.4,54.437C120.4,52.134 118.465,50.25 116.1,50.25L107.5,50.25C105.135,50.25 103.2,52.134 103.2,54.438L103.2,96.312C103.2,98.616 105.135,100.5 107.5,100.5L133.3,100.5C135.665,100.5 137.6,98.616 137.6,96.312L137.6,87.938C137.6,85.634 135.665,83.75 133.3,83.75ZM335.4,184.25L326.8,184.25L326.8,127.666C326.8,121.019 324.059,114.633 319.221,109.922L265.525,57.63C260.688,52.92 254.13,50.25 247.304,50.25L223.6,50.25L223.6,25.125C223.6,11.254 212.044,0 197.8,0L25.8,0C11.556,0 0,11.254 0,25.125L0,192.625C0,206.496 11.556,217.75 25.8,217.75L34.4,217.75C34.4,245.492 57.513,268 86,268C114.487,268 137.6,245.492 137.6,217.75L206.4,217.75C206.4,245.492 229.512,268 258,268C286.488,268 309.6,245.492 309.6,217.75L335.4,217.75C340.13,217.75 344,213.981 344,209.375L344,192.625C344,188.019 340.13,184.25 335.4,184.25ZM86,242.875C71.756,242.875 60.2,231.621 60.2,217.75C60.2,203.879 71.756,192.625 86,192.625C100.244,192.625 111.8,203.879 111.8,217.75C111.8,231.621 100.244,242.875 86,242.875ZM111.8,150.75C78.529,150.75 51.6,124.526 51.6,92.125C51.6,59.725 78.529,33.5 111.8,33.5C145.071,33.5 172,59.724 172,92.125C172,124.525 145.071,150.75 111.8,150.75ZM258,242.875C243.756,242.875 232.2,231.621 232.2,217.75C232.2,203.879 243.756,192.625 258,192.625C272.244,192.625 283.8,203.879 283.8,217.75C283.8,231.621 272.244,242.875 258,242.875ZM301,134L223.6,134L223.6,75.375L247.304,75.375L301,127.666L301,134Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.96882e-14,321.533,-321.533,1.96882e-14,172.079,-41.4918)"><stop offset="0" style="stop-color:rgb(81,194,251);stop-opacity:1"/><stop offset="0" style="stop-color:rgb(81,194,251);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(28,145,254);stop-opacity:1"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
80
phpmon/AppIconEAP.icon/icon.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"fill" : {
|
||||
"linear-gradient" : [
|
||||
"srgb:0.27800,0.58000,0.98800,1.00000",
|
||||
"srgb:0.27800,0.58000,0.98800,1.00000"
|
||||
]
|
||||
},
|
||||
"groups" : [
|
||||
{
|
||||
"layers" : [
|
||||
{
|
||||
"glass" : true,
|
||||
"image-name" : "eap.svg",
|
||||
"name" : "eap",
|
||||
"position" : {
|
||||
"scale" : 2.5,
|
||||
"translation-in-points" : [
|
||||
345.96875,
|
||||
354.0390625
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
1.3203125,
|
||||
6.5390625
|
||||
]
|
||||
},
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency" : {
|
||||
"enabled" : false,
|
||||
"value" : 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"blend-mode" : "screen",
|
||||
"blur-material" : null,
|
||||
"layers" : [
|
||||
{
|
||||
"blend-mode" : "normal",
|
||||
"fill" : {
|
||||
"solid" : "srgb:1.00000,0.99038,0.96423,1.00000"
|
||||
},
|
||||
"glass" : true,
|
||||
"image-name" : "phpmon.svg",
|
||||
"name" : "phpmon",
|
||||
"position" : {
|
||||
"scale" : 1.85,
|
||||
"translation-in-points" : [
|
||||
21.8515625,
|
||||
-6.34375
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"circles" : [
|
||||
"watchOS"
|
||||
],
|
||||
"squares" : "shared"
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon_16x16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_16x16@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_32x32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_32x32@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_128x128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_128x128@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_256x256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_256x256@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_512x512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_512x512@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 783 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 457 KiB |
@@ -1,68 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon_16x16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_16x16@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_32x32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_32x32@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_128x128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_128x128@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_256x256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_256x256@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_512x512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_512x512@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 820 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 485 KiB |
@@ -21,16 +21,39 @@ 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
|
||||
static let SlowBootThresholdInterval: TimeInterval = .seconds(30)
|
||||
|
||||
/**
|
||||
The interval between automatic background update checks.
|
||||
*/
|
||||
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 = .minutes(60)
|
||||
|
||||
/**
|
||||
Retry intervals for failed automatic update checks.
|
||||
Uses exponential backoff before falling back to normal schedule.
|
||||
*/
|
||||
static let UpdateCheckRetryIntervals: [TimeInterval] = [
|
||||
.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
|
||||
@@ -39,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.
|
||||
@@ -49,7 +76,8 @@ struct Constants {
|
||||
*/
|
||||
static var ExperimentalPhpVersions: Set<String> {
|
||||
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")
|
||||
]
|
||||
|
||||
@@ -85,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
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -107,57 +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("https://phpmon.app/early-access/release-notes")
|
||||
|
||||
// API endpoints (via api.phpmon.app)
|
||||
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 StableBuildCaskFile = URL(
|
||||
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon.rb"
|
||||
)!
|
||||
|
||||
static let DevBuildCaskFile = URL(
|
||||
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
|
||||
)!
|
||||
|
||||
// EAP URLs
|
||||
|
||||
static let EarlyAccessCaskFile = URL(
|
||||
string: "https://phpmon.app/builds/early-access/sponsors/phpmon-eap.rb"
|
||||
)!
|
||||
|
||||
static let EarlyAccessChangelog = URL(
|
||||
string: "https://phpmon.app/early-access/release-notes"
|
||||
)!
|
||||
|
||||
static let GitHubReleases = url("https://github.com/nicoverbruggen/phpmon/releases")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
// MARK: Common Shell Commands
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
Runs a `brew` command. Can run as superuser.
|
||||
*/
|
||||
@@ -49,3 +51,10 @@ 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)!
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
25
phpmon/Common/Http/ActiveApi.swift
Normal file
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
11
phpmon/Common/Http/ApiProtocol.swift
Normal file
@@ -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 {
|
||||
|
||||
}
|
||||
9
phpmon/Common/Http/RealApi.swift
Normal file
@@ -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 {}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
41
phpmon/Common/Testables/TestableApi.swift
Normal file
@@ -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) ?? ""
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -53,6 +56,11 @@ class App {
|
||||
return machine
|
||||
}
|
||||
|
||||
static var macVersion: String {
|
||||
let version = ProcessInfo.processInfo.operatingSystemVersion
|
||||
return "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
|
||||
}
|
||||
|
||||
/**
|
||||
A fake architecture.
|
||||
When set, the real machine's system architecture is not used,
|
||||
|
||||
@@ -10,32 +10,28 @@ import Foundation
|
||||
import Cocoa
|
||||
import NVAlert
|
||||
|
||||
enum UpdateCheckResult {
|
||||
case success
|
||||
case networkError
|
||||
case parseError
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Log.info("The app will search for updates...")
|
||||
|
||||
var caskUrl = Constants.Urls.StableBuildCaskFile
|
||||
|
||||
if App.identifier.contains(".phpmon.eap") {
|
||||
caskUrl = Constants.Urls.EarlyAccessCaskFile
|
||||
} else if App.identifier.contains(".phpmon.dev") {
|
||||
caskUrl = Constants.Urls.DevBuildCaskFile
|
||||
}
|
||||
let caskUrl = Constants.Urls.UpdateCheckEndpoint
|
||||
|
||||
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
|
||||
@@ -44,7 +40,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
|
||||
@@ -55,6 +52,8 @@ class AppUpdater {
|
||||
} else if interactive {
|
||||
presentNoNewerVersionAvailableAlert()
|
||||
}
|
||||
|
||||
return .success
|
||||
}
|
||||
|
||||
private func presentCouldNotRetrieveUpdateIfInteractive() {
|
||||
@@ -68,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(
|
||||
@@ -188,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),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="24127" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23727"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24127"/>
|
||||
<capability name="Image references" minToolsVersion="12.0"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
|
||||
@@ -436,7 +436,7 @@
|
||||
</toolbarItem>
|
||||
<searchToolbarItem implicitItemIdentifier="7C834FBE-7118-4082-A09F-7CBECEC1356A" label="Search" paletteLabel="Search" visibilityPriority="1001" id="G2g-jS-RVc">
|
||||
<nil key="toolTip"/>
|
||||
<searchField key="view" focusRingType="none" verticalHuggingPriority="750" textCompletion="NO" id="0gE-Yr-MLy">
|
||||
<searchField key="view" verticalHuggingPriority="750" textCompletion="NO" id="0gE-Yr-MLy">
|
||||
<rect key="frame" x="0.0" y="0.0" width="100" height="21"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" sendsSearchStringImmediately="YES" id="vp9-vH-goQ">
|
||||
@@ -582,7 +582,7 @@ Gw
|
||||
<constraint firstAttribute="bottom" secondItem="8zu-cF-KCX" secondAttribute="bottom" constant="20" symbolic="YES" id="wIl-uw-y3p"/>
|
||||
</constraints>
|
||||
</visualEffectView>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="U1c-qS-cIm">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="U1c-qS-cIm">
|
||||
<rect key="frame" x="98" y="153" width="384" height="19"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="380" id="WgB-hj-d4P"/>
|
||||
@@ -593,7 +593,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="yI6-qf-htf">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="yI6-qf-htf">
|
||||
<rect key="frame" x="98" y="127" width="384" height="16"/>
|
||||
<textFieldCell key="cell" selectable="YES" title="This is a slightly more expanded explanation." id="rY3-Nd-Iit">
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -617,7 +617,7 @@ Gw
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="7eT-Hw-EL9"/>
|
||||
</imageView>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hml-dl-Cah">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hml-dl-Cah">
|
||||
<rect key="frame" x="98" y="70" width="384" height="42"/>
|
||||
<textFieldCell key="cell" selectable="YES" id="7iW-Lc-DqO">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@@ -706,7 +706,7 @@ Gw
|
||||
<action selector="pressedCancel:" target="glS-wF-sEU" id="q0L-YZ-F3J"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i">
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i">
|
||||
<rect key="frame" x="20" y="150" width="440" height="21"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="NFa-1D-Bi4">
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -717,7 +717,7 @@ Gw
|
||||
<outlet property="delegate" destination="glS-wF-sEU" id="Dyf-0M-Gwj"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VzR-5a-cmT">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VzR-5a-cmT">
|
||||
<rect key="frame" x="18" y="128" width="444" height="14"/>
|
||||
<textFieldCell key="cell" title="[i18n] Preview text here" id="bJr-s6-tdP">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@@ -735,7 +735,7 @@ Gw
|
||||
<action selector="pressedSecure:" target="glS-wF-sEU" id="OIj-Pz-5Ea"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mmQ-7e-dlb">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mmQ-7e-dlb">
|
||||
<rect key="frame" x="18" y="60" width="444" height="28"/>
|
||||
<textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges.
You may be prompted for your password or Touch ID." id="4gd-KM-5Fu">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@@ -750,7 +750,7 @@ Gw
|
||||
<url key="url" string="file:///Users/"/>
|
||||
</pathCell>
|
||||
</pathControl>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P0B-Ht-R8n">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P0B-Ht-R8n">
|
||||
<rect key="frame" x="18" y="209" width="128" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Link a Folder" id="S4j-ZC-ddT">
|
||||
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
|
||||
@@ -758,7 +758,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField hidden="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
|
||||
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
|
||||
<rect key="frame" x="140" y="23" width="180" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="jOt-n6-TQf">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@@ -893,7 +893,7 @@ Gw
|
||||
<rect key="frame" x="69" y="0.0" width="200" height="54"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD">
|
||||
<rect key="frame" x="3" y="26" width="145" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="my-domain-name.test" id="SGC-Gm-Mxd">
|
||||
<font key="font" metaFont="systemSemibold" size="13"/>
|
||||
@@ -901,7 +901,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="CXK-Q9-CpO">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="CXK-Q9-CpO">
|
||||
<rect key="frame" x="3" y="12" width="75" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="~/path/to/site" id="fe7-Ha-mR9">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@@ -927,7 +927,7 @@ Gw
|
||||
<rect key="frame" x="69" y="54" width="200" height="54"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="aot-FJ-HIk">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="aot-FJ-HIk">
|
||||
<rect key="frame" x="33" y="26" width="145" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="my-domain-name.test" id="LHu-UF-QlC">
|
||||
<font key="font" metaFont="systemSemibold" size="13"/>
|
||||
@@ -935,7 +935,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="GNH-l8-oki">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="GNH-l8-oki">
|
||||
<rect key="frame" x="33" y="12" width="75" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="~/path/to/site" id="LNw-Ju-0Ot">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@@ -1078,7 +1078,7 @@ Gw
|
||||
<rect key="frame" x="470" y="0.0" width="97" height="54"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key">
|
||||
<rect key="frame" x="6" y="26" width="93" height="14"/>
|
||||
<textFieldCell key="cell" alignment="left" title="Laravel" id="0lu-L6-oKr">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@@ -1086,7 +1086,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aPK-Xc-J4B">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aPK-Xc-J4B">
|
||||
<rect key="frame" x="6" y="15" width="93" height="11"/>
|
||||
<textFieldCell key="cell" alignment="left" title="PHP 8.0" id="puf-Jh-ham">
|
||||
<font key="font" metaFont="miniSystem"/>
|
||||
@@ -1153,7 +1153,7 @@ Gw
|
||||
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/>
|
||||
</constraints>
|
||||
</progressIndicator>
|
||||
<textField wantsLayer="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="xoy-5Y-WDT">
|
||||
<textField wantsLayer="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="xoy-5Y-WDT">
|
||||
<rect key="frame" x="15" y="14" width="71" height="13"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="PLEASE WAIT" id="tMX-Ky-caT">
|
||||
<font key="font" metaFont="system" size="10"/>
|
||||
@@ -1209,7 +1209,7 @@ Gw
|
||||
<rect key="frame" x="0.0" y="0.0" width="540" height="286"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QCK-Z9-w7g">
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QCK-Z9-w7g">
|
||||
<rect key="frame" x="20" y="196" width="500" height="21"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" title="http://127.0.0.1:80" placeholderString="http://127.0.0.1:80" drawsBackground="YES" id="muS-8M-KSy">
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -1220,7 +1220,7 @@ Gw
|
||||
<outlet property="delegate" destination="dwh-CF-6iv" id="lNE-OI-G93"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Uib-vA-HRc">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Uib-vA-HRc">
|
||||
<rect key="frame" x="18" y="221" width="325" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Proxy subject (usually: protocol, IP address and port)" id="G1Z-3f-BhL">
|
||||
<font key="font" metaFont="systemMedium" size="11"/>
|
||||
@@ -1228,7 +1228,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mlA-Zt-Hu8">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mlA-Zt-Hu8">
|
||||
<rect key="frame" x="18" y="172" width="112" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Domain name" id="dQs-oZ-80e">
|
||||
<font key="font" metaFont="systemMedium" size="11"/>
|
||||
@@ -1236,7 +1236,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SNw-oQ-bnb">
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SNw-oQ-bnb">
|
||||
<rect key="frame" x="20" y="147" width="500" height="21"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="gTQ-Y2-Y9w">
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -1292,7 +1292,7 @@ Gw
|
||||
<action selector="pressedCancel:" target="dwh-CF-6iv" id="J2T-Zj-A0j"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JSZ-x8-Pqi">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JSZ-x8-Pqi">
|
||||
<rect key="frame" x="18" y="128" width="504" height="14"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="500" id="sF1-RG-URI"/>
|
||||
@@ -1313,7 +1313,7 @@ Gw
|
||||
<action selector="pressedSecure:" target="dwh-CF-6iv" id="b74-8T-AzO"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5x7-ll-2f7">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5x7-ll-2f7">
|
||||
<rect key="frame" x="18" y="60" width="504" height="28"/>
|
||||
<textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges.
You may be prompted for your password or Touch ID." id="IMB-O5-ZOy">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@@ -1321,7 +1321,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="DAh-br-Dfx">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="DAh-br-Dfx">
|
||||
<rect key="frame" x="18" y="250" width="123" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Add a Proxy" id="AZ1-04-kUl">
|
||||
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
|
||||
@@ -1329,7 +1329,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField hidden="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="w0k-CK-0u4">
|
||||
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="w0k-CK-0u4">
|
||||
<rect key="frame" x="191" y="23" width="180" height="14"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="4sH-94-UJl">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
@@ -1443,7 +1443,7 @@ Gw
|
||||
<subviews>
|
||||
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="L5n-Gw-J27">
|
||||
<rect key="frame" x="-7" y="-7" width="172" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="[i18n] Create a Link" bezelStyle="rounded" image="IconLinked" imagePosition="left" alignment="center" borderStyle="border" imageScaling="proportionallyUpOrDown" inset="2" id="8UP-Sw-TP6">
|
||||
<buttonCell key="cell" type="push" title="[i18n] Create a Link" bezelStyle="rounded" image="IconLinked" imagePosition="leading" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="8UP-Sw-TP6">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent">l</string>
|
||||
@@ -1454,7 +1454,7 @@ Gw
|
||||
</button>
|
||||
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="01Z-IV-hv1">
|
||||
<rect key="frame" x="159" y="-7" width="181" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="[i18n] Create a Proxy" bezelStyle="rounded" image="IconProxy" imagePosition="left" alignment="center" borderStyle="border" imageScaling="proportionallyUpOrDown" inset="2" id="bJ4-q8-1Ej">
|
||||
<buttonCell key="cell" type="push" title="[i18n] Create a Proxy" bezelStyle="rounded" image="IconProxy" imagePosition="leading" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="bJ4-q8-1Ej">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent">p</string>
|
||||
@@ -1473,7 +1473,7 @@ Gw
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fJK-Ke-IK3">
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fJK-Ke-IK3">
|
||||
<rect key="frame" x="18" y="138" width="504" height="19"/>
|
||||
<textFieldCell key="cell" selectable="YES" alignment="left" title="[i18n] What kind of domain would you like to set up?" id="agk-Nj-FLd">
|
||||
<font key="font" metaFont="systemBold" size="15"/>
|
||||
@@ -1481,7 +1481,7 @@ Gw
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField wantsLayer="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="urj-Xq-TrJ">
|
||||
<textField wantsLayer="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="urj-Xq-TrJ">
|
||||
<rect key="frame" x="18" y="60" width="504" height="70"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="500" id="tbl-AV-4qB"/>
|
||||
|
||||
126
phpmon/Domain/App/UpdateScheduler.swift
Normal file
@@ -0,0 +1,126 @@
|
||||
//
|
||||
// UpdateScheduler.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 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
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
27
phpmon/Domain/Integrations/Analytics/LoggableEvent.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// LoggableEvent.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 12/09/2025.
|
||||
// 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"
|
||||
|
||||
case phpVersionSwitched = "php_version_switched"
|
||||
|
||||
case openedDomainManagement = "opened_domain_management"
|
||||
case openedPhpInstallations = "opened_php_installations"
|
||||
case openedPhpExtensions = "opened_php_extensions"
|
||||
|
||||
case openedSettings = "opened_settings"
|
||||
|
||||
// TODO: Add more tracked things.
|
||||
}
|
||||
@@ -24,13 +24,28 @@ struct CaskFile {
|
||||
return self.properties["version"]!
|
||||
}
|
||||
|
||||
private static func loadFromApi(_ url: URL) async -> String {
|
||||
if App.hasLoadedTestableConfiguration || url.absoluteString.contains("https://raw.githubusercontent.com") {
|
||||
return await Shell.pipe("curl -s --max-time 10 '\(url.absoluteString)'").out
|
||||
} else {
|
||||
return await Shell.pipe("""
|
||||
curl -s --max-time 10 \
|
||||
-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)'
|
||||
""").out
|
||||
}
|
||||
}
|
||||
|
||||
public static func from(url: URL) async -> CaskFile? {
|
||||
var string: String?
|
||||
|
||||
if url.scheme == "file" {
|
||||
string = try? String(contentsOf: url)
|
||||
} else {
|
||||
string = await Shell.pipe("curl -s --max-time 10 '\(url.absoluteString)'").out
|
||||
string = await CaskFile.loadFromApi(url)
|
||||
}
|
||||
|
||||
guard let string else {
|
||||
|
||||
@@ -142,7 +142,7 @@ extension MainMenu {
|
||||
}
|
||||
} else {
|
||||
// Check for updates
|
||||
await AppUpdater().checkForUpdates(userInitiated: false)
|
||||
await UpdateScheduler.shared.startAutomaticUpdateChecking()
|
||||
|
||||
// Check if the linked version has changed between launches of phpmon
|
||||
await PhpGuard().compareToLastGlobalVersion()
|
||||
|
||||
@@ -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,18 @@ 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"
|
||||
case updateCheckFailureCount = "update_check_failure_count"
|
||||
}
|
||||
|
||||
/**
|
||||
These are internal stats. They NEVER get shared.
|
||||
*/
|
||||
|
||||
@@ -83,6 +83,10 @@ class Preferences {
|
||||
PreferenceName.displayPresets.rawValue: true,
|
||||
PreferenceName.displayMisc.rawValue: true,
|
||||
|
||||
/// Persistent App State
|
||||
PersistentAppState.lastAutomaticUpdateCheck.rawValue: 0,
|
||||
PersistentAppState.updateCheckFailureCount.rawValue: 0,
|
||||
|
||||
/// Stats
|
||||
InternalStats.switchCount.rawValue: 0,
|
||||
InternalStats.launchCount.rawValue: 0,
|
||||
@@ -90,13 +94,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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
|
||||
|
||||
@@ -172,7 +172,6 @@ struct Preset: Codable, Equatable {
|
||||
|
||||
// MARK: - Menu Items
|
||||
|
||||
// swiftlint:disable void_function_in_ternary
|
||||
public func getMenuItemText() -> String {
|
||||
var info = extensions.count == 1
|
||||
? "preset.extension".localized(extensions.count)
|
||||
@@ -197,7 +196,6 @@ struct Preset: Codable, Equatable {
|
||||
+ info + "</i>"
|
||||
+ "</span>"
|
||||
}
|
||||
// swiftlint:enable void_function_in_ternary
|
||||
|
||||
// MARK: - Reverting
|
||||
|
||||
|
||||
@@ -10,6 +10,22 @@
|
||||
<string>https://github.com/nicoverbruggen/phpmon</string>
|
||||
<key>Connections</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>IsIncoming</key>
|
||||
<false/>
|
||||
<key>Host</key>
|
||||
<string>phpmon.app, api.phpmon.app</string>
|
||||
<key>NetworkProtocol</key>
|
||||
<string>TCP</string>
|
||||
<key>Port</key>
|
||||
<string>80, 443</string>
|
||||
<key>Relevance</key>
|
||||
<string>Essential</string>
|
||||
<key>Purpose</key>
|
||||
<string>PHP Monitor contacts the official phpmon domain to check for updates, and visit aliased links that point to documentation.</string>
|
||||
<key>DenyConsequences</key>
|
||||
<string>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.</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>IsIncoming</key>
|
||||
<false/>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<dict/>
|
||||
</plist>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -52,17 +53,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 +105,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
|
||||
@@ -138,12 +138,27 @@ 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 '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)'"
|
||||
: .delayed(0.5, """
|
||||
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'
|
||||
name 'PHP Monitor DEV'
|
||||
homepage 'https://phpmon.app'
|
||||
|
||||
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'
|
||||
@@ -171,17 +186,19 @@ 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"
|
||||
"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": [
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -193,12 +210,14 @@ 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
|
||||
],
|
||||
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),
|
||||
@@ -215,6 +234,7 @@ class TestableConfigurations {
|
||||
return configuration
|
||||
}
|
||||
}
|
||||
// swiftlint:enable colon
|
||||
|
||||
class ShellStrings {
|
||||
static var shared = ShellStrings()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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],
|
||||
@@ -107,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ 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 '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)'"
|
||||
] = .delayed(0.5, """
|
||||
cask 'phpmon-dev' do
|
||||
depends_on formula: 'gnu-sed'
|
||||
@@ -50,22 +50,21 @@ 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])
|
||||
}
|
||||
|
||||
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
|
||||
configuration.preferenceOverrides[.automaticBackgroundUpdateCheck] = false
|
||||
|
||||
// Ensure an update is available
|
||||
configuration.shellOutput[
|
||||
"curl -s --max-time 10 '\(Constants.Urls.DevBuildCaskFile.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'
|
||||
|
||||
@@ -85,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
|
||||
@@ -103,9 +125,7 @@ final class UpdateCheckTest: UITestCase {
|
||||
configuration.preferenceOverrides[.automaticBackgroundUpdateCheck] = false
|
||||
|
||||
// Ensure an update is available
|
||||
configuration.shellOutput[
|
||||
"curl -s --max-time 10 '\(Constants.Urls.DevBuildCaskFile.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)
|
||||
|
||||
@@ -1,51 +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 {
|
||||
guard let caskFile = await CaskFile.from(url: Constants.Urls.StableBuildCaskFile) 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"))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,50 +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"))
|
||||
}
|
||||
}
|
||||
29
tests/unit/SwiftTestMigrated/Api/TestableApiTest.swift
Normal file
@@ -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
|
||||
|
||||
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"))
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
import Testing
|
||||
|
||||
@Suite("Integration")
|
||||
struct PackagistTest {
|
||||
@Test func canRetrieveLaravelValetVersion() async {
|
||||
let packageToCheck = "laravel/valet"
|
||||
@@ -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 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 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// CaskFileParserTest.swift
|
||||
// Unit Tests
|
||||
//
|
||||
// Created by Nico Verbruggen on 04/02/2023.
|
||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
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")!
|
||||
}
|
||||
|
||||
@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"))
|
||||
}
|
||||
}
|
||||
@@ -6,36 +6,32 @@
|
||||
// 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, "<test>"),
|
||||
"\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.2.rb": .fake(.text, "<test>"),
|
||||
"\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.3.rb": .fake(.text, "<test>"),
|
||||
"\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.4.rb": .fake(.text, "<test>"),
|
||||
"\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.4.rb": .fake(.text, "<test>")
|
||||
])
|
||||
}
|
||||
|
||||
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")])
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,54 +6,53 @@
|
||||
// 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)
|
||||
.url(forResource: "brew-formula", withExtension: "json")!
|
||||
TestBundle.url(forResource: "brew-formula", withExtension: "json")!
|
||||
}
|
||||
|
||||
func test_can_load_extension_json() throws {
|
||||
static var jsonBrewServicesFile: URL {
|
||||
TestBundle.url(forResource: "brew-services", withExtension: "json")!
|
||||
}
|
||||
|
||||
@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)!
|
||||
).first!
|
||||
|
||||
XCTAssertEqual(package.full_name, "php")
|
||||
XCTAssertEqual(package.aliases.first!, "php@8.2")
|
||||
XCTAssertEqual(package.installed.contains(where: { installed in
|
||||
installed.version.starts(with: "8.2")
|
||||
}), true)
|
||||
#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)
|
||||
}
|
||||
|
||||
static var jsonBrewServicesFile: URL {
|
||||
return Bundle(for: Self.self)
|
||||
.url(forResource: "brew-services", withExtension: "json")!
|
||||
}
|
||||
|
||||
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)!
|
||||
)
|
||||
|
||||
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.
|
||||
func test_can_parse_services_json_from_cli_output() async throws {
|
||||
@Test(.disabled("Uses system command; enable at your own risk"))
|
||||
func can_parse_services_json_from_cli_output() async throws {
|
||||
ActiveShell.useSystem()
|
||||
|
||||
let services = try! JSONDecoder().decode(
|
||||
@@ -65,17 +64,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.
|
||||
func test_can_load_extension_json_from_cli_output() async throws {
|
||||
@Test(.disabled("Uses system command; enable at your own risk"))
|
||||
func can_load_extension_json_from_cli_output() async throws {
|
||||
|
||||
ActiveShell.useSystem()
|
||||
|
||||
let package = try! JSONDecoder().decode(
|
||||
@@ -83,7 +84,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")
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -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"
|
||||
}))
|
||||
}
|
||||
@@ -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 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 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 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 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 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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,41 +6,43 @@
|
||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
class PhpConfigurationFileTest: XCTestCase {
|
||||
import Testing
|
||||
import Foundation
|
||||
|
||||
@Suite(.serialized)
|
||||
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 can_load_extension() throws {
|
||||
ActiveFileSystem.useSystem()
|
||||
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)
|
||||
|
||||
#expect(iniFile != nil)
|
||||
#expect(!iniFile!.extensions.isEmpty)
|
||||
}
|
||||
|
||||
@Test func can_check_key_existence() throws {
|
||||
print(Self.phpIniFileUrl.path)
|
||||
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)!
|
||||
|
||||
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 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 can_customize_configuration_value() throws {
|
||||
let destination = Utility
|
||||
.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
|
||||
|
||||
@@ -48,15 +50,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 +67,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")
|
||||
}
|
||||
|
||||
}
|
||||