diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 17a2ad92..d756716b 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -133,6 +133,14 @@ 0381313F2F4DF3BA00653177 /* CommandHistoryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0381313C2F4DF3B400653177 /* CommandHistoryRow.swift */; }; 038131402F4DF3BA00653177 /* CommandHistoryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0381313C2F4DF3B400653177 /* CommandHistoryRow.swift */; }; 038131412F4DFB5B00653177 /* TrackedShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46EBC4A28DB966A007ACC74 /* TrackedShell.swift */; }; + 038131432F4E00C300653177 /* TrackedTestableShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038131422F4E00C000653177 /* TrackedTestableShell.swift */; }; + 038131442F4E00C300653177 /* TrackedTestableShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038131422F4E00C000653177 /* TrackedTestableShell.swift */; }; + 038131452F4E00C300653177 /* TrackedTestableShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038131422F4E00C000653177 /* TrackedTestableShell.swift */; }; + 038131462F4E00C300653177 /* TrackedTestableShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038131422F4E00C000653177 /* TrackedTestableShell.swift */; }; + 038131482F4E00D500653177 /* TrackedTestableCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038131472F4E00D100653177 /* TrackedTestableCommand.swift */; }; + 038131492F4E00D500653177 /* TrackedTestableCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038131472F4E00D100653177 /* TrackedTestableCommand.swift */; }; + 0381314A2F4E00D500653177 /* TrackedTestableCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038131472F4E00D100653177 /* TrackedTestableCommand.swift */; }; + 0381314B2F4E00D500653177 /* TrackedTestableCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038131472F4E00D100653177 /* TrackedTestableCommand.swift */; }; 0386B0B42ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; }; 0386B0B52ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; }; 0386B0B62ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; }; @@ -1118,6 +1126,8 @@ 037F441C2EDB9195002EBF75 /* ConfigWatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigWatchManager.swift; sourceTree = ""; }; 037F44212EDB92EC002EBF75 /* HomebrewWatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewWatchManager.swift; sourceTree = ""; }; 0381313C2F4DF3B400653177 /* CommandHistoryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandHistoryRow.swift; sourceTree = ""; }; + 038131422F4E00C000653177 /* TrackedTestableShell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackedTestableShell.swift; sourceTree = ""; }; + 038131472F4E00D100653177 /* TrackedTestableCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackedTestableCommand.swift; sourceTree = ""; }; 0386B0B32ED36C3D00CA6795 /* Locked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locked.swift; sourceTree = ""; }; 0386B0B82ED36DF800CA6795 /* LockedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedTests.swift; sourceTree = ""; }; 038A2B7D2EDDB24400173ACF /* App+UUID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+UUID.swift"; sourceTree = ""; }; @@ -1533,6 +1543,15 @@ path = Watchers; sourceTree = ""; }; + 0381314C2F4E00E200653177 /* Testable */ = { + isa = PBXGroup; + children = ( + 038131472F4E00D100653177 /* TrackedTestableCommand.swift */, + 038131422F4E00C000653177 /* TrackedTestableShell.swift */, + ); + path = Testable; + sourceTree = ""; + }; 0386B0BD2ED36E2500CA6795 /* Helpers */ = { isa = PBXGroup; children = ( @@ -1591,6 +1610,7 @@ 03D0F9772F4DE7E800613D1E /* Monitoring */ = { isa = PBXGroup; children = ( + 0381314C2F4E00E200653177 /* Testable */, 031A80DB2F4CF1690016F7DD /* CommandTracker.swift */, C46EBC4A28DB966A007ACC74 /* TrackedShell.swift */, C4E49DED28F764A00026AC4E /* TrackedCommand.swift */, @@ -3080,6 +3100,7 @@ 031D74872F46307300D4FF48 /* AddSiteView.swift in Sources */, 0386B0B52ED36C3D00CA6795 /* Locked.swift in Sources */, C4EED88927A48778006D7272 /* InterAppHandler.swift in Sources */, + 038131482F4E00D500653177 /* TrackedTestableCommand.swift in Sources */, C40C7F1E2772136000DDDCDC /* PhpEnvironments.swift in Sources */, C4B79EB629CA387F00A483EE /* BrewPhpFormulaeHandler.swift in Sources */, C476FF9822B0DD830098105B /* Alert.swift in Sources */, @@ -3133,6 +3154,7 @@ C47015022C46D6910069AAE7 /* NVAlertExtension.swift in Sources */, C4E49DEA28F7643D0026AC4E /* CommandProtocol.swift in Sources */, C4E49DF028F764A00026AC4E /* TrackedCommand.swift in Sources */, + 038131452F4E00C300653177 /* TrackedTestableShell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3298,6 +3320,7 @@ C46DC7A62C7B5BC900F19D17 /* Favorites.swift in Sources */, 038A2B812EDDB24C00173ACF /* App+UUID.swift in Sources */, C471E7E728F9BAC20021E251 /* Constants.swift in Sources */, + 0381314B2F4E00D500653177 /* TrackedTestableCommand.swift in Sources */, C471E81628F9BAE80021E251 /* DateExtension.swift in Sources */, 03CC1FF72E3D23130050FC18 /* ZshRunCommand.swift in Sources */, C469E700294CF7B200A82AB2 /* FakeValetProxy.swift in Sources */, @@ -3344,6 +3367,7 @@ C471E82928F9BB330021E251 /* Valet.swift in Sources */, C471E80728F9BAD40021E251 /* PhpConfigurationFile.swift in Sources */, 0329A9A32E92A69000A62A12 /* WarningManager+Evaluations.swift in Sources */, + 038131462F4E00C300653177 /* TrackedTestableShell.swift in Sources */, C471E7D528F9BA8F0021E251 /* TestableConfigurations.swift in Sources */, C436B39F29F3C42500B6A64E /* PreferencesTabs.swift in Sources */, 03CC1FE82E3D22120050FC18 /* InstallHomebrew.swift in Sources */, @@ -3509,7 +3533,9 @@ C4B79ECE29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */, C471E8E028F9BB8F0021E251 /* Preset.swift in Sources */, C471E8E128F9BB8F0021E251 /* PresetHelper.swift in Sources */, + 038131492F4E00D500653177 /* TrackedTestableCommand.swift in Sources */, 031D747D2F46225600D4FF48 /* SimpleButton.swift in Sources */, + 038131442F4E00C300653177 /* TrackedTestableShell.swift in Sources */, C471E8E228F9BB8F0021E251 /* WarningView.swift in Sources */, C471E8E328F9BB8F0021E251 /* PhpDoctorView.swift in Sources */, 0A1A6208D3DD2495FBD8569B /* CommandHistoryView.swift in Sources */, @@ -3741,6 +3767,7 @@ C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */, C4C3643A28AE4FCE00C0770E /* StatusMenu+Items.swift in Sources */, C42759682627662800093CAE /* NSMenuExtension.swift in Sources */, + 038131432F4E00C300653177 /* TrackedTestableShell.swift in Sources */, 03BFF52D2E313244007F96FA /* StatusMenu+Driver.swift in Sources */, C4AFC4B429C4F43300BF4E0D /* HomebrewUpgradableTest.swift in Sources */, 037F441E2EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */, @@ -3884,6 +3911,7 @@ C40934AB298EEDA900D25014 /* CaskFileParserTest.swift in Sources */, C436B39E29F3C42500B6A64E /* PreferencesTabs.swift in Sources */, C43BCD4529FBEF40001547BC /* ModifyPhpVersionCommand.swift in Sources */, + 0381314A2F4E00D500653177 /* TrackedTestableCommand.swift in Sources */, C4551657297AED18009B8466 /* ValetRcTest.swift in Sources */, C464ADAD275A7A3F003FCD53 /* DomainListWindowController.swift in Sources */, C40C7F1F2772136000DDDCDC /* PhpEnvironments.swift in Sources */, diff --git a/phpmon/Common/Monitoring/Testable/TrackedTestableCommand.swift b/phpmon/Common/Monitoring/Testable/TrackedTestableCommand.swift new file mode 100644 index 00000000..bc00df1e --- /dev/null +++ b/phpmon/Common/Monitoring/Testable/TrackedTestableCommand.swift @@ -0,0 +1,38 @@ +// +// TrackedTestableCommand.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 24/02/2026. +// Copyright © 2026 Nico Verbruggen. All rights reserved. +// + +import Foundation + +final class TrackableTestableCommand: TestableCommand { + private let commandTracker: CommandTracker + + init(commands: [String: String], _ commandTracker: CommandTracker) { + self.commandTracker = commandTracker + super.init(commands: commands) + } + + override func execute( + path: String, + arguments: [String], + trimNewlines: Bool, + withStandardError: Bool + ) -> String { + let commandDescription = "\(path) \(arguments.joined(separator: " "))" + let trackingId = commandTracker.trackFromAnyThread(commandDescription) + defer { + commandTracker.completeFromAnyThread(trackingId) + } + + return super.execute( + path: path, + arguments: arguments, + trimNewlines: trimNewlines, + withStandardError: withStandardError + ) + } +} diff --git a/phpmon/Common/Monitoring/Testable/TrackedTestableShell.swift b/phpmon/Common/Monitoring/Testable/TrackedTestableShell.swift new file mode 100644 index 00000000..ca1c548a --- /dev/null +++ b/phpmon/Common/Monitoring/Testable/TrackedTestableShell.swift @@ -0,0 +1,61 @@ +// +// TrackedTestableShell.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 24/02/2026. +// Copyright © 2026 Nico Verbruggen. All rights reserved. +// + +import Foundation + +final class TrackableTestableShell: TestableShell { + private let commandTracker: CommandTracker + + init(expectations: [String: BatchFakeShellOutput], _ commandTracker: CommandTracker) { + self.commandTracker = commandTracker + super.init(expectations: expectations) + } + + override func sync(_ command: String) -> ShellOutput { + let trackingId = commandTracker.trackFromAnyThread(command) + defer { + commandTracker.completeFromAnyThread(trackingId) + } + return super.sync(command) + } + + @discardableResult + override func pipe(_ command: String) async -> ShellOutput { + let trackingId = commandTracker.trackFromAnyThread(command) + defer { + commandTracker.completeFromAnyThread(trackingId) + } + return await super.pipe(command) + } + + @discardableResult + override func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput { + let trackingId = commandTracker.trackFromAnyThread(command) + defer { + commandTracker.completeFromAnyThread(trackingId) + } + return await super.pipe(command, timeout: timeout) + } + + @discardableResult + override func attach( + _ command: String, + didReceiveOutput: @escaping (String, ShellStream) -> Void, + withTimeout timeout: TimeInterval + ) async throws -> (Process, ShellOutput) { + let trackingId = commandTracker.trackFromAnyThread(command) + defer { + commandTracker.completeFromAnyThread(trackingId) + } + return try await super.attach( + command, + didReceiveOutput: didReceiveOutput, + withTimeout: timeout + ) + } +} diff --git a/phpmon/Container/Container.swift b/phpmon/Container/Container.swift index 0b375809..8b1ecdd6 100644 --- a/phpmon/Container/Container.swift +++ b/phpmon/Container/Container.swift @@ -111,12 +111,22 @@ class Container: @unchecked Sendable { fileSystemFiles: [String: FakeFile] = [:], commands: [String: String] = [:], webApiGetResponses: [URL: FakeWebApiResponse] = [:], - webApiPostResponses: [URL: FakeWebApiResponse] = [:] + webApiPostResponses: [URL: FakeWebApiResponse] = [:], + commandTracking: Bool = true, ) { self.commandTracker = CommandTracker() - self.shell = TestableShell(expectations: shellExpectations) + + // Depending on whether we want to fire command tracking, load different handlers + if commandTracking { + self.shell = TrackableTestableShell(expectations: shellExpectations, commandTracker) + self.command = TrackableTestableCommand(commands: commands, commandTracker) + } else { + self.shell = TestableShell(expectations: shellExpectations) + self.command = TestableCommand(commands: commands) + } + self.filesystem = TestableFileSystem(files: fileSystemFiles) - self.command = TestableCommand(commands: commands) + self.webApi = TestableWebApi( getResponses: webApiGetResponses, postResponses: webApiPostResponses diff --git a/tests/ui/MainMenuTest.swift b/tests/ui/MainMenuTest.swift index 1b308908..1cb3fa62 100644 --- a/tests/ui/MainMenuTest.swift +++ b/tests/ui/MainMenuTest.swift @@ -50,6 +50,16 @@ final class MainMenuTest: UITestCase { app.mainMenuItem(withText: "mi_view_onboarding".localized).click() } + final func test_can_open_command_history() throws { + let app = launch(openMenu: true) + app.mainMenuItem(withText: "mi_other".localized).hover() + app.mainMenuItem(withText: "mi_view_command_history".localized).click() + + assertExists(app.windows["command_history.title".localized], 2.0) + + Thread.sleep(forTimeInterval: 5) + } + final func test_can_open_about() throws { let app = launch(openMenu: true) app.mainMenuItem(withText: "mi_about".localized).click()