Add Focus command (#129)
* Add Focus command * Fix a bug on installer * Add entry to the CHANGELOG
This commit is contained in:
parent
037addd237
commit
1eb9b88ac7
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue