1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-11-04 20:20:05 +01:00

♻️ Lint tests, add background update check

This commit is contained in:
2025-09-26 17:00:43 +02:00
parent 1a8fe7e7fc
commit 13013f2513
21 changed files with 309 additions and 86 deletions

View File

@@ -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

View File

@@ -12,6 +12,10 @@
031E2B6A2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; };
031E2B6B2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; };
031E2B6C2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; };
03263A382E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; };
03263A392E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; };
03263A3A2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; };
03263A3B2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; };
033D45982B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; };
033D45992B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; };
033D459A2B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; };
@@ -957,6 +961,7 @@
/* Begin PBXFileReference section */
0309E6662B0D4B2F002AC007 /* BrewExtensionsObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewExtensionsObservable.swift; sourceTree = "<group>"; };
031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewPhpExtension.swift; sourceTree = "<group>"; };
03263A372E86D5E800BD0415 /* UpdateScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateScheduler.swift; sourceTree = "<group>"; };
0336CAAF2B0D0CDA009A1034 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallPhpExtensionCommand.swift; sourceTree = "<group>"; };
033D459D2B0D513900070080 /* RemovePhpExtensionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemovePhpExtensionCommand.swift; sourceTree = "<group>"; };
@@ -2058,6 +2063,7 @@
C495F5AE28A42E080087F70A /* EnvironmentCheck.swift */,
C40FE736282ABA4F00A302C2 /* AppVersion.swift */,
C409349C298EE8E900D25014 /* AppUpdater.swift */,
03263A372E86D5E800BD0415 /* UpdateScheduler.swift */,
);
path = App;
sourceTree = "<group>";
@@ -2679,6 +2685,7 @@
C48DDD0D29C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */,
C45B91532956123A00F4EC78 /* FakeServicesManager.swift in Sources */,
C41C708D28AA7F7900E8D498 /* NoWarningsView.swift in Sources */,
03263A382E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */,
0309E6672B0D4B2F002AC007 /* BrewExtensionsObservable.swift in Sources */,
C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */,
C4205A7E27F4D21800191A39 /* ValetProxy.swift in Sources */,
@@ -3006,6 +3013,7 @@
C471E81A28F9BAE80021E251 /* TimeIntervalExtension.swift in Sources */,
C471E7E128F9BAAB0021E251 /* RealCommand.swift in Sources */,
C471E7E228F9BAAB0021E251 /* ActiveCommand.swift in Sources */,
03263A392E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */,
C471E80A28F9BADC0021E251 /* CreatedFromFile.swift in Sources */,
C471E80528F9BAD40021E251 /* ActivePhpInstallation.swift in Sources */,
C471E80628F9BAD40021E251 /* PhpInstallation.swift in Sources */,
@@ -3286,6 +3294,7 @@
C471E80F28F9BAE80021E251 /* NSMenuExtension.swift in Sources */,
C471E80B28F9BAE80021E251 /* XibLoadable.swift in Sources */,
C471E7F428F9BAC80021E251 /* VersionNumber.swift in Sources */,
03263A3B2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */,
C471E7CB28F9BA5B0021E251 /* TestableCommand.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -3344,6 +3353,7 @@
54D9E0B527E4F51E003B9AD9 /* Key.swift in Sources */,
C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */,
C4C1019C27C65C6F001FACC2 /* Process.swift in Sources */,
03263A3A2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */,
C451AFF72969E40F0078E617 /* HelpButton.swift in Sources */,
C47DF1B0299D5A3B0007055D /* LoginItemManager.swift in Sources */,
C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */,

View File

