From 4ab3cc1e709d1c32a5d1136802e9ff1322b6a5c9 Mon Sep 17 00:00:00 2001 From: maxsoch Date: Mon, 6 Jul 2026 21:17:26 +0200 Subject: [PATCH] add unit test target with BrewParser extraction Extract brew output parsing into BrewParser.swift (pure Swift, no AppKit) and add BrewBarTests target with 7 passing tests covering parseOutdated and parseVersion edge cases. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 1 + BrewBar.xcodeproj/project.pbxproj | 118 ++++++++++++++++++++++++++++++ BrewBar/BrewBarApp.swift | 4 +- BrewBar/BrewParser.swift | 13 ++++ BrewBarTests/BrewBarTests.swift | 39 ++++++++++ 5 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 BrewBar/BrewParser.swift create mode 100644 BrewBarTests/BrewBarTests.swift diff --git a/.gitignore b/.gitignore index 5c60401..2dd0d1a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.xcuserdatad/ CLAUDE.local.md .claude/settings.local.json +build/ diff --git a/BrewBar.xcodeproj/project.pbxproj b/BrewBar.xcodeproj/project.pbxproj index bb3ee40..75590a2 100644 --- a/BrewBar.xcodeproj/project.pbxproj +++ b/BrewBar.xcodeproj/project.pbxproj @@ -6,8 +6,19 @@ objectVersion = 77; objects = { +/* Begin PBXContainerItemProxy section */ + 87F2AA852FEE797F0014F9D6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 87F2AA5A2FEE797F0014F9D6 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 87F2AA612FEE797F0014F9D6; + remoteInfo = BrewBar; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ 87F2AA622FEE797F0014F9D6 /* BrewBar.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BrewBar.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 87F2AA802FEE797F0014F9D6 /* BrewBarTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BrewBarTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -29,6 +40,11 @@ path = BrewBar; sourceTree = ""; }; + 87F2AA812FEE797F0014F9D6 /* BrewBarTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = BrewBarTests; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -39,6 +55,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 87F2AA822FEE797F0014F9D6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -46,6 +69,7 @@ isa = PBXGroup; children = ( 87F2AA642FEE797F0014F9D6 /* BrewBar */, + 87F2AA812FEE797F0014F9D6 /* BrewBarTests */, 87F2AA632FEE797F0014F9D6 /* Products */, ); sourceTree = ""; @@ -54,6 +78,7 @@ isa = PBXGroup; children = ( 87F2AA622FEE797F0014F9D6 /* BrewBar.app */, + 87F2AA802FEE797F0014F9D6 /* BrewBarTests.xctest */, ); name = Products; sourceTree = ""; @@ -83,6 +108,28 @@ productReference = 87F2AA622FEE797F0014F9D6 /* BrewBar.app */; productType = "com.apple.product-type.application"; }; + 87F2AA842FEE797F0014F9D6 /* BrewBarTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 87F2AA892FEE797F0014F9D6 /* Build configuration list for PBXNativeTarget "BrewBarTests" */; + buildPhases = ( + 87F2AA832FEE797F0014F9D6 /* Sources */, + 87F2AA822FEE797F0014F9D6 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 87F2AA862FEE797F0014F9D6 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 87F2AA812FEE797F0014F9D6 /* BrewBarTests */, + ); + name = BrewBarTests; + packageProductDependencies = ( + ); + productName = BrewBarTests; + productReference = 87F2AA802FEE797F0014F9D6 /* BrewBarTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -96,6 +143,10 @@ 87F2AA612FEE797F0014F9D6 = { CreatedOnToolsVersion = 26.3; }; + 87F2AA842FEE797F0014F9D6 = { + CreatedOnToolsVersion = 26.3; + TestTargetID = 87F2AA612FEE797F0014F9D6; + }; }; }; buildConfigurationList = 87F2AA5D2FEE797F0014F9D6 /* Build configuration list for PBXProject "BrewBar" */; @@ -113,6 +164,7 @@ projectRoot = ""; targets = ( 87F2AA612FEE797F0014F9D6 /* BrewBar */, + 87F2AA842FEE797F0014F9D6 /* BrewBarTests */, ); }; /* End PBXProject section */ @@ -135,8 +187,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 87F2AA832FEE797F0014F9D6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 87F2AA862FEE797F0014F9D6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 87F2AA612FEE797F0014F9D6 /* BrewBar */; + targetProxy = 87F2AA852FEE797F0014F9D6 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 87F2AA6B2FEE79800014F9D6 /* Debug */ = { isa = XCBuildConfiguration; @@ -352,6 +419,48 @@ }; name = Release; }; + 87F2AA872FEE797F0014F9D6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = GLVUYYHFD9; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 15.6; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.MaxSoch.BrewBarTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BrewBar.app/Contents/MacOS/BrewBar"; + }; + name = Debug; + }; + 87F2AA882FEE797F0014F9D6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = GLVUYYHFD9; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 15.6; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.MaxSoch.BrewBarTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BrewBar.app/Contents/MacOS/BrewBar"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -373,6 +482,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 87F2AA892FEE797F0014F9D6 /* Build configuration list for PBXNativeTarget "BrewBarTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 87F2AA872FEE797F0014F9D6 /* Debug */, + 87F2AA882FEE797F0014F9D6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 87F2AA5A2FEE797F0014F9D6 /* Project object */; diff --git a/BrewBar/BrewBarApp.swift b/BrewBar/BrewBarApp.swift index c90403b..bbc2211 100644 --- a/BrewBar/BrewBarApp.swift +++ b/BrewBar/BrewBarApp.swift @@ -244,7 +244,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { setMenuBarIcon("brewbar-updating") runBrew("outdated") { output in - let lines = output.split(separator: "\n").map { String($0) } + let lines = BrewParser.parseOutdated(output) self.cachedOutdated = lines self.lastOutdatedFetch = Date() @@ -304,7 +304,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { /// ✅ Brew version (correct) func refreshBrewVersion() { runBrew("--version") { output in - let firstLine = output.split(separator: "\n").first.map(String.init) ?? "Unknown" + let firstLine = BrewParser.parseVersion(output) DispatchQueue.main.async { self.versionItem.title = "🏷 \(firstLine)" diff --git a/BrewBar/BrewParser.swift b/BrewBar/BrewParser.swift new file mode 100644 index 0000000..ef97bd0 --- /dev/null +++ b/BrewBar/BrewParser.swift @@ -0,0 +1,13 @@ +import Foundation + +enum BrewParser { + static func parseOutdated(_ output: String) -> [String] { + output.split(separator: "\n") + .map { String($0) } + .filter { !$0.isEmpty } + } + + static func parseVersion(_ output: String) -> String { + output.split(separator: "\n").first.map(String.init) ?? "Unknown" + } +} diff --git a/BrewBarTests/BrewBarTests.swift b/BrewBarTests/BrewBarTests.swift new file mode 100644 index 0000000..d3cbdfb --- /dev/null +++ b/BrewBarTests/BrewBarTests.swift @@ -0,0 +1,39 @@ +@testable import BrewBar +import XCTest + +final class BrewParserTests: XCTestCase { + // MARK: - parseOutdated + + func testParseOutdated_multiplePackages() { + let output = "wget\nffmpeg\ngit\n" + XCTAssertEqual(BrewParser.parseOutdated(output), ["wget", "ffmpeg", "git"]) + } + + func testParseOutdated_emptyOutput() { + XCTAssertEqual(BrewParser.parseOutdated(""), []) + } + + func testParseOutdated_singlePackage() { + XCTAssertEqual(BrewParser.parseOutdated("wget\n"), ["wget"]) + } + + func testParseOutdated_trailingNewlinesIgnored() { + let output = "wget\n\n\n" + XCTAssertEqual(BrewParser.parseOutdated(output), ["wget"]) + } + + // MARK: - parseVersion + + func testParseVersion_picksFirstLine() { + let output = "Homebrew 4.3.0\nHomebrew/homebrew-core (git revision abc123)\n" + XCTAssertEqual(BrewParser.parseVersion(output), "Homebrew 4.3.0") + } + + func testParseVersion_emptyOutput() { + XCTAssertEqual(BrewParser.parseVersion(""), "Unknown") + } + + func testParseVersion_singleLine() { + XCTAssertEqual(BrewParser.parseVersion("Homebrew 4.3.0"), "Homebrew 4.3.0") + } +}