From 1eb9b88ac75d41630ca8d4471f5ca70836461127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Pi=C3=B1era=20Buend=C3=ADa?= Date: Tue, 11 Sep 2018 18:18:09 +0200 Subject: [PATCH] Add Focus command (#129) * Add Focus command * Fix a bug on installer * Add entry to the CHANGELOG --- CHANGELOG.md | 1 + Sources/TuistCore/Utils/Opener.swift | 55 ++++++++++++ .../TuistCoreTesting/Utils/MockOpener.swift | 15 ++++ Sources/TuistEnvKit/Installer/Installer.swift | 4 +- .../TuistKit/Commands/CommandRegistry.swift | 1 + Sources/TuistKit/Commands/FocusCommand.swift | 84 +++++++++++++++++++ .../Generator/WorkspaceGenerator.swift | 8 +- Tests/TuistCoreTests/Utils/OpenerTests.swift | 51 +++++++++++ .../Commands/FocusCommandTests.swift | 73 ++++++++++++++++ .../Mocks/MockWorkspaceGenerator.swift | 6 +- 10 files changed, 292 insertions(+), 6 deletions(-) create mode 100644 Sources/TuistCore/Utils/Opener.swift create mode 100644 Sources/TuistCoreTesting/Utils/MockOpener.swift create mode 100644 Sources/TuistKit/Commands/FocusCommand.swift create mode 100644 Tests/TuistCoreTests/Utils/OpenerTests.swift create mode 100644 Tests/TuistKitTests/Commands/FocusCommandTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index e4030d177..601077574 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/ - Support for JSON and Yaml manifests https://github.com/tuist/tuist/pull/110 by @pepibumur. - Generate `.gitignore` file when running init command https://github.com/tuist/tuist/pull/118 by @pepibumur. - Git ignore Xcode and macOS files that shouldn't be included on a git repository https://github.com/tuist/tuist/pull/124 by @pepibumur. +- Focus command https://github.com/tuist/tuist/pull/129 by @pepibumur. ### Fixed diff --git a/Sources/TuistCore/Utils/Opener.swift b/Sources/TuistCore/Utils/Opener.swift new file mode 100644 index 000000000..de8d044f3 --- /dev/null +++ b/Sources/TuistCore/Utils/Opener.swift @@ -0,0 +1,55 @@ +import Basic +import Foundation + +enum OpeningError: FatalError, Equatable { + case notFound(AbsolutePath) + + var type: ErrorType { + switch self { + case .notFound: + return .bug + } + } + + var description: String { + switch self { + case let .notFound(path): + return "Couldn't open file at path \(path.asString)" + } + } + + static func == (lhs: OpeningError, rhs: OpeningError) -> Bool { + switch (lhs, rhs) { + case let (.notFound(lhsPath), .notFound(rhsPath)): + return lhsPath == rhsPath + } + } +} + +public protocol Opening: AnyObject { + func open(path: AbsolutePath) throws +} + +public class Opener: Opening { + // MARK: - Attributes + + private let system: Systeming + private let fileHandler: FileHandling + + // MARK: - Init + + public init(system: Systeming = System(), + fileHandler: FileHandling = FileHandler()) { + self.system = system + self.fileHandler = fileHandler + } + + // MARK: - Opening + + public func open(path: AbsolutePath) throws { + if !fileHandler.exists(path) { + throw OpeningError.notFound(path) + } + try system.popen("open", path.asString, verbose: true) + } +} diff --git a/Sources/TuistCoreTesting/Utils/MockOpener.swift b/Sources/TuistCoreTesting/Utils/MockOpener.swift new file mode 100644 index 000000000..0694321ed --- /dev/null +++ b/Sources/TuistCoreTesting/Utils/MockOpener.swift @@ -0,0 +1,15 @@ +import Basic +import Foundation +import TuistCore + +public final class MockOpener: Opening { + var openStub: Error? + var openArgs: [AbsolutePath] = [] + var openCallCount: UInt = 0 + + public func open(path: AbsolutePath) throws { + openCallCount += 1 + openArgs.append(path) + if let openStub = openStub { throw openStub } + } +} diff --git a/Sources/TuistEnvKit/Installer/Installer.swift b/Sources/TuistEnvKit/Installer/Installer.swift index 657ce0a04..546021f64 100644 --- a/Sources/TuistEnvKit/Installer/Installer.swift +++ b/Sources/TuistEnvKit/Installer/Installer.swift @@ -145,7 +145,9 @@ final class Installer: Installing { verbose: false).throwIfError() // Copying files - try system.capture("/bin/mkdir", installationDirectory.asString, verbose: false).throwIfError() + if !fileHandler.exists(installationDirectory) { + try system.capture("/bin/mkdir", installationDirectory.asString, verbose: false).throwIfError() + } try buildCopier.copy(from: buildDirectory, to: installationDirectory) diff --git a/Sources/TuistKit/Commands/CommandRegistry.swift b/Sources/TuistKit/Commands/CommandRegistry.swift index fbe4f7a70..f59e82863 100644 --- a/Sources/TuistKit/Commands/CommandRegistry.swift +++ b/Sources/TuistKit/Commands/CommandRegistry.swift @@ -24,6 +24,7 @@ public final class CommandRegistry { register(command: DumpCommand.self) register(command: VersionCommand.self) register(command: CreateIssueCommand.self) + register(command: FocusCommand.self) register(hiddenCommand: EmbedCommand.self) } diff --git a/Sources/TuistKit/Commands/FocusCommand.swift b/Sources/TuistKit/Commands/FocusCommand.swift new file mode 100644 index 000000000..e64d003de --- /dev/null +++ b/Sources/TuistKit/Commands/FocusCommand.swift @@ -0,0 +1,84 @@ +import Basic +import Foundation +import TuistCore +import Utility + +class FocusCommand: NSObject, Command { + // MARK: - Static + + static let command = "focus" + static let overview = "Opens Xcode ready to focus on the project in the current directory." + + // MARK: - Attributes + + fileprivate let graphLoader: GraphLoading + fileprivate let workspaceGenerator: WorkspaceGenerating + fileprivate let printer: Printing + fileprivate let system: Systeming + fileprivate let resourceLocator: ResourceLocating + fileprivate let fileHandler: FileHandling + fileprivate let opener: Opening + + let configArgument: OptionArgument + + // MARK: - Init + + required convenience init(parser: ArgumentParser) { + self.init(graphLoader: GraphLoader(), + workspaceGenerator: WorkspaceGenerator(), + parser: parser) + } + + init(graphLoader: GraphLoading, + workspaceGenerator: WorkspaceGenerating, + parser: ArgumentParser, + printer: Printing = Printer(), + system: Systeming = System(), + resourceLocator: ResourceLocating = ResourceLocator(), + fileHandler: FileHandling = FileHandler(), + opener: Opening = Opener()) { + let subParser = parser.add(subparser: FocusCommand.command, overview: FocusCommand.overview) + self.graphLoader = graphLoader + self.workspaceGenerator = workspaceGenerator + self.printer = printer + self.system = system + self.resourceLocator = resourceLocator + self.fileHandler = fileHandler + self.opener = opener + configArgument = subParser.add(option: "--config", + shortName: "-c", + kind: String.self, + usage: "The configuration that will be generated.", + completion: .filename) + } + + func run(with arguments: ArgumentParser.Result) throws { + let path = fileHandler.currentPath + let config = try parseConfig(arguments: arguments) + let graph = try graphLoader.load(path: path) + + let workspacePath = try workspaceGenerator.generate(path: path, + graph: graph, + options: GenerationOptions(buildConfiguration: config), + system: system, + printer: printer, + resourceLocator: resourceLocator) + + try opener.open(path: workspacePath) + } + + // MARK: - Fileprivate + + private func parseConfig(arguments: ArgumentParser.Result) throws -> BuildConfiguration { + var config: BuildConfiguration = .debug + if let configString = arguments.get(configArgument) { + guard let buildConfiguration = BuildConfiguration(rawValue: configString.lowercased()) else { + let error = ArgumentParserError.invalidValue(argument: "config", + error: ArgumentConversionError.custom("config can only be debug or release")) + throw error + } + config = buildConfiguration + } + return config + } +} diff --git a/Sources/TuistKit/Generator/WorkspaceGenerator.swift b/Sources/TuistKit/Generator/WorkspaceGenerator.swift index 09a9fcfa6..a06146de2 100644 --- a/Sources/TuistKit/Generator/WorkspaceGenerator.swift +++ b/Sources/TuistKit/Generator/WorkspaceGenerator.swift @@ -4,12 +4,13 @@ import TuistCore import xcodeproj protocol WorkspaceGenerating: AnyObject { + @discardableResult func generate(path: AbsolutePath, graph: Graphing, options: GenerationOptions, system: Systeming, printer: Printing, - resourceLocator: ResourceLocating) throws + resourceLocator: ResourceLocating) throws -> AbsolutePath } final class WorkspaceGenerator: WorkspaceGenerating { @@ -25,12 +26,13 @@ final class WorkspaceGenerator: WorkspaceGenerating { // MARK: - WorkspaceGenerating + @discardableResult func generate(path: AbsolutePath, graph: Graphing, options: GenerationOptions, system: Systeming = System(), printer: Printing = Printer(), - resourceLocator: ResourceLocating = ResourceLocator()) throws { + resourceLocator: ResourceLocating = ResourceLocator()) throws -> AbsolutePath { let workspaceName = "\(graph.name).xcworkspace" printer.print(section: "Generating workspace \(workspaceName)") let workspacePath = path.appending(component: workspaceName) @@ -51,5 +53,7 @@ final class WorkspaceGenerator: WorkspaceGenerating { workspace.data.children.append(XCWorkspaceDataElement.file(fileRef)) } try workspace.write(path: workspacePath, override: true) + + return workspacePath } } diff --git a/Tests/TuistCoreTests/Utils/OpenerTests.swift b/Tests/TuistCoreTests/Utils/OpenerTests.swift new file mode 100644 index 000000000..7f94a8643 --- /dev/null +++ b/Tests/TuistCoreTests/Utils/OpenerTests.swift @@ -0,0 +1,51 @@ +import Basic +import Foundation +import XCTest + +@testable import TuistCore +@testable import TuistCoreTesting + +final class OpeningErrorTests: XCTestCase { + func test_type() { + let path = AbsolutePath("/test") + XCTAssertEqual(OpeningError.notFound(path).type, .bug) + } + + func test_description() { + let path = AbsolutePath("/test") + XCTAssertEqual(OpeningError.notFound(path).description, "Couldn't open file at path /test") + } +} + +final class OpenerTests: XCTestCase { + var system: MockSystem! + var fileHandler: MockFileHandler! + var subject: Opener! + + override func setUp() { + super.setUp() + system = MockSystem() + fileHandler = try! MockFileHandler() + subject = Opener(system: system, + fileHandler: fileHandler) + } + + func test_open_when_path_doesnt_exist() throws { + let path = fileHandler.currentPath.appending(component: "tool") + + XCTAssertThrowsError(try subject.open(path: path)) { + XCTAssertEqual($0 as? OpeningError, OpeningError.notFound(path)) + } + } + + func test_open() throws { + let path = fileHandler.currentPath.appending(component: "tool") + try fileHandler.touch(path) + + system.stub(args: ["open", path.asString], + stderror: nil, + stdout: nil, + exitstatus: 0) + try subject.open(path: path) + } +} diff --git a/Tests/TuistKitTests/Commands/FocusCommandTests.swift b/Tests/TuistKitTests/Commands/FocusCommandTests.swift new file mode 100644 index 000000000..8c5e24bd0 --- /dev/null +++ b/Tests/TuistKitTests/Commands/FocusCommandTests.swift @@ -0,0 +1,73 @@ +import Basic +import Foundation +@testable import TuistCoreTesting +@testable import TuistKit +import Utility +@testable import xcodeproj +import XCTest + +final class FocusCommandTests: XCTestCase { + var subject: FocusCommand! + var errorHandler: MockErrorHandler! + var graphLoader: MockGraphLoader! + var workspaceGenerator: MockWorkspaceGenerator! + var parser: ArgumentParser! + var printer: MockPrinter! + var fileHandler: MockFileHandler! + var opener: MockOpener! + + override func setUp() { + super.setUp() + printer = MockPrinter() + errorHandler = MockErrorHandler() + graphLoader = MockGraphLoader() + workspaceGenerator = MockWorkspaceGenerator() + parser = ArgumentParser.test() + fileHandler = try! MockFileHandler() + opener = MockOpener() + subject = FocusCommand(graphLoader: graphLoader, + workspaceGenerator: workspaceGenerator, + parser: parser, + printer: printer, + fileHandler: fileHandler, + opener: opener) + } + + func test_command() { + XCTAssertEqual(FocusCommand.command, "focus") + } + + func test_overview() { + XCTAssertEqual(FocusCommand.overview, "Opens Xcode ready to focus on the project in the current directory.") + } + + func test_run_fatalErrors_when_theConfigIsInvalid() throws { + let result = try parser.parse([FocusCommand.command, "-c", "invalid_config"]) + XCTAssertThrowsError(try subject.run(with: result)) + } + + func test_run_fatalErrors_when_theworkspaceGenerationFails() throws { + let result = try parser.parse([FocusCommand.command, "-c", "Debug"]) + var configuration: BuildConfiguration? + let error = NSError.test() + workspaceGenerator.generateStub = { _, _, options, _, _, _ in + configuration = options.buildConfiguration + throw error + } + XCTAssertThrowsError(try subject.run(with: result)) { + XCTAssertEqual($0 as NSError?, error) + } + XCTAssertEqual(configuration, .debug) + } + + func test_run() throws { + let result = try parser.parse([FocusCommand.command, "-c", "Debug"]) + let workspacePath = AbsolutePath("/test.xcworkspace") + workspaceGenerator.generateStub = { _, _, _, _, _, _ in + workspacePath + } + try subject.run(with: result) + + XCTAssertEqual(opener.openArgs.last, workspacePath) + } +} diff --git a/Tests/TuistKitTests/Generator/Mocks/MockWorkspaceGenerator.swift b/Tests/TuistKitTests/Generator/Mocks/MockWorkspaceGenerator.swift index cfb3e9e24..e3d76d052 100644 --- a/Tests/TuistKitTests/Generator/Mocks/MockWorkspaceGenerator.swift +++ b/Tests/TuistKitTests/Generator/Mocks/MockWorkspaceGenerator.swift @@ -4,14 +4,14 @@ import TuistCore @testable import TuistKit final class MockWorkspaceGenerator: WorkspaceGenerating { - var generateStub: ((AbsolutePath, Graphing, GenerationOptions, Systeming, Printing, ResourceLocating) throws -> Void)? + var generateStub: ((AbsolutePath, Graphing, GenerationOptions, Systeming, Printing, ResourceLocating) throws -> AbsolutePath)? func generate(path: AbsolutePath, graph: Graphing, options: GenerationOptions, system: Systeming, printer: Printing, - resourceLocator: ResourceLocating) throws { - try generateStub?(path, graph, options, system, printer, resourceLocator) + resourceLocator: ResourceLocating) throws -> AbsolutePath { + return (try generateStub?(path, graph, options, system, printer, resourceLocator)) ?? AbsolutePath("/test") } }