@@ -31,14 +31,25 @@ struct Constants {
/**
The interval between automatic background update checks.
*/
static let AutomaticUpdateCheckInterval: TimeInterval = 60 // 60.0 * 60 * 24 // 24 hours
static let AutomaticUpdateCheckInterval: TimeInterval = 15.0 // 60.0 * 60 * 24 // 24 hours
/**
The minimum interval that must pass before allowing another
automatic update check. This prevents excessive checking
on frequent app restarts (due to crashes or bad config).
*/
static let MinimumUpdateCheckInterval: TimeInterval = 60 // 60.0 * 60 // 60 minutes
static let MinimumUpdateCheckInterval: TimeInterval = 5.0 // 60.0 * 60 // 60 minutes
/**
Retry intervals for failed automatic update checks.
Uses exponential backoff: 5 min 15 min 1 hr 3 hrs before falling back to normal schedule.
*/
static let UpdateCheckRetryIntervals: [TimeInterval] = [
300, // 5 minutes
900, // 15 minutes
3600, // 1 hour
10800 // 3 hours (final attempt)
]
/**
PHP Monitor supplies a hardcoded list of PHP packages in its own

View File

@@ -10,17 +10,24 @@ import Foundation
import Cocoa
import NVAlert
enum UpdateCheckResult {
case success
case networkError
case parseError
case disabled
}
class AppUpdater {
var caskFile: CaskFile!
var latestVersionOnline: AppVersion!
var interactive: Bool = false
public func checkForUpdates(userInitiated: Bool) async {
public func checkForUpdates(userInitiated: Bool) async -> UpdateCheckResult {
self.interactive = userInitiated
if !interactive && !Preferences.isEnabled(.automaticBackgroundUpdateCheck) {
Log.info("Skipping automatic update check due to user preference.")
return
return .disabled
}
Log.info("The app will search for updates...")
@@ -29,7 +36,8 @@ class AppUpdater {
guard let caskFile = await CaskFile.from(url: caskUrl) else {
Log.err("The contents of the CaskFile at '\(caskUrl.absoluteString)' could not be retrieved.")
return presentCouldNotRetrieveUpdateIfInteractive()
presentCouldNotRetrieveUpdateIfInteractive()
return .networkError
}
self.caskFile = caskFile
@@ -38,7 +46,8 @@ class AppUpdater {
guard let onlineVersion = AppVersion.from(caskFile.version) else {
Log.err("The version string from the CaskFile could not be read.")
return presentCouldNotRetrieveUpdateIfInteractive()
presentCouldNotRetrieveUpdateIfInteractive()
return .parseError
}
latestVersionOnline = onlineVersion
@@ -49,6 +58,8 @@ class AppUpdater {
} else if interactive {
presentNoNewerVersionAvailableAlert()
}
return .success
}
private func presentCouldNotRetrieveUpdateIfInteractive() {

View File

@@ -0,0 +1,113 @@
//
// UpdateScheduler.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 26/09/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Foundation
@MainActor
class UpdateScheduler {
static let shared = UpdateScheduler()
private init() {}
/**
Start the automatic update checking process.
This should be called once during app startup.
*/
func startAutomaticUpdateChecking() async {
await performUpdateCheck()
}
/**
Perform an automatic update check and schedule the next one.
*/
private func performUpdateCheck() async {
guard isNotThrottled() else {
// If we are throttled, just schedule a regular check the regular time from now!
scheduleTimer()
return
}
// This check will be aborted if the preference disallows it in AppUpdater
let result = await AppUpdater().checkForUpdates(userInitiated: false)
switch result {
case .success:
// Reset failure count and record successful check
UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue)
UserDefaults.standard.set(Date(), forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue)
scheduleTimer()
Log.info("Update check completed successfully. Next check scheduled in \(Constants.AutomaticUpdateCheckInterval) seconds.")
case .disabled:
// User disabled automatic checks, don't schedule another
Log.info("Automatic update checks are disabled. No further checks will be scheduled.")
case .networkError, .parseError:
// Handle failures with exponential backoff
handleFailure(result: result)
}
}
/**
Handle update check failures with exponential backoff retry logic.
*/
private func handleFailure(result: UpdateCheckResult) {
let currentFailureCount = UserDefaults.standard.integer(
forKey: PersistentAppState.updateCheckFailureCount.rawValue
)
let newFailureCount = currentFailureCount + 1
UserDefaults.standard.set(newFailureCount, forKey: PersistentAppState.updateCheckFailureCount.rawValue)
let retryInterval: TimeInterval
if newFailureCount <= Constants.UpdateCheckRetryIntervals.count {
// Use exponential backoff
retryInterval = Constants.UpdateCheckRetryIntervals[newFailureCount - 1]
Log.info("Update check failed (\(result)). Retry attempt \(newFailureCount) scheduled in \(retryInterval) seconds.")
} else {
// Exceeded max retries, fall back to normal schedule and reset counter
retryInterval = Constants.AutomaticUpdateCheckInterval
UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue)
Log.info("Update check failed (\(result)). Max retries exceeded. Falling back to normal schedule in \(retryInterval) seconds.")
}
scheduleTimer(after: retryInterval)
}
/**
Determine whether another automatic update check should occur based on the last check timestamp.
Returns true if a check should happen, false otherwise.
*/
private func isNotThrottled() -> Bool {
guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else {
return false
}
let minimumTimeAgo = Date().addingTimeInterval(-Constants.MinimumUpdateCheckInterval)
let lastCheckTime = UserDefaults.standard.object(
forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue
) as? Date
// If no previous check or last check was > minimum time frame, should check now
return lastCheckTime == nil || lastCheckTime! < minimumTimeAgo
}
/**
Schedule a timer to perform an update check after the specified interval.
*/
private func scheduleTimer(after interval: TimeInterval = Constants.AutomaticUpdateCheckInterval) {
Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in
Task {
Log.info("Performing scheduled update check after \(interval) seconds.")
await self.performUpdateCheck()
}
}
Log.info("A new update check will occur in \(interval) seconds from now.")
}
}

