diff --git a/.swiftlint.yml b/.swiftlint.yml index 85b7f3c..719c6af 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -3,13 +3,25 @@ disabled_rules: - identifier_name - force_try - force_cast - + - private_over_fileprivate + opt_in_rules: - empty_count included: - phpmon - - phpmon-tests + - phpmon-updater + - tests excluded: - phpmon/Vendor + +line_length: + ignores_function_declarations: true + ignores_comments: true + ignores_urls: true + warning: 120 + error: 200 + +analyzer_rules: + - unused_import diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index a2c6d25..4911492 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -12,6 +12,10 @@ 031E2B6A2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; }; 031E2B6B2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; }; 031E2B6C2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; }; + 03263A382E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; }; + 03263A392E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; }; + 03263A3A2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; }; + 03263A3B2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; }; 033D45982B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; }; 033D45992B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; }; 033D459A2B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; }; @@ -957,6 +961,7 @@ /* Begin PBXFileReference section */ 0309E6662B0D4B2F002AC007 /* BrewExtensionsObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewExtensionsObservable.swift; sourceTree = ""; }; 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewPhpExtension.swift; sourceTree = ""; }; + 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateScheduler.swift; sourceTree = ""; }; 0336CAAF2B0D0CDA009A1034 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallPhpExtensionCommand.swift; sourceTree = ""; }; 033D459D2B0D513900070080 /* RemovePhpExtensionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemovePhpExtensionCommand.swift; sourceTree = ""; }; @@ -2058,6 +2063,7 @@ C495F5AE28A42E080087F70A /* EnvironmentCheck.swift */, C40FE736282ABA4F00A302C2 /* AppVersion.swift */, C409349C298EE8E900D25014 /* AppUpdater.swift */, + 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */, ); path = App; sourceTree = ""; @@ -2679,6 +2685,7 @@ C48DDD0D29C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */, C45B91532956123A00F4EC78 /* FakeServicesManager.swift in Sources */, C41C708D28AA7F7900E8D498 /* NoWarningsView.swift in Sources */, + 03263A382E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */, 0309E6672B0D4B2F002AC007 /* BrewExtensionsObservable.swift in Sources */, C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */, C4205A7E27F4D21800191A39 /* ValetProxy.swift in Sources */, @@ -3006,6 +3013,7 @@ C471E81A28F9BAE80021E251 /* TimeIntervalExtension.swift in Sources */, C471E7E128F9BAAB0021E251 /* RealCommand.swift in Sources */, C471E7E228F9BAAB0021E251 /* ActiveCommand.swift in Sources */, + 03263A392E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */, C471E80A28F9BADC0021E251 /* CreatedFromFile.swift in Sources */, C471E80528F9BAD40021E251 /* ActivePhpInstallation.swift in Sources */, C471E80628F9BAD40021E251 /* PhpInstallation.swift in Sources */, @@ -3286,6 +3294,7 @@ C471E80F28F9BAE80021E251 /* NSMenuExtension.swift in Sources */, C471E80B28F9BAE80021E251 /* XibLoadable.swift in Sources */, C471E7F428F9BAC80021E251 /* VersionNumber.swift in Sources */, + 03263A3B2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */, C471E7CB28F9BA5B0021E251 /* TestableCommand.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3344,6 +3353,7 @@ 54D9E0B527E4F51E003B9AD9 /* Key.swift in Sources */, C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */, C4C1019C27C65C6F001FACC2 /* Process.swift in Sources */, + 03263A3A2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */, C451AFF72969E40F0078E617 /* HelpButton.swift in Sources */, C47DF1B0299D5A3B0007055D /* LoginItemManager.swift in Sources */, C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */, diff --git a/phpmon/Common/Core/Constants.swift b/phpmon/Common/Core/Constants.swift index b984224..ead208c 100644 --- a/phpmon/Common/Core/Constants.swift +++ b/phpmon/Common/Core/Constants.swift @@ -31,14 +31,25 @@ struct Constants { /** The interval between automatic background update checks. */ - static let AutomaticUpdateCheckInterval: TimeInterval = 60 // 60.0 * 60 * 24 // 24 hours + static let AutomaticUpdateCheckInterval: TimeInterval = 15.0 // 60.0 * 60 * 24 // 24 hours /** The minimum interval that must pass before allowing another automatic update check. This prevents excessive checking on frequent app restarts (due to crashes or bad config). */ - static let MinimumUpdateCheckInterval: TimeInterval = 60 // 60.0 * 60 // 60 minutes + static let MinimumUpdateCheckInterval: TimeInterval = 5.0 // 60.0 * 60 // 60 minutes + + /** + Retry intervals for failed automatic update checks. + Uses exponential backoff: 5 min → 15 min → 1 hr → 3 hrs before falling back to normal schedule. + */ + static let UpdateCheckRetryIntervals: [TimeInterval] = [ + 300, // 5 minutes + 900, // 15 minutes + 3600, // 1 hour + 10800 // 3 hours (final attempt) + ] /** PHP Monitor supplies a hardcoded list of PHP packages in its own diff --git a/phpmon/Domain/App/AppUpdater.swift b/phpmon/Domain/App/AppUpdater.swift index 1ae8a5e..30afdda 100644 --- a/phpmon/Domain/App/AppUpdater.swift +++ b/phpmon/Domain/App/AppUpdater.swift @@ -10,17 +10,24 @@ import Foundation import Cocoa import NVAlert +enum UpdateCheckResult { + case success + case networkError + case parseError + case disabled +} + class AppUpdater { var caskFile: CaskFile! var latestVersionOnline: AppVersion! var interactive: Bool = false - public func checkForUpdates(userInitiated: Bool) async { + public func checkForUpdates(userInitiated: Bool) async -> UpdateCheckResult { self.interactive = userInitiated if !interactive && !Preferences.isEnabled(.automaticBackgroundUpdateCheck) { Log.info("Skipping automatic update check due to user preference.") - return + return .disabled } Log.info("The app will search for updates...") @@ -29,7 +36,8 @@ class AppUpdater { guard let caskFile = await CaskFile.from(url: caskUrl) else { Log.err("The contents of the CaskFile at '\(caskUrl.absoluteString)' could not be retrieved.") - return presentCouldNotRetrieveUpdateIfInteractive() + presentCouldNotRetrieveUpdateIfInteractive() + return .networkError } self.caskFile = caskFile @@ -38,7 +46,8 @@ class AppUpdater { guard let onlineVersion = AppVersion.from(caskFile.version) else { Log.err("The version string from the CaskFile could not be read.") - return presentCouldNotRetrieveUpdateIfInteractive() + presentCouldNotRetrieveUpdateIfInteractive() + return .parseError } latestVersionOnline = onlineVersion @@ -49,6 +58,8 @@ class AppUpdater { } else if interactive { presentNoNewerVersionAvailableAlert() } + + return .success } private func presentCouldNotRetrieveUpdateIfInteractive() { diff --git a/phpmon/Domain/App/UpdateScheduler.swift b/phpmon/Domain/App/UpdateScheduler.swift new file mode 100644 index 0000000..961d087 --- /dev/null +++ b/phpmon/Domain/App/UpdateScheduler.swift @@ -0,0 +1,113 @@ +// +// UpdateScheduler.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 26/09/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +import Foundation + +@MainActor +class UpdateScheduler { + static let shared = UpdateScheduler() + + private init() {} + + /** + Start the automatic update checking process. + This should be called once during app startup. + */ + func startAutomaticUpdateChecking() async { + await performUpdateCheck() + } + + /** + Perform an automatic update check and schedule the next one. + */ + private func performUpdateCheck() async { + guard isNotThrottled() else { + // If we are throttled, just schedule a regular check the regular time from now! + scheduleTimer() + return + } + + // This check will be aborted if the preference disallows it in AppUpdater + let result = await AppUpdater().checkForUpdates(userInitiated: false) + + switch result { + case .success: + // Reset failure count and record successful check + UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue) + UserDefaults.standard.set(Date(), forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue) + scheduleTimer() + Log.info("Update check completed successfully. Next check scheduled in \(Constants.AutomaticUpdateCheckInterval) seconds.") + + case .disabled: + // User disabled automatic checks, don't schedule another + Log.info("Automatic update checks are disabled. No further checks will be scheduled.") + + case .networkError, .parseError: + // Handle failures with exponential backoff + handleFailure(result: result) + } + } + + /** + Handle update check failures with exponential backoff retry logic. + */ + private func handleFailure(result: UpdateCheckResult) { + let currentFailureCount = UserDefaults.standard.integer( + forKey: PersistentAppState.updateCheckFailureCount.rawValue + ) + let newFailureCount = currentFailureCount + 1 + + UserDefaults.standard.set(newFailureCount, forKey: PersistentAppState.updateCheckFailureCount.rawValue) + + let retryInterval: TimeInterval + if newFailureCount <= Constants.UpdateCheckRetryIntervals.count { + // Use exponential backoff + retryInterval = Constants.UpdateCheckRetryIntervals[newFailureCount - 1] + Log.info("Update check failed (\(result)). Retry attempt \(newFailureCount) scheduled in \(retryInterval) seconds.") + } else { + // Exceeded max retries, fall back to normal schedule and reset counter + retryInterval = Constants.AutomaticUpdateCheckInterval + UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue) + Log.info("Update check failed (\(result)). Max retries exceeded. Falling back to normal schedule in \(retryInterval) seconds.") + } + + scheduleTimer(after: retryInterval) + } + + /** + Determine whether another automatic update check should occur based on the last check timestamp. + Returns true if a check should happen, false otherwise. + */ + private func isNotThrottled() -> Bool { + guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else { + return false + } + + let minimumTimeAgo = Date().addingTimeInterval(-Constants.MinimumUpdateCheckInterval) + let lastCheckTime = UserDefaults.standard.object( + forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue + ) as? Date + + // If no previous check or last check was > minimum time frame, should check now + return lastCheckTime == nil || lastCheckTime! < minimumTimeAgo + } + + /** + Schedule a timer to perform an update check after the specified interval. + */ + private func scheduleTimer(after interval: TimeInterval = Constants.AutomaticUpdateCheckInterval) { + Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in + Task { + Log.info("Performing scheduled update check after \(interval) seconds.") + await self.performUpdateCheck() + } + } + + Log.info("A new update check will occur in \(interval) seconds from now.") + } +} diff --git a/phpmon/Domain/Menu/MainMenu+Startup.swift b/phpmon/Domain/Menu/MainMenu+Startup.swift index 3e12f50..6641650 100644 --- a/phpmon/Domain/Menu/MainMenu+Startup.swift +++ b/phpmon/Domain/Menu/MainMenu+Startup.swift @@ -142,7 +142,7 @@ extension MainMenu { } } else { // Check for updates - await performAutomaticUpdateCheck() + await UpdateScheduler.shared.startAutomaticUpdateChecking() // Check if the linked version has changed between launches of phpmon await PhpGuard().compareToLastGlobalVersion() @@ -205,60 +205,4 @@ extension MainMenu { Log.info("Detected applications: \(appNames)") } - - /** - Perform an automatic update check and schedule the next one. - */ - private func performAutomaticUpdateCheck() async { - guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else { - // The user has chosen not to receive update notifications - return - } - - guard automaticUpdateCheckIsNotThrottled() else { - // If we are throttled, just schedule a regular check 24 hours from now - scheduleUpdateCheckTimer() - return - } - - await AppUpdater().checkForUpdates(userInitiated: false) - - UserDefaults.standard.set(Date(), forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue) - - scheduleUpdateCheckTimer() - } - - /** - Determine whether another automatic update check should occur based on the last check timestamp. - Returns true if a check should happen, false otherwise. - */ - private func automaticUpdateCheckIsNotThrottled() -> Bool { - guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else { - return false - } - - let minimumTimeAgo = Date().addingTimeInterval(-Constants.MinimumUpdateCheckInterval) - let lastCheckTime = UserDefaults.standard.object( - forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue - ) as? Date - - // If no previous check or last check was > minimum time frame, should check now - return lastCheckTime == nil || lastCheckTime! < minimumTimeAgo - } - - /** - Schedule a timer to perform an update check after the specified interval. - */ - private func scheduleUpdateCheckTimer() { - let interval = Constants.AutomaticUpdateCheckInterval - - Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in - Task { - Log.info("Performing scheduled update check after \(interval) seconds.") - await self.performAutomaticUpdateCheck() - } - } - - Log.info("A new update check will occur in \(interval) seconds from now.") - } } diff --git a/phpmon/Domain/Preferences/PreferenceName.swift b/phpmon/Domain/Preferences/PreferenceName.swift index ef0a043..9689004 100644 --- a/phpmon/Domain/Preferences/PreferenceName.swift +++ b/phpmon/Domain/Preferences/PreferenceName.swift @@ -110,6 +110,7 @@ enum PersistentAppState: String { case wasLaunchedBefore = "launched_before" case lastAutomaticUpdateCheck = "last_automatic_update_check" case userFavorites = "user_favorites" + case updateCheckFailureCount = "update_check_failure_count" } /** diff --git a/phpmon/Domain/Preferences/PreferencesTabs.swift b/phpmon/Domain/Preferences/PreferencesTabs.swift index 4b18952..affc379 100644 --- a/phpmon/Domain/Preferences/PreferencesTabs.swift +++ b/phpmon/Domain/Preferences/PreferencesTabs.swift @@ -50,7 +50,6 @@ class AppearancePreferencesVC: GenericPreferenceVC { class MenuStructurePreferencesVC: GenericPreferenceVC { - // swiftlint:disable line_length public static func fromStoryboard() -> GenericPreferenceVC { let vc = NSStoryboard(name: "Main", bundle: nil) .instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC @@ -67,7 +66,6 @@ class MenuStructurePreferencesVC: GenericPreferenceVC { .addView(when: true, vc.displayFeature("prefs.display_misc", .displayMisc)) .addView(when: true, vc.displayFeature("prefs.display_driver", .displayDriver)) } - // swiftlint:enable line_length } class NotificationPreferencesVC: GenericPreferenceVC { diff --git a/phpmon/Domain/Services/AutomaticUpdateService.swift b/phpmon/Domain/Services/AutomaticUpdateService.swift new file mode 100644 index 0000000..2f71e9d --- /dev/null +++ b/phpmon/Domain/Services/AutomaticUpdateService.swift @@ -0,0 +1,126 @@ +// +// AutomaticUpdateService.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 26/09/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +import Foundation + +actor UpdateScheduler { + static let shared = UpdateScheduler() + + private var currentTimer: Timer? + + private init() {} + + /** + Start the automatic update checking process. This should be called once during app startup. + */ + func startAutomaticUpdateChecking() async { + await performUpdateCheck() + } + + /** + Perform an automatic update check and schedule the next one. + */ + private func performUpdateCheck() async { + guard isNotThrottled() else { + // If we are throttled, just schedule a regular check 24 hours from now. + scheduleTimer() + return + } + + let result = await AppUpdater().checkForUpdates(userInitiated: false) + + switch result { + case .success: + // Reset failure count and record successful check + UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue) + UserDefaults.standard.set(Date(), forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue) + scheduleTimer() + Log.info("Update check succeeded. Next check in \(Constants.AutomaticUpdateCheckInterval)s.") + + case .disabled: + // User disabled automatic checks, don't schedule another + Log.info("Automatic update checks disabled. No further checks scheduled.") + + case .networkError, .parseError: + // Handle failures with exponential backoff + handleFailure(result: result) + } + } + + /** + Handle update check failures with exponential backoff retry logic. + */ + private func handleFailure(result: UpdateCheckResult) { + let currentFailureCount = UserDefaults.standard.integer(forKey: PersistentAppState.updateCheckFailureCount.rawValue) + let newFailureCount = currentFailureCount + 1 + + UserDefaults.standard.set(newFailureCount, forKey: PersistentAppState.updateCheckFailureCount.rawValue) + + let retryInterval: TimeInterval + if newFailureCount <= Constants.UpdateCheckRetryIntervals.count { + // Use exponential backoff + retryInterval = Constants.UpdateCheckRetryIntervals[newFailureCount - 1] + Log.info("Update check failed (\(result)). Retry \(newFailureCount) in \(retryInterval)s.") + } else { + // Exceeded max retries, fall back to normal schedule and reset counter + retryInterval = Constants.AutomaticUpdateCheckInterval + UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue) + Log.info("Update check failed (\(result)). Max retries exceeded. Normal schedule in \(retryInterval)s.") + } + + scheduleTimer(after: retryInterval) + } + + /** + Determine whether another automatic update check should occur based on the last check timestamp. + Returns true if a check should happen, false otherwise. + */ + private func isNotThrottled() -> Bool { + guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else { + return false + } + + let minimumTimeAgo = Date().addingTimeInterval(-Constants.MinimumUpdateCheckInterval) + let lastCheckTime = UserDefaults.standard.object( + forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue + ) as? Date + + // If no previous check or last check was > minimum time frame, should check now + return lastCheckTime == nil || lastCheckTime! < minimumTimeAgo + } + + /** + Schedule a timer to perform an update check after the specified interval. + */ + private func scheduleTimer(after interval: TimeInterval = Constants.AutomaticUpdateCheckInterval) { + // Invalidate any existing timer + currentTimer?.invalidate() + + // Ensure timer is scheduled on main run loop since actors run on background threads + Task { @MainActor in + let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in + Task { + Log.info("Performing scheduled update check after \(interval)s.") + await self.performUpdateCheck() + } + } + + // Store timer reference back in actor + await self.setCurrentTimer(timer) + } + + Log.info("Next update check scheduled in \(interval)s.") + } + + /** + Set the current timer reference. Used to store timer from main thread back to actor. + */ + private func setCurrentTimer(_ timer: Timer) { + currentTimer = timer + } +} diff --git a/tests/Shared/TestableConfigurations.swift b/tests/Shared/TestableConfigurations.swift index 9f6c2de..4b5bdcf 100644 --- a/tests/Shared/TestableConfigurations.swift +++ b/tests/Shared/TestableConfigurations.swift @@ -8,6 +8,7 @@ import Foundation +// swiftlint:disable colon class TestableConfigurations { /** A functional, working system setup that is compatible with PHP Monitor. */ static var working: TestableConfiguration { @@ -179,7 +180,9 @@ class TestableConfigurations { : .delayed(0.2, "OK"), "ln -sF ~/.config/valet/valet84.sock ~/.config/valet/valet.sock" : .instant("OK"), - "/opt/homebrew/bin/brew update >/dev/null && /opt/homebrew/bin/brew outdated --json --formulae": .delayed(2.0, """ + "/opt/homebrew/bin/brew update >/dev/null && /opt/homebrew/bin/brew outdated --json --formulae" + : .delayed(2.0, + """ { "formulae": [ { @@ -199,7 +202,7 @@ class TestableConfigurations { commandOutput: [ "/opt/homebrew/bin/php -r echo ini_get('memory_limit');": "512M", "/opt/homebrew/bin/php -r echo ini_get('upload_max_filesize');": "512M", - "/opt/homebrew/bin/php -r echo ini_get('post_max_size');": "512M", + "/opt/homebrew/bin/php -r echo ini_get('post_max_size');": "512M" ], preferenceOverrides: [ .automaticBackgroundUpdateCheck: false @@ -223,6 +226,7 @@ class TestableConfigurations { return configuration } } +// swiftlint:enable colon class ShellStrings { static var shared = ShellStrings() diff --git a/tests/feature/FeatureTestCase.swift b/tests/feature/FeatureTestCase.swift index a7282af..0d09890 100644 --- a/tests/feature/FeatureTestCase.swift +++ b/tests/feature/FeatureTestCase.swift @@ -17,7 +17,7 @@ class FeatureTestCase: XCTestCase { return fs as! TestableFileSystem } - fatalError("The active filesystem is not a TestableFileSystem. Please use `ActiveFileSystem` to use the fake filesystem.") + fatalError("The active filesystem is not a TestableFileSystem. Please use `ActiveFileSystem`.") } public func assertFileSystemHas( @@ -44,6 +44,4 @@ class FeatureTestCase: XCTestCase { ) { XCTAssertEqual(contents, fakeFileSystem.files[path]?.content, file: file, line: line) } - } - diff --git a/tests/ui/DomainsListTest.swift b/tests/ui/DomainsListTest.swift index b1d79fe..2fe2610 100644 --- a/tests/ui/DomainsListTest.swift +++ b/tests/ui/DomainsListTest.swift @@ -35,7 +35,7 @@ final class DomainsListTest: UITestCase { searchField.click() searchField.typeText("non-existent thing") Thread.sleep(forTimeInterval: 0.2) - XCTAssertTrue(window.tables.tableRows.count == 0) + XCTAssertTrue(window.tables.tableRows.count == 0) // swiftlint:disable:this empty_count searchField.clearText() searchField.click() diff --git a/tests/ui/StartupTest.swift b/tests/ui/StartupTest.swift index 2fd4d30..2f4b3c2 100644 --- a/tests/ui/StartupTest.swift +++ b/tests/ui/StartupTest.swift @@ -26,7 +26,7 @@ final class StartupTest: UITestCase { assertAllExist([ app.dialogs["generic.notice".localized], app.staticTexts["startup.errors.php_binary.title".localized], - app.buttons["generic.ok".localized], + app.buttons["generic.ok".localized] ]) click(app.buttons["generic.ok".localized]) diff --git a/tests/unit/Parsers/ExtensionEnumeratorTest.swift b/tests/unit/Parsers/ExtensionEnumeratorTest.swift index 8ea8111..774d9a3 100644 --- a/tests/unit/Parsers/ExtensionEnumeratorTest.swift +++ b/tests/unit/Parsers/ExtensionEnumeratorTest.swift @@ -15,7 +15,7 @@ final class ExtensionEnumeratorTest: XCTestCase { "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.1.rb": .fake(.text, ""), "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.2.rb": .fake(.text, ""), "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.3.rb": .fake(.text, ""), - "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.4.rb": .fake(.text, ""), + "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.4.rb": .fake(.text, "") ]) } @@ -37,5 +37,4 @@ final class ExtensionEnumeratorTest: XCTestCase { XCTAssertEqual(formulae["8.3"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.3")]) XCTAssertEqual(formulae["8.4"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.4")]) } - } diff --git a/tests/unit/Parsers/ValetRcTest.swift b/tests/unit/Parsers/ValetRcTest.swift index a89c028..3a4428f 100644 --- a/tests/unit/Parsers/ValetRcTest.swift +++ b/tests/unit/Parsers/ValetRcTest.swift @@ -22,7 +22,6 @@ class ValetRcTest: XCTestCase { .url(forResource: "valetrc", withExtension: "broken")! } - // MARK: - Tests func test_can_extract_fields_from_valetrc_file() throws { diff --git a/tests/unit/Testables/Filesystem/RealFileSystemTest.swift b/tests/unit/Testables/Filesystem/RealFileSystemTest.swift index 8831dc2..b43dc0f 100644 --- a/tests/unit/Testables/Filesystem/RealFileSystemTest.swift +++ b/tests/unit/Testables/Filesystem/RealFileSystemTest.swift @@ -71,8 +71,6 @@ class RealFileSystemTest: XCTestCase { return executablePath } - - func test_can_read_file_as_text() { let temporaryDirectory = self.createUniqueTemporaryDirectory() let executable = self.createTestBinaryFile(temporaryDirectory) diff --git a/tests/unit/Testables/Shell/TestableShellTest.swift b/tests/unit/Testables/Shell/TestableShellTest.swift index 1df17af..d61ad15 100644 --- a/tests/unit/Testables/Shell/TestableShellTest.swift +++ b/tests/unit/Testables/Shell/TestableShellTest.swift @@ -30,7 +30,7 @@ class TestableShellTest: XCTestCase { XCTAssertEqual("Hello world\nGoodbye world", output.out) } - + func test_fake_shell_synchronous_output() { let greeting = BatchFakeShellOutput(items: [ .instant("Hello world\n"), diff --git a/tests/unit/Testables/TestableConfigurationTest.swift b/tests/unit/Testables/TestableConfigurationTest.swift index 511cde4..1550544 100644 --- a/tests/unit/Testables/TestableConfigurationTest.swift +++ b/tests/unit/Testables/TestableConfigurationTest.swift @@ -12,7 +12,7 @@ class TestableConfigurationTest: XCTestCase { func test_configuration_can_be_saved_as_json() async { // WORKING var configuration = TestableConfigurations.working - + try! configuration.toJson().write( toFile: NSHomeDirectory() + "/.phpmon_fconf_working.json", atomically: true, @@ -38,4 +38,3 @@ class TestableConfigurationTest: XCTestCase { ) } } - diff --git a/tests/unit/Versions/PhpVersionNumberTest.swift b/tests/unit/Versions/PhpVersionNumberTest.swift index d12f3dc..34b6893 100644 --- a/tests/unit/Versions/PhpVersionNumberTest.swift +++ b/tests/unit/Versions/PhpVersionNumberTest.swift @@ -8,7 +8,7 @@ import XCTest -// swiftlint:disable type_body_length +// swiftlint:disable type_body_length file_length class PhpVersionNumberTest: XCTestCase { func test_can_deconstruct_php_version() throws { @@ -51,7 +51,6 @@ class PhpVersionNumberTest: XCTestCase { XCTAssertEqual(version!.minor, 0) } - func test_can_check_wildcard_version_constraint() throws { // Wildcard for patch only XCTAssertEqual( @@ -408,3 +407,4 @@ class PhpVersionNumberTest: XCTestCase { ) } } +// swiftlint:enable type_body_length file_length diff --git a/tests/unit/_ST/Commands/CommandTest.swift b/tests/unit/_ST/Commands/CommandTest.swift index 90e6f2d..5b7feee 100644 --- a/tests/unit/_ST/Commands/CommandTest.swift +++ b/tests/unit/_ST/Commands/CommandTest.swift @@ -8,7 +8,7 @@ import Testing -@Suite("Commands") +@Suite("Commands") struct CommandTest { @Test diff --git a/tests/unit/_ST/Parsers/ValetConfigurationTest.swift b/tests/unit/_ST/Parsers/ValetConfigurationTest.swift index c4168e3..d251cc8 100644 --- a/tests/unit/_ST/Parsers/ValetConfigurationTest.swift +++ b/tests/unit/_ST/Parsers/ValetConfigurationTest.swift @@ -18,7 +18,7 @@ struct ValetConfigurationTest { )! } - @Test("Can load config file") + @Test("Can load config file") func can_load_config_file() throws { let json = try? String( contentsOf: Self.jsonConfigFileUrl,