diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index d34c539..181b062 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -11,12 +11,12 @@ on: jobs: test: if: github.event.pull_request.draft == false - runs-on: macos-14 + runs-on: macos-26 timeout-minutes: 10 steps: - uses: actions/checkout@v4 - name: Select Xcode - run: sudo xcode-select -s /Applications/Xcode_15.3.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_26.3.app/Contents/Developer - name: Run tests run: sudo swift test --parallel -Xswiftc -DCI -v diff --git a/Package.resolved b/Package.resolved index c36ddcf..5dd17a1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,12 +1,13 @@ { + "originHash" : "176905f781cea79f89254540d531b56a9cc2c902c62fb2822303ec6d51ebd53e", "pins" : [ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser", "state" : { - "revision" : "41982a3656a71c768319979febd796c6fd111d5c", - "version" : "1.5.0" + "revision" : "626b5b7b2f45e1b0b1c6f4a309296d1d21d7311b", + "version" : "1.7.1" } }, { @@ -19,5 +20,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index 6b36696..100a5a0 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.2 import PackageDescription diff --git a/README.md b/README.md index abda727..22139d2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A tool that finds Xcode color assets by their hex codes. The idea behind this to


- +

diff --git a/Sources/hexcode/Commands/Hexcode.swift b/Sources/hexcode/Commands/Hexcode.swift index 3a4fbc0..1b40bfd 100644 --- a/Sources/hexcode/Commands/Hexcode.swift +++ b/Sources/hexcode/Commands/Hexcode.swift @@ -10,7 +10,7 @@ struct Hexcode: AsyncParsableCommand { by their hexadecimal codes. """, usage: "hexcode [--directory ]", - version: "hexcode 0.2.1", + version: "hexcode 0.2.2", subcommands: [ FindColor.self, FindDuplicates.self, diff --git a/Sources/hexcode/Controllers/AssetCollector.swift b/Sources/hexcode/Controllers/AssetCollector.swift index 3da5a24..98bca04 100644 --- a/Sources/hexcode/Controllers/AssetCollector.swift +++ b/Sources/hexcode/Controllers/AssetCollector.swift @@ -2,8 +2,6 @@ import Foundation /// Finds color asset folders and converts them to Swift objects. protocol AssetCollecting { - /// `FileManager` instance used for the search. - var fileManager: FileManager { get set } /// Finds all color sets in the directory and its subdirectories. /// - parameter directory: Path to the directory to search for color sets. /// - throws: Errors when valid directory not found at given path or can't read content. @@ -11,8 +9,14 @@ protocol AssetCollecting { func collectAssets(in directory: String) async throws -> [NamedColorSet] } -final class AssetCollector: AssetCollecting { - var fileManager: FileManager = .default +final class AssetCollector: AssetCollecting, Sendable { + /// Judging from Apple's documentation, the use of the default shared `FileManager` from multiple threads is fine. + nonisolated(unsafe) + let fileManager: FileManager + + init(fileManager: FileManager = .default) { + self.fileManager = fileManager + } func collectAssets(in directory: String) async throws -> [NamedColorSet] { var isDirectory: ObjCBool = false @@ -55,7 +59,6 @@ extension AssetCollector { private func findColorSets( at paths: [String], in searchRootDirectory: String, - alreadyFoundColorSets: [NamedColorSet] = [] ) async -> [NamedColorSet] { let colorSets = await withTaskGroup(of: [NamedColorSet].self) { group in for path in paths { @@ -64,7 +67,7 @@ extension AssetCollector { switch contentAtPath { case .colorSet(let colorSet): - return self.makeNamedColorset( + return Self.makeNamedColorset( from: colorSet, at: path, in: searchRootDirectory @@ -76,7 +79,6 @@ extension AssetCollector { let colorSetsFromSubdirectory = await self.findColorSets( at: fullSubpaths, in: searchRootDirectory, - alreadyFoundColorSets: alreadyFoundColorSets ) return colorSetsFromSubdirectory @@ -86,13 +88,13 @@ extension AssetCollector { } } - return await group.reduce(alreadyFoundColorSets, +) + return await group.reduce([], +) } return colorSets } - private func makeNamedColorset( + private static func makeNamedColorset( from colorSet: ColorSet, at path: String, in searchRootDirectory: String @@ -119,7 +121,7 @@ extension AssetCollector { try fileManager.contentsOfDirectory(atPath: directory) } - private func getAssetName(from path: String, in searchRootDirectory: String) -> String { + private static func getAssetName(from path: String, in searchRootDirectory: String) -> String { let trimmedPath = String(path.trimmingPrefix(searchRootDirectory + "/")) return URL(filePath: trimmedPath) .deletingPathExtension() diff --git a/Sources/hexcode/Controllers/ColorFinder.swift b/Sources/hexcode/Controllers/ColorFinder.swift index b2c3c16..5845ace 100644 --- a/Sources/hexcode/Controllers/ColorFinder.swift +++ b/Sources/hexcode/Controllers/ColorFinder.swift @@ -45,7 +45,7 @@ final class ColorFinder: ColorFinding { for currentColor in currentColors { let rgbHex = currentColor.color.rgbHex - if rgbHex.isEmpty || duplicates.keys.contains(rgbHex) { + if rgbHex.isEmpty || duplicates[rgbHex] != nil { continue } @@ -81,9 +81,7 @@ final class ColorFinder: ColorFinding { ) colorNames.append(currentColorSetName) - if duplicates[rgbHex] == nil { - duplicates[rgbHex] = colorNames.sorted() - } + duplicates[rgbHex] = colorNames.sorted() } } diff --git a/Sources/hexcode/HexcodeApp.swift b/Sources/hexcode/HexcodeApp.swift index eaae35d..9d2e007 100644 --- a/Sources/hexcode/HexcodeApp.swift +++ b/Sources/hexcode/HexcodeApp.swift @@ -8,17 +8,14 @@ final class HexcodeApp { init( fileManager: FileManager = .default, - assetCollector: AssetCollecting = AssetCollector(), + assetCollector: AssetCollecting? = nil, colorFinder: ColorFinding = ColorFinder(), outputFunction: @escaping ((String) -> Void) = { print($0) } ) { self.fileManager = fileManager self.colorFinder = colorFinder self.output = outputFunction - - var assetCollector = assetCollector - assetCollector.fileManager = fileManager - self.assetCollector = assetCollector + self.assetCollector = assetCollector ?? AssetCollector(fileManager: fileManager) } /// Entry point for the default `find-color` subcommand logic. diff --git a/Sources/hexcode/Models/ColorAsset+Color.swift b/Sources/hexcode/Models/ColorAsset+Color.swift index e5831ef..0161fad 100644 --- a/Sources/hexcode/Models/ColorAsset+Color.swift +++ b/Sources/hexcode/Models/ColorAsset+Color.swift @@ -50,16 +50,11 @@ extension ColorAsset.Color { } private func isValidHexComponent(_ component: String) -> Bool { - matchRegex(component, pattern: "0x[0-9a-fA-F]{2}") - } - - private func matchRegex(_ string: String, pattern: String) -> Bool { - guard let regex = try? NSRegularExpression(pattern: pattern) else { return false } - let foundMatches = regex.matches( - in: string, + let foundMatches = Self.hexRegex.matches( + in: component, range: NSRange( location: 0, - length: string.count + length: component.count ) ) return foundMatches.count == 1 @@ -87,6 +82,9 @@ extension ColorAsset.Color { private static let floatConversionMultiplier: Float = 255.2 private static let hexFormatString = "%02X" + private static let hexRegex = try! NSRegularExpression( + pattern: "0x[0-9a-fA-F]{2}" + ) private static let formatter: NumberFormatter = { let formatter = NumberFormatter() formatter.decimalSeparator = "." diff --git a/Tests/hexcodeTests/AssetCollectorTests.swift b/Tests/hexcodeTests/AssetCollectorTests.swift index 50f5b66..18f711e 100644 --- a/Tests/hexcodeTests/AssetCollectorTests.swift +++ b/Tests/hexcodeTests/AssetCollectorTests.swift @@ -12,12 +12,24 @@ final class AssetCollectorTests: XCTestCase { override func setUp() { mocks = Mocks() - sut = SUT() - sut.fileManager = mocks.fileManager + sut = SUT(fileManager: mocks.fileManager) } // MARK: - Tests + func test_init_withDefaultParameters_setsFileManagerToDefault() async throws { + // Given, When + sut = SUT() + + // Then + XCTAssertTrue(sut.fileManager === FileManager.default) + } + + func test_init_withNonDefaultFileManagerPassed_setsTheGivenFileManager() async throws { + // Then + XCTAssertTrue(sut.fileManager === mocks.fileManager) + } + func test_collectAssets_inAssetCatalogDirectory_findsAssets() async throws { // Given let catalogPath = "Assets.xcassets" diff --git a/Tests/hexcodeTests/HexcodeAppTests.swift b/Tests/hexcodeTests/HexcodeAppTests.swift index 6eb53f7..4e45c40 100644 --- a/Tests/hexcodeTests/HexcodeAppTests.swift +++ b/Tests/hexcodeTests/HexcodeAppTests.swift @@ -17,24 +17,6 @@ final class HexcodeAppTests: XCTestCase { sut = makeSUT() } - // MARK: - Test init - - func test_init_withoutFileManager_passesDefaultFileManagerIntoAssetCollector() { - // When - _ = SUT(assetCollector: mocks.assetCollector) - - // Then - XCTAssertEqual(mocks.assetCollector.calls, [.setFileManager(.default)]) - } - - func test_init_passingFileManager_passesDefaultFileManagerIntoAssetCollector() { - // When - _ = SUT(fileManager: mocks.fileManager, assetCollector: mocks.assetCollector) - - // Then - XCTAssertEqual(mocks.assetCollector.calls, [.setFileManager(mocks.fileManager)]) - } - // MARK: - Test runFindColor func test_runFindColor_withoutDirectory_runsInCurrentDirectoryFromFileManager() async throws { diff --git a/Tests/hexcodeTests/Mocks/AssetCollectorMock.swift b/Tests/hexcodeTests/Mocks/AssetCollectorMock.swift index 2dc2941..4bd2c65 100644 --- a/Tests/hexcodeTests/Mocks/AssetCollectorMock.swift +++ b/Tests/hexcodeTests/Mocks/AssetCollectorMock.swift @@ -4,13 +4,10 @@ import Foundation final class AssetCollectorMock { enum Call: Equatable { case collectAssetsIn(directory: String) - case getFileManager - case setFileManager(FileManager) } struct CallResults { var collectAssets: Result<[NamedColorSet], Error> = .success([]) - var fileManager: FileManager = FileManagerMock() } private(set) var calls: [Call] = [] @@ -25,17 +22,6 @@ final class AssetCollectorMock { // MARK: - AssetCollecting extension AssetCollectorMock: AssetCollecting { - var fileManager: FileManager { - get { - calls.append(.getFileManager) - return results.fileManager - } - - set { - calls.append(.setFileManager(newValue)) - } - } - func collectAssets(in directory: String) throws -> [NamedColorSet] { calls.append(.collectAssetsIn(directory: directory)) return try results.collectAssets.get()