View File

@@ -142,7 +142,7 @@ extension MainMenu {
}
} else {
// Check for updates
await performAutomaticUpdateCheck()
await UpdateScheduler.shared.startAutomaticUpdateChecking()
// Check if the linked version has changed between launches of phpmon
await PhpGuard().compareToLastGlobalVersion()
@@ -205,60 +205,4 @@ extension MainMenu {
Log.info("Detected applications: \(appNames)")
}
/**
Perform an automatic update check and schedule the next one.
*/
private func performAutomaticUpdateCheck() async {
guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else {
// The user has chosen not to receive update notifications
return
}
guard automaticUpdateCheckIsNotThrottled() else {
// If we are throttled, just schedule a regular check 24 hours from now
scheduleUpdateCheckTimer()
return
}
await AppUpdater().checkForUpdates(userInitiated: false)
UserDefaults.standard.set(Date(), forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue)
scheduleUpdateCheckTimer()
}
/**
Determine whether another automatic update check should occur based on the last check timestamp.
Returns true if a check should happen, false otherwise.
*/
private func automaticUpdateCheckIsNotThrottled() -> Bool {
guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else {
return false
}
let minimumTimeAgo = Date().addingTimeInterval(-Constants.MinimumUpdateCheckInterval)
let lastCheckTime = UserDefaults.standard.object(
forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue
) as? Date
// If no previous check or last check was > minimum time frame, should check now
return lastCheckTime == nil || lastCheckTime! < minimumTimeAgo
}
/**
Schedule a timer to perform an update check after the specified interval.
*/
private func scheduleUpdateCheckTimer() {
let interval = Constants.AutomaticUpdateCheckInterval
Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in
Task {
Log.info("Performing scheduled update check after \(interval) seconds.")
await self.performAutomaticUpdateCheck()
}
}
Log.info("A new update check will occur in \(interval) seconds from now.")
}
}

View File

