Compare commits

..

3 commits

Author SHA1 Message Date
maxsoch 4ab3cc1e70 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 <noreply@anthropic.com>
2026-07-06 21:17:26 +02:00
maxsoch 6f255380d0 add dev tooling from /init: swiftlint, skills, gitignore; apply swiftformat
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-06 21:07:17 +02:00
Maxence Socheleau 5def46f2cd n/a 2026-07-03 15:41:00 +02:00
9 changed files with 341 additions and 57 deletions

View file

@ -0,0 +1,25 @@
---
name: build
description: Build BrewBar with xcodebuild in Debug configuration and report any compiler errors or warnings.
disable-model-invocation: false
---
Build the BrewBar project using xcodebuild with the Debug configuration.
Run the following command from the project root:
```
xcodebuild \
-project BrewBar.xcodeproj \
-scheme BrewBar \
-configuration Debug \
-destination "platform=macOS" \
build \
2>&1
```
After running:
1. If the build succeeds, report "Build succeeded" and note any warnings.
2. If it fails, show the relevant error lines (not the full raw output), explain what each error means, and suggest a fix if the cause is clear.
This is a zero-dependency project (no SPM, CocoaPods, or Carthage), so no package resolution step is needed.

View file

@ -0,0 +1,34 @@
---
name: run
description: Build and launch BrewBar.app for manual testing. Use after making changes to verify the app runs and the menu bar icon appears correctly.
disable-model-invocation: false
---
Build and launch BrewBar for manual testing.
Steps:
1. Build the app in Debug configuration:
```
xcodebuild \
-project BrewBar.xcodeproj \
-scheme BrewBar \
-configuration Debug \
-destination "platform=macOS" \
build \
SYMROOT=build \
2>&1
```
If the build fails, stop here and report the errors with explanations.
2. If the build succeeds, open the built app:
```
open build/Debug/BrewBar.app
```
3. Tell the user:
- The app is a menu bar app (.accessory policy) — it will NOT appear in the Dock, only in the menu bar at the top right of the screen
- Look for the BrewBar icon in the menu bar to confirm it launched
- Clicking the icon opens the menu; the app runs Homebrew commands in the background
- To quit, use the Quit option in the app's menu
Note: If a previous instance is already running, macOS may bring it forward instead of launching a new one. Quit the existing instance first if you need a fresh launch.

3
.gitignore vendored
View file

@ -1 +1,4 @@
*.xcuserdatad/
CLAUDE.local.md
.claude/settings.local.json
build/

35
.swiftlint.yml Normal file
View file

@ -0,0 +1,35 @@
disabled_rules:
- trailing_whitespace # swiftformat handles this
- todo # todos in comments are fine
opt_in_rules:
- force_unwrapping
- empty_count
- closure_spacing
- contains_over_first_not_nil
included:
- BrewBar
excluded:
- BrewBar.xcodeproj
line_length:
warning: 140
error: 200
file_length:
warning: 500
error: 800
function_body_length:
warning: 80
error: 120
type_body_length:
warning: 400
error: 600
cyclomatic_complexity:
warning: 15
error: 25

View file

@ -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 = "<group>";
};
87F2AA812FEE797F0014F9D6 /* BrewBarTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = BrewBarTests;
sourceTree = "<group>";
};
/* 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 = "<group>";
@ -54,6 +78,7 @@
isa = PBXGroup;
children = (
87F2AA622FEE797F0014F9D6 /* BrewBar.app */,
87F2AA802FEE797F0014F9D6 /* BrewBarTests.xctest */,
);
name = Products;
sourceTree = "<group>";
@ -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;
@ -261,11 +328,20 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GLVUYYHFD9;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BrewBar/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = BrewBar;
@ -280,6 +356,12 @@
PRODUCT_BUNDLE_IDENTIFIER = com.MaxSoch.BrewBar;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
@ -294,11 +376,20 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GLVUYYHFD9;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BrewBar/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = BrewBar;
@ -313,6 +404,12 @@
PRODUCT_BUNDLE_IDENTIFIER = com.MaxSoch.BrewBar;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
@ -322,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 */
@ -343,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 */;

View file

