From 8eca1a55b5a230d1d33f64fe53bdcd4b97e348ca Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Thu, 16 Oct 2025 19:01:55 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Container=20refactoring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 10 +++++++++ phpmon/Container/Container+Fake.swift | 30 ++++++++++++++++++++++++- phpmon/Container/Container+Real.swift | 15 +++++++++++++ phpmon/Container/Container.swift | 32 +++++++++++++-------------- 4 files changed, 69 insertions(+), 18 deletions(-) create mode 100644 phpmon/Container/Container+Real.swift diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 70614d4b..cb71eabd 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -86,6 +86,10 @@ 03BFF52D2E313244007F96FA /* StatusMenu+Driver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BFF52B2E313240007F96FA /* StatusMenu+Driver.swift */; }; 03BFF52E2E313244007F96FA /* StatusMenu+Driver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BFF52B2E313240007F96FA /* StatusMenu+Driver.swift */; }; 03BFF52F2E313244007F96FA /* StatusMenu+Driver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BFF52B2E313240007F96FA /* StatusMenu+Driver.swift */; }; + 03C099442EA15C8E00B76D43 /* Container+Real.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C099432EA15C8B00B76D43 /* Container+Real.swift */; }; + 03C099452EA15C8E00B76D43 /* Container+Real.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C099432EA15C8B00B76D43 /* Container+Real.swift */; }; + 03C099462EA15C8E00B76D43 /* Container+Real.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C099432EA15C8B00B76D43 /* Container+Real.swift */; }; + 03C099472EA15C8E00B76D43 /* Container+Real.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C099432EA15C8B00B76D43 /* Container+Real.swift */; }; 03CC1FE52E3D22120050FC18 /* InstallHomebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CC1FE42E3D220F0050FC18 /* InstallHomebrew.swift */; }; 03CC1FE62E3D22120050FC18 /* InstallHomebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CC1FE42E3D220F0050FC18 /* InstallHomebrew.swift */; }; 03CC1FE72E3D22120050FC18 /* InstallHomebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CC1FE42E3D220F0050FC18 /* InstallHomebrew.swift */; }; @@ -1002,6 +1006,7 @@ 039E1D782E5F0F2C0072D13D /* ValetUpgrader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetUpgrader.swift; sourceTree = ""; }; 03BFF5262E312C39007F96FA /* Startup+Timers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Startup+Timers.swift"; sourceTree = ""; }; 03BFF52B2E313240007F96FA /* StatusMenu+Driver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusMenu+Driver.swift"; sourceTree = ""; }; + 03C099432EA15C8B00B76D43 /* Container+Real.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Container+Real.swift"; sourceTree = ""; }; 03CC1FE42E3D220F0050FC18 /* InstallHomebrew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallHomebrew.swift; sourceTree = ""; }; 03CC1FF32E3D230B0050FC18 /* ZshRunCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZshRunCommand.swift; sourceTree = ""; }; 03FE39E52E81682800B7B5AC /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = ""; }; @@ -1426,6 +1431,7 @@ isa = PBXGroup; children = ( 0329A9A02E92A2A800A62A12 /* Container.swift */, + 03C099432EA15C8B00B76D43 /* Container+Real.swift */, 031F247F2EA1071700CFB8D9 /* Container+Fake.swift */, ); path = Container; @@ -2738,6 +2744,7 @@ 039616102E74A61E002DD7F6 /* LoggableEvent.swift in Sources */, C4C0E8DF27F88AEB002D32A9 /* FakeDomainScanner.swift in Sources */, C44B3A4628E5C70100718CB1 /* TimeIntervalExtension.swift in Sources */, + 03C099452EA15C8E00B76D43 /* Container+Real.swift in Sources */, C4463FCC29804BCB007B93D5 /* RCFile.swift in Sources */, C44264BE2850B86C007400F1 /* SwiftUIHelper.swift in Sources */, C4E9D2C02878B336008FFDAD /* OnboardingView.swift in Sources */, @@ -2929,6 +2936,7 @@ C471E83F28F9BB650021E251 /* AppDelegate.swift in Sources */, C471E84028F9BB650021E251 /* AppDelegate+MenuOutlets.swift in Sources */, C4D36603291132B7006BD146 /* ValetScanners.swift in Sources */, + 03C099442EA15C8E00B76D43 /* Container+Real.swift in Sources */, C471E84128F9BB650021E251 /* AppDelegate+Notifications.swift in Sources */, C471E84228F9BB650021E251 /* AppDelegate+InterApp.swift in Sources */, C471E84328F9BB650021E251 /* App.swift in Sources */, @@ -3219,6 +3227,7 @@ C471E8D128F9BB8F0021E251 /* MenuBarIcons.swift in Sources */, C471E8D228F9BB8F0021E251 /* Stats.swift in Sources */, C471E8D328F9BB8F0021E251 /* GlobalKeybindPreference.swift in Sources */, + 03C099462EA15C8E00B76D43 /* Container+Real.swift in Sources */, 039C29152E8AA163007F5FAB /* ActiveApi.swift in Sources */, C471E8D528F9BB8F0021E251 /* CheckboxPreferenceView.swift in Sources */, C471E8D728F9BB8F0021E251 /* SelectPreferenceView.swift in Sources */, @@ -3366,6 +3375,7 @@ 54B48B60275F66AE006D90C5 /* Application.swift in Sources */, 039C291A2E8AA314007F5FAB /* TestableApi.swift in Sources */, C4FE011228084FC200D1DE6D /* SelectionVC.swift in Sources */, + 03C099472EA15C8E00B76D43 /* Container+Real.swift in Sources */, C4D3661B291173EA006BD146 /* DictionaryExtension.swift in Sources */, C45D654D29F52F74004C28F9 /* BrewPermissionFixer.swift in Sources */, C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */, diff --git a/phpmon/Container/Container+Fake.swift b/phpmon/Container/Container+Fake.swift index eda1e906..ec8d3559 100644 --- a/phpmon/Container/Container+Fake.swift +++ b/phpmon/Container/Container+Fake.swift @@ -7,7 +7,10 @@ // extension Container { - public func overrideFake( + /** + Manually specify what overrides need to be active for the container. + */ + private func overrideFake( shellExpectations: [String: BatchFakeShellOutput] = [:], fileSystemFiles: [String: FakeFile] = [:], commands: [String: String] = [:] @@ -17,18 +20,43 @@ extension Container { self.command = TestableCommand(commands: commands) } + /** + Use a `TestableConfiguration` as the basis for shell, filesystem and more. + This is used for testing scenarios to avoid needing to have a specific system configuration. + Ideal for feature or UI tests, where a complete "computer configuration" needs to be mimicked. + */ + public func overrideWith(config: TestableConfiguration) { + self.overrideFake( + shellExpectations: config.shellOutput, + fileSystemFiles: config.filesystem, + commands: config.commandOutput + ) + } + + /** + Create a new DI `Container` with fake shell responses, filesystem structure and given commands. + Ideal for testing without a complex TestableConfiguration, so great for unit tests that + require injecting a new `Container` instance without requiring a complex setup process. + */ public static func fake( shell: [String: BatchFakeShellOutput] = [:], files: [String: FakeFile] = [:], commands: [String: String] = [:] ) -> Container { + // Create a new container let container = Container() + + // Fill the container with production (real) components container.prepare() + + // Replace the key ones with fake ones, so we don't touch the tester's OS, filesystem, etc. container.overrideFake( shellExpectations: shell, fileSystemFiles: files, commands: commands ) + + // Return the newly created container return container } } diff --git a/phpmon/Container/Container+Real.swift b/phpmon/Container/Container+Real.swift new file mode 100644 index 00000000..94029a41 --- /dev/null +++ b/phpmon/Container/Container+Real.swift @@ -0,0 +1,15 @@ +// +// Container+Real.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 16/10/2025. +// Copyright © 2025 Nico Verbruggen. All rights reserved. +// + +extension Container { + public static func real() -> Container { + let container = Container() + container.prepare() + return container + } +} diff --git a/phpmon/Container/Container.swift b/phpmon/Container/Container.swift index 5cfefdba..cee7addf 100644 --- a/phpmon/Container/Container.swift +++ b/phpmon/Container/Container.swift @@ -12,37 +12,35 @@ class Container { var filesystem: FileSystemProtocol! var command: CommandProtocol! - // Additional abstractions + // Extra abstractions var paths: Paths! var phpEnvs: PhpEnvironments! var favorites: Favorites! + var warningManager: WarningManager! // pending rename? - // TODO: Pending a rename - var warningManager: WarningManager! - + /// + /// The initializer is empty. You must call `prepare` to enable the container. + /// To avoid issues with unsafe access, the actual objects are set in `prepare`. + /// `self` is not available in this constructor, after all. The alternative + /// is to use lazy variables here, but I don't think it's that clean, especially + /// given the other initializers available via the extensions. + /// init() {} + /// + /// Creates new instances belonging to the container, while referencing + /// the container itself and passing the reference on to each component that needs it. + /// public func prepare() { + // Core self.shell = RealShell(container: self) self.filesystem = RealFileSystem(container: self) self.command = RealCommand() + // Extra self.paths = Paths(container: self) self.phpEnvs = PhpEnvironments(container: self) - self.favorites = Favorites() self.warningManager = WarningManager(container: self) } - - public func overrideWith(config: TestableConfiguration) { - self.shell = TestableShell(expectations: config.shellOutput) - self.filesystem = TestableFileSystem(files: config.filesystem) - self.command = TestableCommand(commands: config.commandOutput) - } - - public static func real() -> Container { - let container = Container() - container.prepare() - return container - } }