@@ -110,6 +110,7 @@ enum PersistentAppState: String {
case wasLaunchedBefore = "launched_before"
case lastAutomaticUpdateCheck = "last_automatic_update_check"
case userFavorites = "user_favorites"
case updateCheckFailureCount = "update_check_failure_count"
}
/**

View File

@@ -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 {

View File

@@ -0,0 +1,126 @@
//
// AutomaticUpdateService.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 26/09/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Foundation
actor UpdateScheduler {
static let shared = UpdateScheduler()
private var currentTimer: Timer?
private init() {}
/**
Start the automatic update checking process. This should be called once during app startup.
*/
func startAutomaticUpdateChecking() async {
await performUpdateCheck()
}
/**
Perform an automatic update check and schedule the next one.
*/
private func performUpdateCheck() async {
guard isNotThrottled() else {
// If we are throttled, just schedule a regular check 24 hours from now.
scheduleTimer()
return
}
let result = await AppUpdater().checkForUpdates(userInitiated: false)
switch result {
case .success:
// Reset failure count and record successful check
UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue)
UserDefaults.standard.set(Date(), forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue)
scheduleTimer()
Log.info("Update check succeeded. Next check in \(Constants.AutomaticUpdateCheckInterval)s.")
case .disabled:
// User disabled automatic checks, don't schedule another
Log.info("Automatic update checks disabled. No further checks scheduled.")
case .networkError, .parseError:
// Handle failures with exponential backoff
handleFailure(result: result)
}
}
/**
Handle update check failures with exponential backoff retry logic.
*/
private func handleFailure(result: UpdateCheckResult) {
let currentFailureCount = UserDefaults.standard.integer(forKey: PersistentAppState.updateCheckFailureCount.rawValue)
let newFailureCount = currentFailureCount + 1
UserDefaults.standard.set(newFailureCount, forKey: PersistentAppState.updateCheckFailureCount.rawValue)
let retryInterval: TimeInterval
if newFailureCount <= Constants.UpdateCheckRetryIntervals.count {
// Use exponential backoff
retryInterval = Constants.UpdateCheckRetryIntervals[newFailureCount - 1]
Log.info("Update check failed (\(result)). Retry \(newFailureCount) in \(retryInterval)s.")
} else {
// Exceeded max retries, fall back to normal schedule and reset counter
retryInterval = Constants.AutomaticUpdateCheckInterval
UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue)
Log.info("Update check failed (\(result)). Max retries exceeded. Normal schedule in \(retryInterval)s.")
}
scheduleTimer(after: retryInterval)
}
/**
Determine whether another automatic update check should occur based on the last check timestamp.
Returns true if a check should happen, false otherwise.
*/
private func isNotThrottled() -> Bool {
guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else {
return false
}
let minimumTimeAgo = Date().addingTimeInterval(-Constants.MinimumUpdateCheckInterval)
let lastCheckTime = UserDefaults.standard.object(
forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue
) as? Date
// If no previous check or last check was > minimum time frame, should check now
return lastCheckTime == nil || lastCheckTime! < minimumTimeAgo
}
/**
Schedule a timer to perform an update check after the specified interval.
*/
private func scheduleTimer(after interval: TimeInterval = Constants.AutomaticUpdateCheckInterval) {
// Invalidate any existing timer
currentTimer?.invalidate()
// Ensure timer is scheduled on main run loop since actors run on background threads
Task { @MainActor in
let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in
Task {
Log.info("Performing scheduled update check after \(interval)s.")
await self.performUpdateCheck()
}
}
// Store timer reference back in actor
await self.setCurrentTimer(timer)
}
Log.info("Next update check scheduled in \(interval)s.")
}
/**
Set the current timer reference. Used to store timer from main thread back to actor.
*/
private func setCurrentTimer(_ timer: Timer) {
currentTimer = timer
}
}

View File

