diff --git a/Executable/Sources/Colibri.swift b/Executable/Sources/Colibri.swift index 4359f28..288e88b 100644 --- a/Executable/Sources/Colibri.swift +++ b/Executable/Sources/Colibri.swift @@ -9,7 +9,8 @@ struct Colibri: AsyncParsableCommand { abstract: "The utility to manage your Hummingbird apps", subcommands: [ Build.self, - Create.self + Create.self, + Outdated.self ], defaultSubcommand: Create.self ) diff --git a/Executable/Sources/Commands/OutdatedCommand.swift b/Executable/Sources/Commands/OutdatedCommand.swift new file mode 100644 index 0000000..686da2b --- /dev/null +++ b/Executable/Sources/Commands/OutdatedCommand.swift @@ -0,0 +1,29 @@ +import ArgumentParser +import ColibriLibrary + +extension Colibri { + struct Outdated: AsyncParsableCommand { + + // MARK: Properties + + static let configuration = CommandConfiguration( + commandName: "outdated-dependencies", + abstract: "Check for outdated package dependencies in a Hummingbird app", + helpNames: .shortAndLong, + aliases: ["outdated"] + ) + + @OptionGroup var options: Options + + // MARK: Functions + + mutating func run() async throws { + let terminalService = TerminalService() + + let outdatedDependencies = OutdatedDependenciesTask(terminalService: terminalService) + + try await outdatedDependencies(at: options.locationURL) + } + + } +} diff --git a/Executable/Sources/Options/OutdatedOptions.swift b/Executable/Sources/Options/OutdatedOptions.swift new file mode 100644 index 0000000..ab501b9 --- /dev/null +++ b/Executable/Sources/Options/OutdatedOptions.swift @@ -0,0 +1,13 @@ +import ArgumentParser +import ColibriLibrary + +extension Colibri.Outdated { + struct Options: ParsableArguments, Locationable { + + // MARK: Properties + + @Option(name: .shortAndLong) + var location: String? + + } +} diff --git a/Library/Sources/Internal/Extensions/URL+Extensions.swift b/Library/Sources/Internal/Extensions/URL+Extensions.swift index 01d4ea3..4ab418d 100644 --- a/Library/Sources/Internal/Extensions/URL+Extensions.swift +++ b/Library/Sources/Internal/Extensions/URL+Extensions.swift @@ -16,7 +16,7 @@ extension URL { var pathString: String { if #available(macOS 13.0, *) { - path(percentEncoded: true) + path(percentEncoded: false) } else { path } diff --git a/Library/Sources/Public/Tasks/OutdatedDependenciesTask.swift b/Library/Sources/Public/Tasks/OutdatedDependenciesTask.swift new file mode 100644 index 0000000..38a33de --- /dev/null +++ b/Library/Sources/Public/Tasks/OutdatedDependenciesTask.swift @@ -0,0 +1,31 @@ +import Foundation + +public struct OutdatedDependenciesTask { + + // MARK: Properties + + private let terminalService: TerminalServicing + + // MARK: Initialisers + + public init(terminalService: TerminalServicing) { + self.terminalService = terminalService + } + + // MARK: Functions + + public func callAsFunction(at location: URL? = nil) async throws (TerminalServiceError) { + let executableURL = URL(at: "/usr/bin/swift") + + var arguments: [String] = ["package", "update"] + + if let location { + arguments.append(contentsOf: ["--package-path", location.pathString]) + } + + arguments.append("--dry-run") + + try await terminalService.run(executableURL, arguments: arguments) + } + +} diff --git a/Test/Sources/Cases/Internal/Extensions/URL+ExtensionsTests.swift b/Test/Sources/Cases/Internal/Extensions/URL+ExtensionsTests.swift index b5525ea..ac809d5 100644 --- a/Test/Sources/Cases/Internal/Extensions/URL+ExtensionsTests.swift +++ b/Test/Sources/Cases/Internal/Extensions/URL+ExtensionsTests.swift @@ -24,8 +24,8 @@ struct URL_ExtensionsTests { // MARK: Computed tests - @Test(arguments: zip([URL.someFile, .dotFile, .tildeFile, .someURL], - [String.someFilePath, .dotPath, .tildePath, .empty])) + @Test(arguments: zip([URL.someFile, .dotFile, .tildeFile, .someEncodedFile, .someURL], + [String.someFilePath, .dotPath, .tildePath, .someEncodedPath, .empty])) func pathString( with url: URL, expects path: String @@ -63,6 +63,7 @@ private extension String { static let dotPath = "." static let empty = "" static let tildePath = "~" + static let someEncodedPath = "/sömê/páth/fîlê" static let someFilePath = "/some/file/path" } @@ -70,6 +71,7 @@ private extension String { private extension URL { static let dotFile = URL(at: .dotPath) + static let someEncodedFile = URL(at: "/sömê/páth/fîlê") static let someFile = URL(at: .someFilePath) static let someURL = URL(string: "https://some.url.path")! static let tildeFile = URL(at: .tildePath) diff --git a/Test/Sources/Cases/Public/Tasks/OutdatedDependenciesTaskTests.swift b/Test/Sources/Cases/Public/Tasks/OutdatedDependenciesTaskTests.swift new file mode 100644 index 0000000..b79a211 --- /dev/null +++ b/Test/Sources/Cases/Public/Tasks/OutdatedDependenciesTaskTests.swift @@ -0,0 +1,42 @@ +import Foundation +import Testing + +@testable import ColibriLibrary + +struct OutdatedDependenciesTaskTests { + + @Test(arguments: [nil, URL.someCurrentFolder]) + func task(at location: URL?) async throws { + // GIVEN + let terminalService = TerminalServiceSpy() + let task = OutdatedDependenciesTask(terminalService: terminalService) + + // WHEN + try await task(at: location) + + // THEN + let executableURL = URL(at: "/usr/bin/swift") + let arguments = if let location { + ["package", "update", "--package-path", location.pathString, "--dry-run"] + } else { + ["package", "update", "--dry-run"] + } + + #expect(terminalService.actions.count == 1) + #expect(terminalService.actions[0] == .ran(executableURL, arguments)) + } + + @Test(arguments: [nil, URL.someCurrentFolder], [TerminalServiceError.unexpected, .output(""), .captured("")]) + func task(at location: URL?, throws error: TerminalServiceError) async throws { + // GIVEN + let terminalService = TerminalServiceMock(action: .error(error)) + let task = BuildProjectTask(terminalService: terminalService) + + // WHEN + // THEN + await #expect(throws: error) { + try await task(at: location) + } + } + +}