@ -2,7 +2,6 @@ import Cocoa
@main
class AppDelegate: NSObject, NSApplicationDelegate {
static func main() {
let app = NSApplication.shared
let delegate = AppDelegate()
@ -24,7 +23,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
var brewPath: String?
var refreshTimer: Timer?
//var refreshInterval: TimeInterval = 3600
/// var refreshInterval: TimeInterval = 3600
var refreshInterval: TimeInterval {
get {
let saved = UserDefaults.standard.double(forKey: "refreshInterval")
@ -42,8 +41,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// MARK: - Menu
func applicationDidFinishLaunching(_ notification: Notification) {
func applicationDidFinishLaunching(_: Notification) {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
setMenuBarIcon("brewbar-uptodate")
@ -57,16 +55,14 @@ class AppDelegate: NSObject, NSApplicationDelegate {
menu.addItem(NSMenuItem(title: "🔄 Refresh", action: #selector(refreshAction), keyEquivalent: "r"))
menu.addItem(NSMenuItem(title: "📦 Upgrade All", action: #selector(upgradeAll), keyEquivalent: "u"))
let outdatedItem = NSMenuItem(title: "📋 Outdated", action: nil, keyEquivalent: "")
outdatedSubmenu = NSMenu()
outdatedSubmenu.addItem(NSMenuItem(title: "Refresh List", action: #selector(forceRefreshOutdated), keyEquivalent: ""))
outdatedItem.submenu = outdatedSubmenu
menu.addItem(outdatedItem)
outdatedSubmenu = NSMenu()
outdatedSubmenu.addItem(NSMenuItem(title: "Refresh List", action: #selector(forceRefreshOutdated), keyEquivalent: ""))
outdatedItem.submenu = outdatedSubmenu
menu.addItem(outdatedItem)
menu.addItem(NSMenuItem(title: "📜 Logs", action: #selector(showLogs), keyEquivalent: "l"))
menu.addItem(NSMenuItem.separator())
let intervalItem = NSMenuItem(title: "⏱ Refresh Interval", action: nil, keyEquivalent: "")
let intervalSubmenu = NSMenu()
@ -81,30 +77,27 @@ class AppDelegate: NSObject, NSApplicationDelegate {
intervalSubmenu.addItem(item)
}
let customMenuItem = NSMenuItem()
let customView = NSView(frame: NSRect(x: 0, y: 0, width: 200, height: 30))
let customMenuItem = NSMenuItem()
let customView = NSView(frame: NSRect(x: 0, y: 0, width: 200, height: 30))
let textField = NSTextField(frame: NSRect(x: 10, y: 4, width: 120, height: 22))
textField.placeholderString = "Secondes..."
textField.stringValue = "\(Int(refreshInterval))"
let textField = NSTextField(frame: NSRect(x: 10, y: 4, width: 120, height: 22))
textField.placeholderString = "Secondes..."
textField.stringValue = "\(Int(refreshInterval))"
let confirmButton = NSButton(frame: NSRect(x: 138, y: 4, width: 52, height: 22))
confirmButton.title = "✓ Set"
confirmButton.bezelStyle = .rounded
confirmButton.target = self
confirmButton.action = #selector(applyCustomInterval(_:))
let confirmButton = NSButton(frame: NSRect(x: 138, y: 4, width: 52, height: 22))
confirmButton.title = "✓ Set"
confirmButton.bezelStyle = .rounded
confirmButton.target = self
confirmButton.action = #selector(applyCustomInterval(_:))
customView.addSubview(textField)
customView.addSubview(confirmButton)
customMenuItem.view = customView
intervalSubmenu.addItem(customMenuItem)
customView.addSubview(textField)
customView.addSubview(confirmButton)
customMenuItem.view = customView
intervalSubmenu.addItem(customMenuItem)
intervalItem.submenu = intervalSubmenu
menu.addItem(intervalItem)
versionItem = NSMenuItem(title: "🏷 Brew: ...", action: nil, keyEquivalent: "")
menu.addItem(versionItem)
@ -145,7 +138,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@objc func setRefreshInterval(_ sender: NSMenuItem) {
guard let interval = sender.representedObject as? TimeInterval else { return }
refreshInterval = interval // le set sauvegarde dans UserDefaults
refreshInterval = interval // le set sauvegarde dans UserDefaults
restartTimer()
}
@ -166,7 +159,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let paths = [
"/opt/homebrew/bin/brew",
"/usr/local/bin/brew"
"/usr/local/bin/brew",
]
for path in paths {
@ -176,15 +169,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
}
//fallback
// fallback
brewPath = "brew"
return "brew"
}
func runBrew(_ command: String, completion: @escaping (String) -> Void = { _ in }) {
DispatchQueue.global().async {
let brew = self.resolveBrewPath()
let cleaned = command.replacingOccurrences(of: "brew ", with: "")
let fullCommand = "\(brew) \(cleaned)"
@ -233,9 +224,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
}
// SMART refresh (uses cache)
/// SMART refresh (uses cache)
@objc func refreshOutdatedList() {
// if fetched in last 5s uses cache
if let last = lastOutdatedFetch, Date().timeIntervalSince(last) < 5 {
updateOutdatedMenu(with: cachedOutdated)
@ -245,7 +235,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
fetchOutdated()
}
// FORCE refresh (menu button)
/// FORCE refresh (menu button)
@objc func forceRefreshOutdated() {
fetchOutdated()
}
@ -254,8 +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()
@ -273,7 +262,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
func updateOutdatedMenu(with lines: [String]) {
outdatedSubmenu.removeAllItems()
outdatedSubmenu.addItem(NSMenuItem(title: "Refresh List", action: #selector(forceRefreshOutdated), keyEquivalent: ""))
@ -283,7 +271,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
for formula in lines {
let item = NSMenuItem(title: formula, action: #selector(self.upgradeSingle(_:)), keyEquivalent: "")
let item = NSMenuItem(title: formula, action: #selector(upgradeSingle(_:)), keyEquivalent: "")
item.representedObject = formula
outdatedSubmenu.addItem(item)
}
@ -313,10 +301,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
}
// Brew version (correct)
/// 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)"

13
BrewBar/BrewParser.swift Normal file
View file

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

View file

@ -1,11 +1,9 @@
import Cocoa
class LogWindowController: NSWindowController {
var textView: NSTextView!
convenience init() {
let contentRect = NSRect(x: 0, y: 0, width: 600, height: 400)
let styleMask: NSWindow.StyleMask = [.titled, .closable, .resizable]
@ -31,6 +29,7 @@ class LogWindowController: NSWindowController {
func update(text: String) {
textView.string = text
textView.scrollToEndOfDocument(nil)
}
}

View file

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