@@ -8,6 +8,7 @@
import Foundation
// swiftlint:disable colon
class TestableConfigurations {
/** A functional, working system setup that is compatible with PHP Monitor. */
static var working: TestableConfiguration {
@@ -179,7 +180,9 @@ class TestableConfigurations {
: .delayed(0.2, "OK"),
"ln -sF ~/.config/valet/valet84.sock ~/.config/valet/valet.sock"
: .instant("OK"),
"/opt/homebrew/bin/brew update >/dev/null && /opt/homebrew/bin/brew outdated --json --formulae": .delayed(2.0, """
"/opt/homebrew/bin/brew update >/dev/null && /opt/homebrew/bin/brew outdated --json --formulae"
: .delayed(2.0,
"""
{
"formulae": [
{
@@ -199,7 +202,7 @@ class TestableConfigurations {
commandOutput: [
"/opt/homebrew/bin/php -r echo ini_get('memory_limit');": "512M",
"/opt/homebrew/bin/php -r echo ini_get('upload_max_filesize');": "512M",
"/opt/homebrew/bin/php -r echo ini_get('post_max_size');": "512M",
"/opt/homebrew/bin/php -r echo ini_get('post_max_size');": "512M"
],
preferenceOverrides: [
.automaticBackgroundUpdateCheck: false
@@ -223,6 +226,7 @@ class TestableConfigurations {
return configuration
}
}
// swiftlint:enable colon
class ShellStrings {
static var shared = ShellStrings()

View File

@@ -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)
}
}

View File

@@ -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()

View File

@@ -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])

View File

@@ -15,7 +15,7 @@ final class ExtensionEnumeratorTest: XCTestCase {
"\(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>")
])
}
@@ -37,5 +37,4 @@ final class ExtensionEnumeratorTest: XCTestCase {
XCTAssertEqual(formulae["8.3"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.3")])
XCTAssertEqual(formulae["8.4"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.4")])
}
}

View File

@@ -22,7 +22,6 @@ class ValetRcTest: XCTestCase {
.url(forResource: "valetrc", withExtension: "broken")!
}
// MARK: - Tests
func test_can_extract_fields_from_valetrc_file() throws {

View File

@@ -71,8 +71,6 @@ class RealFileSystemTest: XCTestCase {
return executablePath
}
func test_can_read_file_as_text() {
let temporaryDirectory = self.createUniqueTemporaryDirectory()
let executable = self.createTestBinaryFile(temporaryDirectory)

View File

@@ -30,7 +30,7 @@ class TestableShellTest: XCTestCase {
XCTAssertEqual("Hello world\nGoodbye world", output.out)
}
func test_fake_shell_synchronous_output() {
let greeting = BatchFakeShellOutput(items: [
.instant("Hello world\n"),

View File

@@ -12,7 +12,7 @@ class TestableConfigurationTest: XCTestCase {
func test_configuration_can_be_saved_as_json() async {
// WORKING
var configuration = TestableConfigurations.working
try! configuration.toJson().write(
toFile: NSHomeDirectory() + "/.phpmon_fconf_working.json",
atomically: true,
@@ -38,4 +38,3 @@ class TestableConfigurationTest: XCTestCase {
)
}
}

View File

@@ -8,7 +8,7 @@
import XCTest
// swiftlint:disable type_body_length
// swiftlint:disable type_body_length file_length
class PhpVersionNumberTest: XCTestCase {
func test_can_deconstruct_php_version() throws {
@@ -51,7 +51,6 @@ class PhpVersionNumberTest: XCTestCase {
XCTAssertEqual(version!.minor, 0)
}
func test_can_check_wildcard_version_constraint() throws {
// Wildcard for patch only
XCTAssertEqual(
@@ -408,3 +407,4 @@ class PhpVersionNumberTest: XCTestCase {
)
}
}
// swiftlint:enable type_body_length file_length

View File

@@ -8,7 +8,7 @@
import Testing
@Suite("Commands")
@Suite("Commands")
struct CommandTest {
@Test

View File

@@ -18,7 +18,7 @@ struct ValetConfigurationTest {
)!
}
@Test("Can load config file")
@Test("Can load config file")
func can_load_config_file() throws {
let json = try? String(
contentsOf: Self.jsonConfigFileUrl,