Add Focus command (#129)

* Add Focus command

* Fix a bug on installer

* Add entry to the CHANGELOG
This commit is contained in:
Pedro Piñera Buendía 2018-09-11 18:18:09 +02:00 committed by GitHub
parent 037addd237
commit 1eb9b88ac7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 292 additions and 6 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String>
// 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
}
}

View File

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

View File

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

View File

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

View File

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