Add force flag to the update and install command (#157)

* Define force argument

* Add documentation

* Update Installer to accept force installs

* Add tests
This commit is contained in:
Pedro Piñera Buendía 2018-11-07 12:51:54 -05:00 committed by GitHub
parent 3a15268a32
commit 30ff33c3cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 307 additions and 57 deletions

View File

@ -1,6 +1,5 @@
import Basic
import Foundation
import TuistCore
/// Protocol that defines the interface of a local environment controller.
/// It manages the local directory where tuistenv stores the tuist versions and user settings.

View File

@ -81,7 +81,7 @@ final class BundleCommand: Command {
// Installing
if !fileHandler.exists(versionPath) {
printer.print("Version \(version) not available locally. Installing...")
try installer.install(version: version)
try installer.install(version: version, force: false)
}
// Copying

View File

@ -93,7 +93,7 @@ class CommandRunner: CommandRunning {
if let highgestVersion = versionsController.semverVersions().last?.description {
version = highgestVersion
} else {
try updater.update()
try updater.update(force: false)
guard let highgestVersion = versionsController.semverVersions().last?.description else {
throw CommandRunnerError.versionNotFound
}
@ -107,7 +107,7 @@ class CommandRunner: CommandRunning {
func runVersion(_ version: String) throws {
if !versionsController.versions().contains(where: { $0.description == version }) {
printer.print("Version \(version) not found locally. Installing...")
try installer.install(version: version)
try installer.install(version: version, force: false)
}
let path = versionsController.path(version: version)

View File

@ -2,19 +2,33 @@ import Foundation
import TuistCore
import Utility
/// Command that installs new versions of Tuist in the system.
final class InstallCommand: Command {
// MARK: - Command
/// Command name.
static var command: String = "install"
/// Command description.
static var overview: String = "Installs a version of tuist"
// MARK: - Attributes
/// Controller to manage system versions.
private let versionsController: VersionsControlling
/// Printer instance to output messages to the user.
private let printer: Printing
/// Installer instance to run the installation.
private let installer: Installing
/// Version argument to specify the version that will be installed.
let versionArgument: PositionalArgument<String>
/// Force argument (-f). When passed, it re-installs the version compiling it from the source.
let forceArgument: OptionArgument<Bool>
// MARK: - Init
convenience init(parser: ArgumentParser) {
@ -37,15 +51,24 @@ final class InstallCommand: Command {
kind: String.self,
optional: false,
usage: "The version of tuist to be installed")
forceArgument = subParser.add(option: "--force",
shortName: "-f",
kind: Bool.self,
usage: "Re-installs the version compiling it from the source", completion: nil)
}
/// Runs the install command.
///
/// - Parameter result: Result obtained from parsing the CLI arguments.
/// - Throws: An error if the installation process fails.
func run(with result: ArgumentParser.Result) throws {
let force = result.get(forceArgument) ?? false
let version = result.get(versionArgument)!
let versions = versionsController.versions().map({ $0.description })
if versions.contains(version) {
printer.print(warning: "Version \(version) already installed, skipping")
return
}
try installer.install(version: version)
try installer.install(version: version, force: force)
}
}

View File

@ -2,39 +2,63 @@ import Foundation
import TuistCore
import Utility
/// Command that updates the version of Tuist in the environment.
final class UpdateCommand: Command {
// MARK: - Command
/// Name of the command.
static var command: String = "update"
/// Description of the command.
static var overview: String = "Installs the latest version if it's not already installed"
// MARK: - Attributes
private let versionsController: VersionsControlling
/// Updater instance that runs the update.
private let updater: Updating
/// Printer instance to output updates during the process.
private let printer: Printing
/// Force argument (-f). When passed, it re-installs the latest version compiling it from the source.
let forceArgument: OptionArgument<Bool>
// MARK: - Init
/// Initializes the update command.
///
/// - Parameter parser: Argument parser where the command should be registered.
convenience init(parser: ArgumentParser) {
self.init(parser: parser,
versionsController: VersionsController(),
updater: Updater(),
printer: Printer())
}
/// Initializes the update command.
///
/// - Parameters:
/// - parser: Argument parser where the command should be registered.
/// - updater: Updater instance that runs the update.
/// - printer: Printer instance to output updates during the process.
init(parser: ArgumentParser,
versionsController: VersionsControlling,
updater: Updating,
printer: Printer) {
parser.add(subparser: UpdateCommand.command, overview: UpdateCommand.overview)
self.versionsController = versionsController
printer: Printing) {
let subParser = parser.add(subparser: UpdateCommand.command, overview: UpdateCommand.overview)
self.printer = printer
self.updater = updater
forceArgument = subParser.add(option: "--force",
shortName: "-f",
kind: Bool.self,
usage: "Re-installs the latest version compiling it from the source", completion: nil)
}
func run(with _: ArgumentParser.Result) throws {
/// Runs the update command.
///
/// - Parameter result: Result obtained from parsing the CLI arguments.
/// - Throws: An error if the update process fails.
func run(with result: ArgumentParser.Result) throws {
let force = result.get(forceArgument) ?? false
printer.print(section: "Checking for updates...")
try updater.update()
try updater.update(force: force)
}
}

View File

@ -2,10 +2,21 @@ import Basic
import Foundation
import TuistCore
/// Protocol that defines the interface of an instance that can install versions of Tuist.
protocol Installing: AnyObject {
func install(version: String) throws
/// It installs a version of Tuist in the local environment.
///
/// - Parameters:
/// - version: Version to be installed.
/// - force: When true, it ignores the Swift version and compiles it from the source.
/// - Throws: An error if the installation fails.
func install(version: String, force: Bool) throws
}
/// Error thrown by the installer.
///
/// - versionNotFound: When the specified version cannot be found.
/// - incompatibleSwiftVersion: When the environment Swift version is incompatible with the Swift version Tuist has been compiled with.
enum InstallerError: FatalError, Equatable {
case versionNotFound(String)
case incompatibleSwiftVersion(local: String, expected: String)
@ -38,6 +49,7 @@ enum InstallerError: FatalError, Equatable {
}
}
/// Class that manages the installation of Tuist versions.
final class Installer: Installing {
// MARK: - Attributes
@ -66,12 +78,20 @@ final class Installer: Installing {
// MARK: - Installing
func install(version: String) throws {
func install(version: String, force: Bool) throws {
let temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: true)
try install(version: version, temporaryDirectory: temporaryDirectory)
try install(version: version, temporaryDirectory: temporaryDirectory, force: force)
}
func install(version: String, temporaryDirectory: TemporaryDirectory) throws {
func install(version: String, temporaryDirectory: TemporaryDirectory, force: Bool = false) throws {
// We ignore the Swift version and install from the soruce code
if force {
printer.print("Forcing the installation of \(version) from the source code")
try installFromSource(version: version,
temporaryDirectory: temporaryDirectory)
return
}
try verifySwiftVersion(version: version)
var bundleURL: URL?
@ -184,10 +204,11 @@ final class Installer: Installing {
verbose: false,
environment: System.userEnvironment).throwIfError()
// Copying files
if !fileHandler.exists(installationDirectory) {
try system.capture("/bin/mkdir", arguments: installationDirectory.asString, verbose: false, environment: nil).throwIfError()
if fileHandler.exists(installationDirectory) {
try fileHandler.delete(installationDirectory)
}
try fileHandler.createFolder(installationDirectory)
try buildCopier.copy(from: buildDirectory,
to: installationDirectory)

View File

@ -2,7 +2,7 @@ import Foundation
import TuistCore
protocol Updating: AnyObject {
func update() throws
func update(force: Bool) throws
}
final class Updater: Updating {
@ -27,24 +27,27 @@ final class Updater: Updating {
// MARK: - Internal
func update() throws {
func update(force: Bool) throws {
let releases = try githubClient.releases()
guard let highestRemoteVersion = releases.map({ $0.version }).sorted().last else {
print("No remote versions found")
printer.print("No remote versions found")
return
}
if let highestLocalVersion = versionsController.semverVersions().sorted().last {
if force {
printer.print("Forcing the update of version \(highestRemoteVersion)")
try installer.install(version: highestRemoteVersion.description, force: true)
} else if let highestLocalVersion = versionsController.semverVersions().sorted().last {
if highestRemoteVersion <= highestLocalVersion {
printer.print("There are no updates available")
} else {
printer.print("Installing new version available \(highestRemoteVersion)")
try installer.install(version: highestRemoteVersion.description)
try installer.install(version: highestRemoteVersion.description, force: false)
}
} else {
printer.print("No local versions available. Installing the latest version \(highestRemoteVersion)")
try installer.install(version: highestRemoteVersion.description)
try installer.install(version: highestRemoteVersion.description, force: false)
}
}
}

View File

@ -68,8 +68,8 @@ final class BundleCommandTests: XCTestCase {
let tuistVersionPath = fileHandler.currentPath.appending(component: Constants.versionFileName)
try "3.2.1".write(to: tuistVersionPath.url, atomically: true, encoding: .utf8)
installer.installStub = { versionToInstall in
let versionPath = self.versionsController.path(version: versionToInstall)
installer.installStub = { version, _ in
let versionPath = self.versionsController.path(version: version)
try self.fileHandler.createFolder(versionPath)
try Data().write(to: versionPath.appending(component: "test").url)
}
@ -102,8 +102,8 @@ final class BundleCommandTests: XCTestCase {
try "3.2.1".write(to: tuistVersionPath.url, atomically: true, encoding: .utf8)
installer.installStub = { versionToInstall in
let versionPath = self.versionsController.path(version: versionToInstall)
installer.installStub = { version, _ in
let versionPath = self.versionsController.path(version: version)
try self.fileHandler.createFolder(versionPath)
try Data().write(to: versionPath.appending(component: "test").url)
}

View File

@ -84,8 +84,8 @@ final class CommandRunnerTests: XCTestCase {
versionResolver.resolveStub = { _ in ResolvedVersion.versionFile(self.fileHandler.currentPath, "3.2.1") }
var installedVersion: String?
installer.installStub = { installedVersion = $0 }
var installArgs: [(version: String, force: Bool)] = []
installer.installStub = { version, force in installArgs.append((version: version, force: force)) }
system.stub(args: [binaryPath.asString, "--help"],
stderror: nil,
@ -97,7 +97,8 @@ final class CommandRunnerTests: XCTestCase {
XCTAssertEqual(printer.printArgs.count, 2)
XCTAssertEqual(printer.printArgs.first, "Using version 3.2.1 defined at \(fileHandler.currentPath.asString)")
XCTAssertEqual(printer.printArgs.last, "Version 3.2.1 not found locally. Installing...")
XCTAssertEqual(installedVersion, "3.2.1")
XCTAssertEqual(installArgs.count, 1)
XCTAssertEqual(installArgs.first?.version, "3.2.1")
}
func test_when_version_file_and_install_fails() throws {
@ -106,7 +107,7 @@ final class CommandRunnerTests: XCTestCase {
versionResolver.resolveStub = { _ in ResolvedVersion.versionFile(self.fileHandler.currentPath, "3.2.1") }
let error = NSError.test()
installer.installStub = { _ in throw error }
installer.installStub = { _, _ in throw error }
XCTAssertThrowsError(try subject.run()) {
XCTAssertEqual($0 as NSError, error)
@ -159,7 +160,7 @@ final class CommandRunnerTests: XCTestCase {
versionResolver.resolveStub = { _ in ResolvedVersion.undefined }
versionsController.semverVersionsStub = []
updater.updateStub = {
updater.updateStub = { _ in
self.versionsController.semverVersionsStub = [Version(string: "3.2.1")!]
}
@ -182,7 +183,7 @@ final class CommandRunnerTests: XCTestCase {
versionsController.semverVersionsStub = []
let error = NSError.test()
updater.updateStub = {
updater.updateStub = { _ in
throw error
}

View File

@ -54,11 +54,28 @@ final class InstallCommandTests: XCTestCase {
versionsController.versionsStub = []
var installedVersion: String?
installer.installStub = { installedVersion = $0 }
var installArgs: [(version: String, force: Bool)] = []
installer.installStub = { version, force in installArgs.append((version: version, force: force)) }
try subject.run(with: result)
XCTAssertEqual(installedVersion, "3.2.1")
XCTAssertEqual(installArgs.count, 1)
XCTAssertEqual(installArgs.first?.version, "3.2.1")
XCTAssertEqual(installArgs.first?.force, false)
}
func test_run_when_force() throws {
let result = try parser.parse(["install", "3.2.1", "-f"])
versionsController.versionsStub = []
var installArgs: [(version: String, force: Bool)] = []
installer.installStub = { version, force in installArgs.append((version: version, force: force)) }
try subject.run(with: result)
XCTAssertEqual(installArgs.count, 1)
XCTAssertEqual(installArgs.first?.version, "3.2.1")
XCTAssertEqual(installArgs.first?.force, true)
}
}

View File

@ -1,9 +1,26 @@
import Foundation
@testable import TuistEnvKit
import Utility
import XCTest
@testable import TuistCoreTesting
@testable import TuistEnvKit
@testable import Utility
final class UpdateCommandTests: XCTestCase {
var parser: ArgumentParser!
var subject: UpdateCommand!
var updater: MockUpdater!
var printer: MockPrinter!
override func setUp() {
super.setUp()
parser = ArgumentParser(usage: "test", overview: "overview")
updater = MockUpdater()
printer = MockPrinter()
subject = UpdateCommand(parser: parser,
updater: updater,
printer: printer)
}
func test_command() {
XCTAssertEqual(UpdateCommand.command, "update")
}
@ -11,4 +28,24 @@ final class UpdateCommandTests: XCTestCase {
func test_overview() {
XCTAssertEqual(UpdateCommand.overview, "Installs the latest version if it's not already installed")
}
func test_init_registers_the_command() {
XCTAssertEqual(parser.subparsers.count, 1)
XCTAssertEqual(parser.subparsers.first?.key, UpdateCommand.command)
XCTAssertEqual(parser.subparsers.first?.value.overview, UpdateCommand.overview)
}
func test_run() throws {
let result = try parser.parse(["update", "-f"])
var updateCalls: [Bool] = []
updater.updateStub = { force in
updateCalls.append(force)
}
try subject.run(with: result)
XCTAssertEqual(printer.printSectionArgs, ["Checking for updates..."])
XCTAssertEqual(updateCalls, [true])
}
}

View File

@ -180,9 +180,10 @@ final class InstallerTests: XCTestCase {
func test_install_when_no_bundled_release() throws {
let version = "3.2.1"
let temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: true)
let installationDirectory = fileHandler.currentPath.appending(component: "3.2.1")
versionsController.installStub = { _, closure in
try closure(self.fileHandler.currentPath)
try closure(installationDirectory)
}
system.stub(args: [
@ -223,13 +224,6 @@ final class InstallerTests: XCTestCase {
stderror: nil,
stdout: nil,
exitstatus: 0)
system.stub(args: [
"/bin/mkdir",
fileHandler.currentPath.asString,
],
stderror: nil,
stdout: nil,
exitstatus: 0)
try subject.install(version: version, temporaryDirectory: temporaryDirectory)
@ -241,7 +235,68 @@ final class InstallerTests: XCTestCase {
XCTAssertEqual(printer.printArgs[1], "Building using Swift (it might take a while)")
XCTAssertEqual(printer.printArgs[2], "Version 3.2.1 installed")
let tuistVersionPath = fileHandler.currentPath.appending(component: Constants.versionFileName)
let tuistVersionPath = installationDirectory.appending(component: Constants.versionFileName)
XCTAssertTrue(fileHandler.exists(tuistVersionPath))
}
func test_install_when_force() throws {
let version = "3.2.1"
let temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: true)
let installationDirectory = fileHandler.currentPath.appending(component: "3.2.1")
versionsController.installStub = { _, closure in
try closure(installationDirectory)
}
system.stub(args: [
"/usr/bin/env", "git",
"clone", Constants.gitRepositoryURL,
temporaryDirectory.path.asString,
],
stderror: nil,
stdout: nil,
exitstatus: 0)
system.stub(args: [
"/usr/bin/env", "git", "-C", temporaryDirectory.path.asString,
"checkout", version,
],
stderror: nil,
stdout: nil,
exitstatus: 0)
system.stub(args: ["/usr/bin/xcrun", "-f", "swift"],
stderror: nil,
stdout: "/path/to/swift",
exitstatus: 0)
system.stub(args: [
"/path/to/swift", "build",
"--product", "tuist",
"--package-path", temporaryDirectory.path.asString,
"--configuration", "release",
"-Xswiftc", "-static-stdlib",
],
stderror: nil,
stdout: nil,
exitstatus: 0)
system.stub(args: [
"/path/to/swift", "build",
"--product", "ProjectDescription",
"--package-path", temporaryDirectory.path.asString,
"--configuration", "release",
],
stderror: nil,
stdout: nil,
exitstatus: 0)
try subject.install(version: version, temporaryDirectory: temporaryDirectory, force: true)
XCTAssertEqual(printer.printArgs.count, 4)
XCTAssertEqual(printer.printArgs[0], "Forcing the installation of 3.2.1 from the source code")
XCTAssertEqual(printer.printArgs[1], "Pulling source code")
XCTAssertEqual(printer.printArgs[2], "Building using Swift (it might take a while)")
XCTAssertEqual(printer.printArgs[3], "Version 3.2.1 installed")
let tuistVersionPath = installationDirectory.appending(component: Constants.versionFileName)
XCTAssertTrue(fileHandler.exists(tuistVersionPath))
}

View File

@ -3,10 +3,10 @@ import Foundation
final class MockInstaller: Installing {
var installCallCount: UInt = 0
var installStub: ((String) throws -> Void)?
var installStub: ((String, Bool) throws -> Void)?
func install(version: String) throws {
func install(version: String, force: Bool) throws {
installCallCount += 1
try installStub?(version)
try installStub?(version, force)
}
}

View File

@ -3,10 +3,10 @@ import Foundation
final class MockUpdater: Updating {
var updateCallCount: UInt = 0
var updateStub: (() throws -> Void)?
var updateStub: ((Bool) throws -> Void)?
func update() throws {
func update(force: Bool) throws {
updateCallCount += 1
try updateStub?()
try updateStub?(force)
}
}

View File

@ -1,10 +1,80 @@
import Foundation
@testable import TuistEnvKit
import XCTest
@testable import TuistCoreTesting
@testable import TuistEnvKit
final class UpdaterTests: XCTestCase {
var githubClient: MockGitHubClient!
var versionsController: MockVersionsController!
var installer: MockInstaller!
var printer: MockPrinter!
var subject: Updater!
override func setUp() {
githubClient = MockGitHubClient()
versionsController = try! MockVersionsController()
installer = MockInstaller()
printer = MockPrinter()
subject = Updater(githubClient: githubClient,
versionsController: versionsController,
installer: installer,
printer: printer)
}
func test_update_when_no_remote_releases() throws {
githubClient.releasesStub = { [] }
try subject.update(force: false)
XCTAssertEqual(printer.printArgs, ["No remote versions found"])
}
func test_update_when_force() throws {
githubClient.releasesStub = { [Release.test(version: "3.2.1")] }
var installArgs: [(version: String, force: Bool)] = []
installer.installStub = { version, force in installArgs.append((version: version, force: force)) }
try subject.update(force: true)
XCTAssertEqual(printer.printArgs, ["Forcing the update of version 3.2.1"])
XCTAssertEqual(installArgs.count, 1)
XCTAssertEqual(installArgs.first?.version, "3.2.1")
XCTAssertEqual(installArgs.first?.force, true)
}
func test_update_when_there_are_no_updates() throws {
versionsController.semverVersionsStub = ["3.2.1"]
githubClient.releasesStub = { [Release.test(version: "3.2.1")] }
try subject.update(force: false)
XCTAssertEqual(printer.printArgs, ["There are no updates available"])
}
func test_update_when_there_are_updates() throws {
versionsController.semverVersionsStub = ["3.1.1"]
githubClient.releasesStub = { [Release.test(version: "3.2.1")] }
var installArgs: [(version: String, force: Bool)] = []
installer.installStub = { version, force in installArgs.append((version: version, force: force)) }
try subject.update(force: false)
XCTAssertEqual(printer.printArgs, ["Installing new version available 3.2.1"])
XCTAssertEqual(installArgs.count, 1)
XCTAssertEqual(installArgs.first?.version, "3.2.1")
XCTAssertEqual(installArgs.first?.force, false)
}
func test_update_when_no_local_versions_available() throws {
versionsController.semverVersionsStub = []
githubClient.releasesStub = { [Release.test(version: "3.2.1")] }
var installArgs: [(version: String, force: Bool)] = []
installer.installStub = { version, force in installArgs.append((version: version, force: force)) }
try subject.update(force: false)
XCTAssertEqual(printer.printArgs, ["No local versions available. Installing the latest version 3.2.1"])
XCTAssertEqual(installArgs.count, 1)
XCTAssertEqual(installArgs.first?.version, "3.2.1")
XCTAssertEqual(installArgs.first?.force, false)
}
}