Argument parser (#1154)

* Implement GenerateCommand and TuistCommand.

* Handle verbose.

* Add GenerateService tests.

* Remove service protocol.

* Revert parser changes.

* Fix swiftlint issues.

* Readd GenerateService.

* Run swiftformat.

* Adopt ArgumentParser library for Up command

* Fix formatting

* Rewrite ScaffoldCommand.

* Rewrite scaffold and list tests.

* Fix running list subcommand.

* Convert InitCommand.

* Add InitService tests.

* Fix double optional.

* Run swiftformat.

* Rewrite Focus command.

* Migrate edit command.

* Migrate dump command.

* Migrate graph command.

* Migrate lint command.

* Migrate Version command.

* Revert "Migrate Version command."

This reverts commit b4a69d89da.

* Migrate Version command.

* Migrate build command.

* Migrate cache command.

* Migrate CreateIssue command.

* Migrate cloud commands.

* Migrate signing command.

* Migrate local command.

* Rewrite env commands.

* Remove env CommandRegistry.

* Fix install tests.

* Fix editor tests.

* Fix processing tuist command.

* Change options to flag.

* Edit changelog.

Co-authored-by: Daniel Jankowski <daniell.jankowskii@gmail.com>
This commit is contained in:
Marek Fořt 2020-04-13 10:11:12 +02:00 committed by GitHub
parent 623b9e16dc
commit 12c87d111e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 1956 additions and 2272 deletions

View File

@ -6,6 +6,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
### Changed
- Migrate to new argument parser https://github.com/tuist/tuist/pull/1154 by @fortmarek
- Only warn about copying Info.plist when it's the target's Info.plist https://github.com/tuist/tuist/pull/1203 by @sgrgrsn
### Added

View File

@ -109,6 +109,15 @@
"version": "0.4.1"
}
},
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser",
"state": {
"branch": null,
"revision": "8d31a0905c346a45c87773ad50862b5b3df8dff6",
"version": "0.0.4"
}
},
{
"package": "llbuild",
"repositoryURL": "https://github.com/apple/swift-llbuild.git",

View File

@ -38,6 +38,7 @@ let package = Package(
.package(url: "https://github.com/stencilproject/Stencil", .branch("master")),
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", .upToNextMajor(from: "4.1.0")),
.package(url: "https://github.com/httpswift/swifter.git", .upToNextMajor(from: "1.4.7")),
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "0.0.4")),
],
targets: [
.target(
@ -58,7 +59,7 @@ let package = Package(
),
.target(
name: "TuistKit",
dependencies: ["XcodeProj", "SPMUtility", "TuistSupport", "TuistGenerator", "TuistCache", "TuistAutomation", "ProjectDescription", "Signals", "RxSwift", "RxBlocking", "Checksum", "TuistLoader", "TuistInsights", "TuistScaffold", "TuistSigning", "TuistCloud"]
dependencies: ["XcodeProj", "SPMUtility", "ArgumentParser", "TuistSupport", "TuistGenerator", "TuistCache", "TuistAutomation", "ProjectDescription", "Signals", "RxSwift", "RxBlocking", "Checksum", "TuistLoader", "TuistInsights", "TuistScaffold", "TuistSigning", "TuistCloud"]
),
.testTarget(
name: "TuistKitTests",
@ -74,7 +75,7 @@ let package = Package(
),
.target(
name: "TuistEnvKit",
dependencies: ["SPMUtility", "TuistSupport", "RxSwift", "RxBlocking"]
dependencies: ["ArgumentParser", "SPMUtility", "TuistSupport", "RxSwift", "RxBlocking"]
),
.testTarget(
name: "TuistEnvKitTests",

View File

@ -1,87 +1,14 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistSupport
enum BundleCommandError: FatalError, Equatable {
case missingVersionFile(AbsolutePath)
var type: ErrorType {
switch self {
case .missingVersionFile:
return .abort
}
struct BundleCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "bundle",
abstract: "Bundles the version specified in the .tuist-version file into the .tuist-bin directory")
}
var description: String {
switch self {
case let .missingVersionFile(path):
return "Couldn't find a .tuist-version file in the directory \(path.pathString)"
}
}
static func == (lhs: BundleCommandError, rhs: BundleCommandError) -> Bool {
switch (lhs, rhs) {
case let (.missingVersionFile(lhsPath), .missingVersionFile(rhsPath)):
return lhsPath == rhsPath
}
}
}
final class BundleCommand: Command {
// MARK: - Command
static var command: String = "bundle"
static var overview: String = "Bundles the version specified in the .tuist-version file into the .tuist-bin directory"
// MARK: - Attributes
private let versionsController: VersionsControlling
private let installer: Installing
// MARK: - Init
convenience init(parser: ArgumentParser) {
self.init(parser: parser,
versionsController: VersionsController(),
installer: Installer())
}
init(parser: ArgumentParser,
versionsController: VersionsControlling,
installer: Installing) {
_ = parser.add(subparser: BundleCommand.command, overview: BundleCommand.overview)
self.versionsController = versionsController
self.installer = installer
}
// MARK: - Internal
func run(with _: ArgumentParser.Result) throws {
let versionFilePath = FileHandler.shared.currentPath.appending(component: Constants.versionFileName)
let binFolderPath = FileHandler.shared.currentPath.appending(component: Constants.binFolderName)
if !FileHandler.shared.exists(versionFilePath) {
throw BundleCommandError.missingVersionFile(FileHandler.shared.currentPath)
}
let version = try String(contentsOf: versionFilePath.url)
logger.notice("Bundling the version \(version) in the directory \(binFolderPath.pathString)", metadata: .section)
let versionPath = versionsController.path(version: version)
// Installing
if !FileHandler.shared.exists(versionPath) {
logger.notice("Version \(version) not available locally. Installing...")
try installer.install(version: version, force: false)
}
// Copying
if FileHandler.shared.exists(binFolderPath) {
try FileHandler.shared.delete(binFolderPath)
}
try FileHandler.shared.copy(from: versionPath, to: binFolderPath)
logger.notice("tuist bundled successfully at \(binFolderPath.pathString)", metadata: .success)
func run() throws {
try BundleService().run()
}
}

View File

@ -1,86 +0,0 @@
import Basic
import Foundation
import SPMUtility
import TuistSupport
public final class CommandRegistry {
// MARK: - Attributes
let parser: ArgumentParser
var commands: [Command] = []
private let errorHandler: ErrorHandling
private let processArguments: () -> [String]
private let commandRunner: CommandRunning
// MARK: - Init
public convenience init() {
self.init(processArguments: CommandRegistry.processArguments,
commands: [
LocalCommand.self,
BundleCommand.self,
UpdateCommand.self,
InstallCommand.self,
UninstallCommand.self,
VersionCommand.self,
])
}
init(processArguments: @escaping () -> [String],
errorHandler: ErrorHandling = ErrorHandler(),
commandRunner: CommandRunning = CommandRunner(),
commands: [Command.Type] = []) {
parser = ArgumentParser(commandName: "tuist",
usage: "<command> <options>",
overview: "Manage the environment tuist versions.")
self.processArguments = processArguments
self.errorHandler = errorHandler
self.commandRunner = commandRunner
commands.forEach(register)
}
// MARK: - Public
public func run() {
do {
if processArguments().dropFirst().first == "--help-env" {
parser.printUsage(on: stdoutStream)
} else if let parsedArguments = try parse() {
try process(arguments: parsedArguments)
} else {
try commandRunner.run()
}
} catch let error as FatalError {
errorHandler.fatal(error: error)
} catch {
errorHandler.fatal(error: UnhandledError(error: error))
}
}
// MARK: - Fileprivate
private func parse() throws -> ArgumentParser.Result? {
let arguments = Array(processArguments().dropFirst())
guard let firstArgument = arguments.first else { return nil }
if commands.map({ type(of: $0).command }).contains(firstArgument) {
return try parser.parse(arguments)
}
return nil
}
private func register(command: Command.Type) {
commands.append(command.init(parser: parser))
}
private func process(arguments: ArgumentParser.Result) throws {
let subparser = arguments.subparser(parser)!
let command = commands.first(where: { type(of: $0).command == subparser })!
try command.run(with: arguments)
}
// MARK: - Static
static func processArguments() -> [String] {
CommandRunner.arguments()
}
}

View File

@ -1,68 +1,26 @@
import ArgumentParser
import Foundation
import SPMUtility
import TuistSupport
/// 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
/// 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) {
self.init(parser: parser,
versionsController: VersionsController(),
installer: Installer())
struct InstallCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "install",
abstract: "Installs a version of tuist")
}
init(parser: ArgumentParser,
versionsController: VersionsControlling,
installer: Installing) {
let subParser = parser.add(subparser: InstallCommand.command,
overview: InstallCommand.overview)
self.versionsController = versionsController
self.installer = installer
versionArgument = subParser.add(positional: "version",
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)
}
@Argument(
help: "The version of tuist to be installed"
)
var version: String
/// 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) {
logger.warning("Version \(version) already installed, skipping")
return
}
try installer.install(version: version, force: force)
@Flag(
name: .shortAndLong,
help: "Re-installs the version compiling it from the source"
)
var force: Bool
func run() throws {
try InstallService().run(version: version,
force: force)
}
}

View File

@ -1,64 +1,21 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistSupport
class LocalCommand: Command {
// MARK: - Command
static var command: String = "local"
// swiftlint:disable:next line_length
static var overview: String = "Creates a .tuist-version file to pin the tuist version that should be used in the current directory. If the version is not specified, it prints the local versions"
// MARK: - Attributes
let versionArgument: PositionalArgument<String>
let versionController: VersionsControlling
// MARK: - Init
required convenience init(parser: ArgumentParser) {
self.init(parser: parser,
versionController: VersionsController())
struct LocalCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "local",
// swiftlint:disable:next line_length
abstract: "Creates a .tuist-version file to pin the tuist version that should be used in the current directory. If the version is not specified, it prints the local versions")
}
init(parser: ArgumentParser,
versionController: VersionsControlling) {
let subParser = parser.add(subparser: LocalCommand.command,
overview: LocalCommand.overview)
versionArgument = subParser.add(positional: "version",
kind: String.self,
optional: true,
usage: "The version that you would like to pin your current directory to")
self.versionController = versionController
}
@Argument(
help: "The version that you would like to pin your current directory to"
)
var version: String?
// MARK: - Internal
func run(with result: ArgumentParser.Result) throws {
if let version = result.get(versionArgument) {
try createVersionFile(version: version)
} else {
try printLocalVersions()
}
}
// MARK: - Fileprivate
private func printLocalVersions() throws {
logger.notice("The following versions are available in the local environment:", metadata: .section)
let versions = versionController.semverVersions()
let output = versions.sorted().reversed().map { "- \($0)" }.joined(separator: "\n")
logger.notice("\(output)")
}
private func createVersionFile(version: String) throws {
let currentPath = FileHandler.shared.currentPath
logger.notice("Generating \(Constants.versionFileName) file with version \(version)", metadata: .section)
let tuistVersionPath = currentPath.appending(component: Constants.versionFileName)
try "\(version)".write(to: URL(fileURLWithPath: tuistVersionPath.pathString),
atomically: true,
encoding: .utf8)
logger.notice("File generated at path \(tuistVersionPath.pathString)", metadata: .success)
func run() throws {
try LocalService().run(version: version)
}
}

View File

@ -0,0 +1,61 @@
import ArgumentParser
import Foundation
import TuistSupport
public struct TuistCommand: ParsableCommand {
public init() {}
public static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "tuist",
abstract: "Manage the environment tuist versions",
subcommands: [
LocalCommand.self,
BundleCommand.self,
UpdateCommand.self,
InstallCommand.self,
UninstallCommand.self,
VersionCommand.self,
])
}
public static func main(_: [String]? = nil) -> Never {
let errorHandler = ErrorHandler()
do {
let processedArguments = processArguments()
if processedArguments.dropFirst().first == "--help-env" {
throw CleanExit.helpRequest(self)
} else if let parsedArguments = try parse() {
try parseAsRoot(parsedArguments).run()
} else {
try CommandRunner().run()
}
exit()
} catch let error as CleanExit {
_exit(exitCode(for: error).rawValue)
} catch let error as FatalError {
errorHandler.fatal(error: error)
_exit(exitCode(for: error).rawValue)
} catch {
errorHandler.fatal(error: UnhandledError(error: error))
_exit(exitCode(for: error).rawValue)
}
}
// MARK: - Helpers
private static func parse() throws -> [String]? {
let arguments = Array(processArguments().dropFirst())
guard let firstArgument = arguments.first else { return nil }
let containsCommand = configuration.subcommands.map { $0.configuration.commandName }.contains(firstArgument)
if containsCommand {
return arguments
}
return nil
}
// MARK: - Static
static func processArguments() -> [String] {
CommandRunner.arguments()
}
}

View File

@ -1,48 +1,19 @@
import ArgumentParser
import Foundation
import SPMUtility
import TuistSupport
final class UninstallCommand: Command {
// MARK: - Command
static var command: String = "uninstall"
static var overview: String = "Uninstalls a version of tuist"
// MARK: - Attributes
private let versionsController: VersionsControlling
private let installer: Installing
let versionArgument: PositionalArgument<String>
// MARK: - Init
convenience init(parser: ArgumentParser) {
self.init(parser: parser,
versionsController: VersionsController(),
installer: Installer())
struct UninstallCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "uninstall",
abstract: "Uninstalls a version of tuist")
}
init(parser: ArgumentParser,
versionsController: VersionsControlling,
installer: Installing) {
let subParser = parser.add(subparser: UninstallCommand.command,
overview: UninstallCommand.overview)
self.versionsController = versionsController
self.installer = installer
versionArgument = subParser.add(positional: "version",
kind: String.self,
optional: false,
usage: "The version of tuist to be uninstalled")
}
@Argument(
help: "The version of tuist to be uninstalled"
)
var version: String
func run(with result: ArgumentParser.Result) throws {
let version = result.get(versionArgument)!
let versions = versionsController.versions().map { $0.description }
if versions.contains(version) {
try versionsController.uninstall(version: version)
logger.notice("Version \(version) uninstalled", metadata: .success)
} else {
logger.warning("Version \(version) cannot be uninstalled because it's not installed")
}
func run() throws {
try UninstallService().run(version: version)
}
}

View File

@ -1,57 +1,22 @@
import ArgumentParser
import Foundation
import SPMUtility
import TuistSupport
/// 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
/// Updater instance that runs the update.
private let updater: Updating
struct UpdateCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "update",
abstract: "Installs the latest version if it's not already installed")
}
/// Force argument (-f). When passed, it re-installs the latest version compiling it from the source.
let forceArgument: OptionArgument<Bool>
@Flag(
name: .shortAndLong,
help: "Re-installs the latest version compiling it from the source"
)
var force: 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,
updater: Updater())
}
/// Initializes the update command.
///
/// - Parameters:
/// - parser: Argument parser where the command should be registered.
/// - updater: Updater instance that runs the update.
init(parser: ArgumentParser,
updater: Updating) {
let subParser = parser.add(subparser: UpdateCommand.command, overview: UpdateCommand.overview)
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)
}
/// 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
logger.notice("Checking for updates...", metadata: .section)
try updater.update(force: force)
func run() throws {
try UpdateService().run(force: force)
}
}

View File

@ -1,23 +1,11 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistSupport
class VersionCommand: NSObject, Command {
// MARK: - Command
static let command = "envversion"
static let overview = "Outputs the current version of tuist env."
// MARK: - Init
required init(parser: ArgumentParser) {
parser.add(subparser: VersionCommand.command, overview: VersionCommand.overview)
}
// MARK: - Command
func run(with _: ArgumentParser.Result) {
logger.notice("\(Constants.version)")
struct VersionCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "envversion",
abstract: "Outputs the current version of tuist env.")
}
}

View File

@ -0,0 +1,67 @@
import Basic
import Foundation
import TuistSupport
enum BundleServiceError: FatalError, Equatable {
case missingVersionFile(AbsolutePath)
var type: ErrorType {
switch self {
case .missingVersionFile:
return .abort
}
}
var description: String {
switch self {
case let .missingVersionFile(path):
return "Couldn't find a .tuist-version file in the directory \(path.pathString)"
}
}
static func == (lhs: BundleServiceError, rhs: BundleServiceError) -> Bool {
switch (lhs, rhs) {
case let (.missingVersionFile(lhsPath), .missingVersionFile(rhsPath)):
return lhsPath == rhsPath
}
}
}
final class BundleService {
private let versionsController: VersionsControlling
private let installer: Installing
init(versionsController: VersionsControlling = VersionsController(),
installer: Installing = Installer()) {
self.versionsController = versionsController
self.installer = installer
}
func run() throws {
let versionFilePath = FileHandler.shared.currentPath.appending(component: Constants.versionFileName)
let binFolderPath = FileHandler.shared.currentPath.appending(component: Constants.binFolderName)
if !FileHandler.shared.exists(versionFilePath) {
throw BundleServiceError.missingVersionFile(FileHandler.shared.currentPath)
}
let version = try String(contentsOf: versionFilePath.url)
logger.notice("Bundling the version \(version) in the directory \(binFolderPath.pathString)", metadata: .section)
let versionPath = versionsController.path(version: version)
// Installing
if !FileHandler.shared.exists(versionPath) {
logger.notice("Version \(version) not available locally. Installing...")
try installer.install(version: version, force: false)
}
// Copying
if FileHandler.shared.exists(binFolderPath) {
try FileHandler.shared.delete(binFolderPath)
}
try FileHandler.shared.copy(from: versionPath, to: binFolderPath)
logger.notice("tuist bundled successfully at \(binFolderPath.pathString)", metadata: .success)
}
}

View File

@ -0,0 +1,25 @@
import Foundation
import TuistSupport
final class InstallService {
/// Controller to manage system versions.
private let versionsController: VersionsControlling
/// Installer instance to run the installation.
private let installer: Installing
init(versionsController: VersionsControlling = VersionsController(),
installer: Installing = Installer()) {
self.versionsController = versionsController
self.installer = installer
}
func run(version: String, force: Bool) throws {
let versions = versionsController.versions().map { $0.description }
if versions.contains(version) {
logger.warning("Version \(version) already installed, skipping")
return
}
try installer.install(version: version, force: force)
}
}

View File

@ -0,0 +1,38 @@
import Basic
import Foundation
import TuistSupport
final class LocalService {
private let versionController: VersionsControlling
init(versionController: VersionsControlling = VersionsController()) {
self.versionController = versionController
}
func run(version: String?) throws {
if let version = version {
try createVersionFile(version: version)
} else {
try printLocalVersions()
}
}
// MARK: - Helpers
private func printLocalVersions() throws {
logger.notice("The following versions are available in the local environment:", metadata: .section)
let versions = versionController.semverVersions()
let output = versions.sorted().reversed().map { "- \($0)" }.joined(separator: "\n")
logger.notice("\(output)")
}
private func createVersionFile(version: String) throws {
let currentPath = FileHandler.shared.currentPath
logger.notice("Generating \(Constants.versionFileName) file with version \(version)", metadata: .section)
let tuistVersionPath = currentPath.appending(component: Constants.versionFileName)
try "\(version)".write(to: URL(fileURLWithPath: tuistVersionPath.pathString),
atomically: true,
encoding: .utf8)
logger.notice("File generated at path \(tuistVersionPath.pathString)", metadata: .success)
}
}

View File

@ -0,0 +1,26 @@
import Foundation
import TuistSupport
final class UninstallService {
/// Controller to manage system versions.
private let versionsController: VersionsControlling
/// Installer instance to run the installation.
private let installer: Installing
init(versionsController: VersionsControlling = VersionsController(),
installer: Installing = Installer()) {
self.versionsController = versionsController
self.installer = installer
}
func run(version: String) throws {
let versions = versionsController.versions().map { $0.description }
if versions.contains(version) {
try versionsController.uninstall(version: version)
logger.notice("Version \(version) uninstalled", metadata: .success)
} else {
logger.warning("Version \(version) cannot be uninstalled because it's not installed")
}
}
}

View File

@ -0,0 +1,16 @@
import Foundation
import TuistSupport
final class UpdateService {
/// Updater instance that runs the update.
private let updater: Updating
init(updater: Updating = Updater()) {
self.updater = updater
}
func run(force: Bool) throws {
logger.notice("Checking for updates...", metadata: .section)
try updater.update(force: force)
}
}

View File

@ -0,0 +1,9 @@
import Basic
import Foundation
import TuistSupport
final class VersionService {
func run() throws {
logger.notice("\(Constants.version)")
}
}

View File

@ -1,32 +1,15 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistSupport
enum BuildCommandError: FatalError {
// Error description
var description: String {
""
}
// Error type
var type: ErrorType { .abort }
}
/// Command that builds a target from the project in the current directory.
class BuildCommand: NSObject, RawCommand {
/// Command name.
static var command: String = "build"
/// Command description.
static var overview: String = "Builds a project target."
/// Default constructor.
required override init() {
super.init()
struct BuildCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "build",
abstract: "Builds a project target")
}
func run(arguments _: [String]) throws {
logger.notice("Command not available yet")
func run() throws {
try BuildService().run()
}
}

View File

@ -1,61 +1,22 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistSupport
/// Command to cache frameworks as .xcframeworks and speed up your and others' build times.
class CacheCommand: NSObject, Command {
// MARK: - Attributes
/// Name of the command.
static let command = "cache"
/// Description of the command.
static let overview = "Cache frameworks as .xcframeworks to speed up build times in generated projects"
/// Path to the project directory.
let pathArgument: OptionArgument<String>
/// Cache controller.
let cacheController: CacheControlling
// MARK: - Init
/// Initializes the command with the CLI parser.
///
/// - Parameter parser: CLI parser where the command should register itself.
public required convenience init(parser: ArgumentParser) {
self.init(parser: parser, cacheController: CacheController())
struct CacheCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "cache",
abstract: "Cache frameworks as .xcframeworks to speed up build times in generated projects")
}
public init(parser: ArgumentParser,
cacheController: CacheControlling) {
let subParser = parser.add(subparser: CacheCommand.command, overview: CacheCommand.overview)
self.cacheController = cacheController
pathArgument = subParser.add(option: "--path",
shortName: "-p",
kind: String.self,
usage: "The path to the directory that contains the project whose frameworks will be cached.",
completion: .filename)
}
@Option(
name: .shortAndLong,
help: "The path to the directory that contains the project whose frameworks will be cached"
)
var path: String?
/// Runs the command using the result from parsing the command line arguments.
///
/// - Throws: An error if the the configuration of the environment fails.
func run(with result: ArgumentParser.Result) throws {
let path = self.path(arguments: result)
try cacheController.cache(path: path)
}
/// Parses the arguments and returns the path to the directory where
/// the up command should be ran.
///
/// - Parameter arguments: Result from parsing the command line arguments.
/// - Returns: Path to be used for the up command.
private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
guard let path = arguments.get(pathArgument) else {
return FileHandler.shared.currentPath
}
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
func run() throws {
try CacheService().run(path: path)
}
}

View File

@ -1,24 +1,14 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistCore
import TuistSigning
import TuistSupport
class CloudAuthCommand: NSObject, Command {
// MARK: - Attributes
static let command = "auth"
static let overview = "Authenticates the user on the server with the URL defined in the Config.swift file."
private let cloudAuthService = CloudAuthService()
// MARK: - Init
public required init(parser: ArgumentParser) {
_ = parser.add(subparser: CloudAuthCommand.command, overview: CloudAuthCommand.overview)
struct CloudAuthCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "auth",
abstract: "Authenticates the user on the server with the URL defined in the Config.swift file")
}
func run(with _: ArgumentParser.Result) throws {
try cloudAuthService.authenticate()
func run() throws {
try CloudAuthService().authenticate()
}
}

View File

@ -1,24 +1,14 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistCore
import TuistSigning
import TuistSupport
class CloudLogoutCommand: NSObject, Command {
// MARK: - Attributes
static let command = "logout"
static let overview = "Removes any existing session to authenticate on the server with the URL defined in the Config.swift file."
private let service = CloudLogoutService()
// MARK: - Init
public required init(parser: ArgumentParser) {
_ = parser.add(subparser: CloudLogoutCommand.command, overview: CloudLogoutCommand.overview)
struct CloudLogoutCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "logout",
abstract: "Removes any existing session to authenticate on the server with the URL defined in the Config.swift file")
}
func run(with _: ArgumentParser.Result) throws {
try service.logout()
func run() throws {
try CloudLogoutService().logout()
}
}

View File

@ -1,24 +1,14 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistCore
import TuistSigning
import TuistSupport
class CloudSessionCommand: NSObject, Command {
// MARK: - Attributes
static let command = "session"
static let overview = "Prints any existing session to authenticate on the server with the URL defined in the Config.swift file."
private let service = CloudSessionService()
// MARK: - Init
public required init(parser: ArgumentParser) {
_ = parser.add(subparser: CloudSessionCommand.command, overview: CloudSessionCommand.overview)
struct CloudSessionCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "session",
abstract: "Prints any existing session to authenticate on the server with the URL defined in the Config.swift file")
}
func run(with _: ArgumentParser.Result) throws {
try service.printSession()
func run() throws {
try CloudSessionService().printSession()
}
}

View File

@ -1,34 +1,14 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistCore
import TuistSigning
import TuistSupport
class CloudCommand: NSObject, Command {
// MARK: - Attributes
static let command = "cloud"
static let overview = "A set of commands for cloud-related operations."
let subcommands: [Command]
private let argumentParser: ArgumentParser
// MARK: - Init
public required init(parser: ArgumentParser) {
_ = parser.add(subparser: CloudCommand.command, overview: CloudCommand.overview)
let argumentParser = ArgumentParser(commandName: Self.command, usage: "tuist cloud <command> <options>", overview: Self.overview)
let subcommands: [Command.Type] = [CloudAuthCommand.self, CloudSessionCommand.self, CloudLogoutCommand.self]
self.subcommands = subcommands.map { $0.init(parser: argumentParser) }
self.argumentParser = argumentParser
}
func parse(with _: ArgumentParser, arguments: [String]) throws -> (ArgumentParser.Result, ArgumentParser) {
return (try argumentParser.parse(Array(arguments.dropFirst())), argumentParser)
}
func run(with _: ArgumentParser.Result) throws {
argumentParser.printUsage(on: stdoutStream)
struct CloudCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "cloud",
abstract: "A set of commands for cloud-related operations", subcommands: [
CloudAuthCommand.self,
CloudSessionCommand.self,
CloudLogoutCommand.self,
])
}
}

View File

@ -1,135 +0,0 @@
import Basic
import Foundation
import SPMUtility
import TuistSupport
public final class CommandRegistry {
// MARK: - Attributes
let parser: ArgumentParser
var commands: [Command] = []
var rawCommands: [RawCommand] = []
var hiddenCommands: [String: HiddenCommand] = [:]
private let errorHandler: ErrorHandling
private let processArguments: () -> [String]
// MARK: - Init
public convenience init() {
self.init(errorHandler: ErrorHandler(),
processArguments: CommandRegistry.processArguments)
register(command: InitCommand.self)
register(command: ScaffoldCommand.self)
register(command: GenerateCommand.self)
register(command: DumpCommand.self)
register(command: VersionCommand.self)
register(command: CreateIssueCommand.self)
register(command: FocusCommand.self)
register(command: UpCommand.self)
register(command: GraphCommand.self)
register(command: EditCommand.self)
register(command: CacheCommand.self)
register(command: LintCommand.self)
register(command: SigningCommand.self)
register(command: CloudCommand.self)
register(rawCommand: BuildCommand.self)
}
init(errorHandler: ErrorHandling,
processArguments: @escaping () -> [String]) {
self.errorHandler = errorHandler
parser = ArgumentParser(commandName: "tuist",
usage: "<command> <options>",
overview: "Generate, build and test your Xcode projects.")
self.processArguments = processArguments
}
public static func processArguments() -> [String] {
Array(ProcessInfo.processInfo.arguments).filter { $0 != "--verbose" }
}
// MARK: - Internal
func register(command: Command.Type) {
commands.append(command.init(parser: parser))
}
func register(hiddenCommand command: HiddenCommand.Type) {
hiddenCommands[command.command] = command.init()
}
func register(rawCommand command: RawCommand.Type) {
rawCommands.append(command.init())
parser.add(subparser: command.command, overview: command.overview)
}
// MARK: - Public
public func run() {
do {
// Hidden command
if let hiddenCommand = hiddenCommand() {
try hiddenCommand.run(arguments: argumentsDroppingCommand())
// Raw command
} else if let commandName = commandName(),
let command = rawCommands.first(where: { type(of: $0).command == commandName }) {
try command.run(arguments: argumentsDroppingCommand())
// Normal command
} else {
guard let (parsedArguments, parser) = try parse() else {
self.parser.printUsage(on: stdoutStream)
return
}
try process(arguments: parsedArguments, parser: parser)
}
} catch let error as FatalError {
errorHandler.fatal(error: error)
} catch {
errorHandler.fatal(error: UnhandledError(error: error))
}
}
// MARK: - Fileprivate
func argumentsDroppingCommand() -> [String] {
Array(processArguments().dropFirst(2))
}
/// Returns the command name.
///
/// - Returns: Command name.
func commandName() -> String? {
let arguments = processArguments()
if arguments.count < 2 { return nil }
return arguments[1]
}
private func parse() throws -> (ArgumentParser.Result, ArgumentParser)? {
let arguments = Array(processArguments().dropFirst())
guard let argumentName = arguments.first else { return nil }
let subparser = try parser.parse([argumentName]).subparser(parser)
if let command = commands.first(where: { type(of: $0).command == subparser }) {
return try command.parse(with: parser, arguments: arguments)
}
return (try parser.parse(arguments), parser)
}
private func hiddenCommand() -> HiddenCommand? {
let arguments = Array(processArguments().dropFirst())
guard let commandName = arguments.first else { return nil }
return hiddenCommands[commandName]
}
private func process(arguments: ArgumentParser.Result, parser: ArgumentParser) throws {
guard let subparser = arguments.subparser(parser) else {
parser.printUsage(on: stdoutStream)
return
}
let allCommands = commands + commands.flatMap { $0.subcommands }
if let command = allCommands.first(where: { type(of: $0).command == subparser }) {
try command.run(with: arguments)
}
}
}

View File

@ -1,25 +1,14 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistSupport
class CreateIssueCommand: NSObject, Command {
static let createIssueUrl: String = "https://github.com/tuist/tuist/issues/new"
// MARK: - Command
static let command = "create-issue"
static let overview = "Opens the GitHub page to create a new issue."
// MARK: - Init
required init(parser: ArgumentParser) {
parser.add(subparser: CreateIssueCommand.command, overview: CreateIssueCommand.overview)
struct CreateIssueCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "create-issue",
abstract: "Opens the GitHub page to create a new issue")
}
// MARK: - Command
func run(with _: ArgumentParser.Result) throws {
try System.shared.run("/usr/bin/open", CreateIssueCommand.createIssueUrl)
func run() throws {
try CreateIssueService().run()
}
}

View File

@ -1,49 +1,24 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistLoader
import TuistSupport
class DumpCommand: NSObject, Command {
// MARK: - Command
static let command = "dump"
static let overview = "Outputs the project manifest as a JSON"
struct DumpCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "dump",
abstract: "Outputs the project manifest as a JSON")
}
// MARK: - Attributes
private let manifestLoader: ManifestLoading
let pathArgument: OptionArgument<String>
@Option(
name: .shortAndLong,
help: "The path to the folder where the project manifest is"
)
var path: String?
// MARK: - Init
public required convenience init(parser: ArgumentParser) {
self.init(manifestLoader: ManifestLoader(),
parser: parser)
}
init(manifestLoader: ManifestLoading,
parser: ArgumentParser) {
let subParser = parser.add(subparser: DumpCommand.command, overview: DumpCommand.overview)
self.manifestLoader = manifestLoader
pathArgument = subParser.add(option: "--path",
shortName: "-p",
kind: String.self,
usage: "The path to the folder where the project manifest is",
completion: .filename)
}
// MARK: - Command
func run(with arguments: ArgumentParser.Result) throws {
var path: AbsolutePath!
if let argumentPath = arguments.get(pathArgument) {
path = AbsolutePath(argumentPath, relativeTo: AbsolutePath.current)
} else {
path = AbsolutePath.current
}
let project = try manifestLoader.loadProject(at: path)
let json: JSON = try project.toJSON()
logger.notice("\(json.toString(prettyPrint: true))")
func run() throws {
try DumpService().run(path: path)
}
}

View File

@ -1,88 +1,30 @@
import ArgumentParser
import Basic
import Foundation
import Signals
import SPMUtility
import TuistGenerator
import TuistSupport
class EditCommand: NSObject, Command {
// MARK: - Static
static let command = "edit"
static let overview = "Generates a temporary project to edit the project in the current directory"
// MARK: - Attributes
private let projectEditor: ProjectEditing
private let opener: Opening
private let pathArgument: OptionArgument<String>
private let permanentArgument: OptionArgument<Bool>
// MARK: - Init
required convenience init(parser: ArgumentParser) {
self.init(parser: parser, projectEditor: ProjectEditor(), opener: Opener())
struct EditCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "edit",
abstract: "Generates a temporary project to edit the project in the current directory")
}
init(parser: ArgumentParser, projectEditor: ProjectEditing, opener: Opening) {
let subparser = parser.add(subparser: EditCommand.command, overview: EditCommand.overview)
pathArgument = subparser.add(option: "--path",
shortName: "-p",
kind: String.self,
usage: "The path to the directory whose project will be edited.",
completion: .filename)
permanentArgument = subparser.add(option: "--permanent",
shortName: "-P",
kind: Bool.self,
usage: "It creates the project in the current directory or the one indicated by -p and doesn't block the process.") // swiftlint:disable:this line_length
@Option(
name: .shortAndLong,
help: "The path to the directory whose project will be edited"
)
var path: String?
self.projectEditor = projectEditor
self.opener = opener
}
@Flag(
name: .shortAndLong,
help: "It creates the project in the current directory or the one indicated by -p and doesn't block the process"
)
var permanent: Bool
func run(with arguments: ArgumentParser.Result) throws {
let path = self.path(arguments: arguments)
let permanent = self.permanent(arguments: arguments)
let generationDirectory = permanent ? path : EditCommand.temporaryDirectory.path
let xcodeprojPath = try projectEditor.edit(at: path, in: generationDirectory)
if !permanent {
Signals.trap(signals: [.int, .abrt]) { _ in
// swiftlint:disable:next force_try
try! FileHandler.shared.delete(EditCommand.temporaryDirectory.path)
exit(0)
}
logger.pretty("Opening Xcode to edit the project. Press \(.keystroke("CTRL + C")) once you are done editing")
try opener.open(path: xcodeprojPath)
} else {
logger.notice("Xcode project generated at \(xcodeprojPath.pathString)", metadata: .success)
}
}
// MARK: - Fileprivate
fileprivate static var _temporaryDirectory: TemporaryDirectory?
fileprivate static var temporaryDirectory: TemporaryDirectory {
// swiftlint:disable:next identifier_name
if let _temporaryDirectory = _temporaryDirectory { return _temporaryDirectory }
// swiftlint:disable:next force_try
_temporaryDirectory = try! TemporaryDirectory(removeTreeOnDeinit: true)
return _temporaryDirectory!
}
private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
if let path = arguments.get(pathArgument) {
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
} else {
return FileHandler.shared.currentPath
}
}
private func permanent(arguments: ArgumentParser.Result) -> Bool {
if let permanent = arguments.get(permanentArgument) {
return permanent
} else {
return false
}
func run() throws {
try EditService().run(path: path,
permanent: permanent)
}
}

View File

@ -1,8 +1,8 @@
import ArgumentParser
import Basic
import Foundation
import RxBlocking
import RxSwift
import SPMUtility
import TuistCache
import TuistCore
import TuistGenerator
@ -10,54 +10,13 @@ import TuistLoader
import TuistSupport
/// The focus command generates the Xcode workspace and launches it on Xcode.
class FocusCommand: NSObject, Command {
// MARK: - Static
/// Command name that is used for the CLI.
static let command = "focus"
/// Command description that is shown when using help from the CLI.
static let overview = "Opens Xcode ready to focus on the project in the current directory."
// MARK: - Attributes
/// Generator instance to generate the project workspace.
private let generator: ProjectGenerating
/// Opener instance to run open in the system.
private let opener: Opening
// MARK: - Init
/// Initializes the focus command with the argument parser where the command needs to register itself.
///
/// - Parameter parser: Argument parser that parses the CLI arguments.
required convenience init(parser: ArgumentParser) {
self.init(parser: parser,
generator: ProjectGenerator(graphMapperProvider: GraphMapperProvider(useCache: true)),
opener: Opener())
struct FocusCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "focus",
abstract: "Opens Xcode ready to focus on the project in the current directory")
}
/// Initializes the focus command with its attributes.
///
/// - Parameters:
/// - parser: Argument parser that parses the CLI arguments.
/// - generator: Generator instance to generate the project workspace.
/// - opener: Opener instance to run open in the system.
init(parser: ArgumentParser,
generator: ProjectGenerating,
opener: Opening) {
parser.add(subparser: FocusCommand.command, overview: FocusCommand.overview)
self.generator = generator
self.opener = opener
}
func run(with _: ArgumentParser.Result) throws {
let path = FileHandler.shared.currentPath
let workspacePath = try generator.generate(path: path,
projectOnly: false)
try opener.open(path: workspacePath)
func run() throws {
try FocusService().run()
}
}

View File

@ -1,70 +1,28 @@
import Basic
import ArgumentParser
import Foundation
import SPMUtility
import TuistGenerator
import TuistLoader
import TuistSupport
class GenerateCommand: NSObject, Command {
// MARK: - Static
static let command = "generate"
static let overview = "Generates an Xcode workspace to start working on the project."
// MARK: - Attributes
private let clock: Clock
private let generator: ProjectGenerating
let pathArgument: OptionArgument<String>
let projectOnlyArgument: OptionArgument<Bool>
// MARK: - Init
required convenience init(parser: ArgumentParser) {
let projectGenerator = ProjectGenerator()
self.init(parser: parser,
generator: projectGenerator,
clock: WallClock())
struct GenerateCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "generate",
abstract: "Generates an Xcode workspace to start working on the project.",
subcommands: [])
}
init(parser: ArgumentParser,
generator: ProjectGenerating,
clock: Clock) {
let subParser = parser.add(subparser: GenerateCommand.command, overview: GenerateCommand.overview)
self.generator = generator
self.clock = clock
@Option(
name: .shortAndLong,
help: "The path where the project will be generated."
)
var path: String?
pathArgument = subParser.add(option: "--path",
shortName: "-p",
kind: String.self,
usage: "The path where the project will be generated.",
completion: .filename)
@Option(
name: .shortAndLong,
default: false,
help: "Only generate the local project (without generating its dependencies)."
)
var projectOnly: Bool
projectOnlyArgument = subParser.add(option: "--project-only",
kind: Bool.self,
usage: "Only generate the local project (without generating its dependencies).")
}
func run(with arguments: ArgumentParser.Result) throws {
let timer = clock.startTimer()
let path = self.path(arguments: arguments)
let projectOnly = arguments.get(projectOnlyArgument) ?? false
try generator.generate(path: path, projectOnly: projectOnly)
let time = String(format: "%.3f", timer.stop())
logger.notice("Project generated.", metadata: .success)
logger.notice("Total time taken: \(time)s")
}
// MARK: - Fileprivate
private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
if let path = arguments.get(pathArgument) {
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
} else {
return FileHandler.shared.currentPath
}
func run() throws {
try GenerateService().run(path: path,
projectOnly: projectOnly)
}
}

View File

@ -1,55 +1,18 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistGenerator
import TuistLoader
import TuistSupport
/// Command that generates and exports a dot graph from the workspace or project in the current directory.
class GraphCommand: NSObject, Command {
/// Command name.
static var command: String = "graph"
/// Command description.
static var overview: String = "Generates a dot graph from the workspace or project in the current directory."
/// Dot graph generator.
let dotGraphGenerator: DotGraphGenerating
/// Manifest loader.
let manifestLoader: ManifestLoading
required convenience init(parser: ArgumentParser) {
let manifestLoader = ManifestLoader()
let manifestLinter = ManifestLinter()
let modelLoader = GeneratorModelLoader(manifestLoader: manifestLoader,
manifestLinter: manifestLinter)
let dotGraphGenerator = DotGraphGenerator(modelLoader: modelLoader)
self.init(parser: parser,
dotGraphGenerator: dotGraphGenerator,
manifestLoader: manifestLoader)
struct GraphCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "graph",
abstract: "Generates a dot graph from the workspace or project in the current directory")
}
init(parser: ArgumentParser,
dotGraphGenerator: DotGraphGenerating,
manifestLoader: ManifestLoading) {
parser.add(subparser: GraphCommand.command, overview: GraphCommand.overview)
self.dotGraphGenerator = dotGraphGenerator
self.manifestLoader = manifestLoader
}
func run(with _: ArgumentParser.Result) throws {
let graph = try dotGraphGenerator.generate(at: FileHandler.shared.currentPath,
manifestLoader: manifestLoader)
let path = FileHandler.shared.currentPath.appending(component: "graph.dot")
if FileHandler.shared.exists(path) {
logger.notice("Deleting existing graph at \(path.pathString)")
try FileHandler.shared.delete(path)
}
try FileHandler.shared.write(graph, path: path, atomically: true)
logger.notice("Graph exported to \(path.pathString)", metadata: .success)
func run() throws {
try GraphService().run()
}
}

View File

@ -1,6 +1,6 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistCore
import TuistGenerator
import TuistLoader
@ -10,263 +10,160 @@ import TuistSupport
private typealias Platform = TuistCore.Platform
private typealias Product = TuistCore.Product
enum InitCommandError: FatalError, Equatable {
case ungettableProjectName(AbsolutePath)
case nonEmptyDirectory(AbsolutePath)
case templateNotFound(String)
case templateNotProvided
case attributeNotProvided(String)
var type: ErrorType {
.abort
struct InitCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "init",
abstract: "Bootstraps a project")
}
var description: String {
switch self {
case let .templateNotFound(template):
return "Could not find template \(template). Make sure it exists at Tuist/Templates/\(template)"
case .templateNotProvided:
return "You must provide template name"
case let .ungettableProjectName(path):
return "Couldn't infer the project name from path \(path.pathString)."
case let .nonEmptyDirectory(path):
return "Can't initialize a project in the non-empty directory at path \(path.pathString)."
case let .attributeNotProvided(name):
return "You must provide \(name) option. Add --\(name) desired_value to your command."
@Option(
name: .shortAndLong,
help: "The platform (ios, tvos or macos) the product will be for (Default: ios)"
)
var platform: String?
@Option(
name: .shortAndLong,
help: "The path to the folder where the project will be generated (Default: Current directory)"
)
var path: String?
@Option(
name: .shortAndLong,
help: "The name of the project. If it's not passed (Default: Name of the directory)"
)
var name: String?
@Option(
name: .shortAndLong,
help: "The name of the template to use (you can list available templates with tuist scaffold list)"
)
var template: String?
var requiredTemplateOptions: [String: String] = [:]
var optionalTemplateOptions: [String: String?] = [:]
init() {}
// Custom decoding to decode dynamic options
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
platform = try container.decodeIfPresent(Option<String>.self, forKey: .platform)?.wrappedValue
name = try container.decodeIfPresent(Option<String>.self, forKey: .name)?.wrappedValue
template = try container.decodeIfPresent(Option<String>.self, forKey: .template)?.wrappedValue
path = try container.decodeIfPresent(Option<String>.self, forKey: .path)?.wrappedValue
try InitCommand.requiredTemplateOptions.forEach { option in
requiredTemplateOptions[option.name] = try container.decode(Option<String>.self,
forKey: .required(option.name)).wrappedValue
}
try InitCommand.optionalTemplateOptions.forEach { option in
optionalTemplateOptions[option.name] = try container.decode(Option<String?>.self,
forKey: .optional(option.name)).wrappedValue
}
}
static func == (lhs: InitCommandError, rhs: InitCommandError) -> Bool {
switch (lhs, rhs) {
case let (.ungettableProjectName(lhsPath), .ungettableProjectName(rhsPath)):
return lhsPath == rhsPath
case let (.nonEmptyDirectory(lhsPath), .nonEmptyDirectory(rhsPath)):
return lhsPath == rhsPath
case let (.templateNotFound(lhsTemplate), .templateNotFound(rhsTemplate)):
return lhsTemplate == rhsTemplate
case (.templateNotProvided, .templateNotProvided):
return true
default:
return false
}
func run() throws {
try InitService().run(name: name,
platform: platform,
path: path,
templateName: template,
requiredTemplateOptions: requiredTemplateOptions,
optionalTemplateOptions: optionalTemplateOptions)
}
}
class InitCommand: NSObject, Command {
// MARK: - Attributes
// MARK: - Preprocessing
static let command = "init"
static let overview = "Bootstraps a project."
private let platformArgument: OptionArgument<String>
private let pathArgument: OptionArgument<String>
private let nameArgument: OptionArgument<String>
private let templateArgument: OptionArgument<String>
private var attributesArguments: [String: OptionArgument<String>] = [:]
private let subParser: ArgumentParser
private let templatesDirectoryLocator: TemplatesDirectoryLocating
private let templateGenerator: TemplateGenerating
private let templateLoader: TemplateLoading
extension InitCommand {
static var requiredTemplateOptions: [(name: String, option: Option<String>)] = []
static var optionalTemplateOptions: [(name: String, option: Option<String?>)] = []
// MARK: - Init
/// We do not know template's option in advance -> we need to dynamically add them
static func preprocess(_ arguments: [String]? = nil) throws {
guard
let arguments = arguments,
arguments.contains("--template")
else { return }
public required convenience init(parser: ArgumentParser) {
self.init(parser: parser,
templatesDirectoryLocator: TemplatesDirectoryLocator(),
templateGenerator: TemplateGenerator(),
templateLoader: TemplateLoader())
}
init(parser: ArgumentParser,
templatesDirectoryLocator: TemplatesDirectoryLocating,
templateGenerator: TemplateGenerating,
templateLoader: TemplateLoading) {
subParser = parser.add(subparser: InitCommand.command, overview: InitCommand.overview)
platformArgument = subParser.add(option: "--platform",
shortName: nil,
kind: String.self,
usage: "The platform (ios, tvos or macos) the product will be for (Default: ios).",
completion: ShellCompletion.values([
(value: "ios", description: "iOS platform"),
(value: "tvos", description: "tvOS platform"),
(value: "macos", description: "macOS platform"),
]))
pathArgument = subParser.add(option: "--path",
shortName: "-p",
kind: String.self,
usage: "The path to the folder where the project will be generated (Default: Current directory).",
completion: .filename)
nameArgument = subParser.add(option: "--name",
shortName: "-n",
kind: String.self,
usage: "The name of the project. If it's not passed (Default: Name of the directory).",
completion: nil)
templateArgument = subParser.add(option: "--template",
shortName: "-t",
kind: String.self,
usage: "The name of the template to use (you can list available templates with tuist scaffold --list).",
completion: nil)
self.templatesDirectoryLocator = templatesDirectoryLocator
self.templateGenerator = templateGenerator
self.templateLoader = templateLoader
}
func parse(with parser: ArgumentParser, arguments: [String]) throws -> ArgumentParser.Result {
guard arguments.contains("--template") else { return try parser.parse(arguments) }
// Plucking out path and template argument
let pairedArguments = stride(from: 1, to: arguments.count, by: 2).map {
arguments[$0 ..< min($0 + 2, arguments.count)]
// We want to parse only the name of template, not its arguments which will be dynamically added
// Plucking out path argument
let pairedArguments: [[String]] = stride(from: 1, to: arguments.count, by: 2).map {
Array(arguments[$0 ..< min($0 + 2, arguments.count)])
}
let possibleValues = ["--path", "-p", "--template", "-t"]
let filteredArguments = pairedArguments
.filter {
$0.first == "--path" || $0.first == "--template"
possibleValues.contains($0.first ?? "")
}
.flatMap { Array($0) }
// We want to parse only the name of template, not its arguments which will be dynamically added
let resultArguments = try parser.parse(Array(arguments.prefix(1)) + filteredArguments)
.flatMap { $0 }
guard let templateName = resultArguments.get(templateArgument) else { throw InitCommandError.templateNotProvided }
let path = self.path(arguments: resultArguments)
let directories = try templatesDirectoryLocator.templateDirectories(at: path)
let templateDirectory = try self.templateDirectory(templateDirectories: directories,
template: templateName)
let template = try templateLoader.loadTemplate(at: templateDirectory)
// Dynamically add attributes from template to `subParser`
attributesArguments = template.attributes.reduce([:]) {
var mutableDictionary = $0
mutableDictionary[$1.name] = subParser.add(option: "--\($1.name)",
kind: String.self)
return mutableDictionary
}
return try parser.parse(arguments)
}
func run(with arguments: ArgumentParser.Result) throws {
let platform = try self.platform(arguments: arguments)
let path = self.path(arguments: arguments)
let name = try self.name(arguments: arguments, path: path)
try verifyDirectoryIsEmpty(path: path)
let directories = try templatesDirectoryLocator.templateDirectories(at: path)
if let template = arguments.get(templateArgument) {
guard
let templateDirectory = directories.first(where: { $0.basename == template })
else { throw InitCommandError.templateNotFound(template) }
let template = try templateLoader.loadTemplate(at: templateDirectory)
let parsedAttributes = try validateAttributes(attributesArguments,
template: template,
name: name,
platform: platform,
arguments: arguments)
try templateGenerator.generate(template: template,
to: path,
attributes: parsedAttributes)
} else {
guard
let templateDirectory = directories.first(where: { $0.basename == "default" })
else { throw InitCommandError.templateNotFound("default") }
let template = try templateLoader.loadTemplate(at: templateDirectory)
try templateGenerator.generate(template: template,
to: path,
attributes: ["name": name, "platform": platform.caseValue])
}
logger.notice("Project generated at path \(path.pathString).", metadata: .success)
}
// MARK: - Fileprivate
/// Checks if the given directory is empty, essentially that it doesn't contain any file or directory.
///
/// - Parameter path: Directory to be checked.
/// - Throws: An InitCommandError.nonEmptyDirectory error when the directory is not empty.
private func verifyDirectoryIsEmpty(path: AbsolutePath) throws {
if !path.glob("*").isEmpty {
throw InitCommandError.nonEmptyDirectory(path)
}
}
/// Validates if all `attributes` from `template` have been provided
/// If those attributes are optional, they default to `default` if not provided
/// - Returns: Array of parsed attributes
private func validateAttributes(_ attributes: [String: OptionArgument<String>],
template: Template,
name: String,
platform: Platform,
arguments: ArgumentParser.Result) throws -> [String: String] {
try template.attributes.reduce(into: [:]) { attributesDict, attribute in
if attribute.name == "name" {
attributesDict[attribute.name] = name
return
}
if attribute.name == "platform" {
attributesDict[attribute.name] = platform.caseValue
return
}
switch attribute {
case let .required(name):
guard
let argument = attributes[name],
let value = arguments.get(argument)
else { throw InitCommandError.attributeNotProvided(name) }
attributesDict[name] = value
case let .optional(name, default: defaultValue):
guard
let argument = attributes[name],
let value: String = arguments.get(argument)
else {
attributesDict[name] = defaultValue
return
}
attributesDict[name] = value
}
}
}
/// Finds template directory
/// - Parameters:
/// - templateDirectories: Paths of available templates
/// - template: Name of template
/// - Returns: `AbsolutePath` of template directory
private func templateDirectory(templateDirectories: [AbsolutePath], template: String) throws -> AbsolutePath {
guard
let templateDirectory = templateDirectories.first(where: { $0.basename == template })
else { throw InitCommandError.templateNotFound(template) }
return templateDirectory
}
let command = try parseAsRoot(filteredArguments) as? InitCommand,
let templateName = command.template,
templateName != "default"
else { return }
private func name(arguments: ArgumentParser.Result, path: AbsolutePath) throws -> String {
if let name = arguments.get(nameArgument) {
return name
} else if let name = path.components.last {
return name
} else {
throw InitCommandError.ungettableProjectName(AbsolutePath.current)
let (required, optional) = try InitService().loadTemplateOptions(templateName: templateName,
path: command.path)
InitCommand.requiredTemplateOptions = required.map {
(name: $0, option: Option<String>(name: .shortAndLong))
}
}
private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
if let path = arguments.get(pathArgument) {
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
} else {
return FileHandler.shared.currentPath
}
}
private func platform(arguments: ArgumentParser.Result) throws -> Platform {
if let platformString = arguments.get(platformArgument) {
if let platform = Platform(rawValue: platformString) {
return platform
} else {
throw ArgumentParserError.invalidValue(argument: "platform", error: .custom("Platform should be either ios, tvos, or macos"))
}
} else {
return .iOS
InitCommand.optionalTemplateOptions = optional.map {
(name: $0, option: Option<String?>(name: .shortAndLong))
}
}
}
// MARK: - InitCommand.CodingKeys
extension InitCommand {
enum CodingKeys: CodingKey {
case platform
case name
case template
case path
case required(String)
case optional(String)
var stringValue: String {
switch self {
case .platform:
return "platform"
case .name:
return "name"
case .template:
return "template"
case .path:
return "path"
case let .required(required):
return required
case let .optional(optional):
return optional
}
}
// Not used
var intValue: Int? { nil }
init?(intValue _: Int) { nil }
init?(stringValue _: String) { nil }
}
}
/// ArgumentParser library gets the list of options from a mirror
/// Since we do not declare template's options in advance, we need to rewrite the mirror implementation and add them ourselves
extension InitCommand: CustomReflectable {
var customMirror: Mirror {
let requiredTemplateChildren = InitCommand.requiredTemplateOptions
.map { Mirror.Child(label: $0.name, value: $0.option) }
let optionalTemplateChildren = InitCommand.optionalTemplateOptions
.map { Mirror.Child(label: $0.name, value: $0.option) }
let children = [
Mirror.Child(label: "platform", value: _platform),
Mirror.Child(label: "name", value: _name),
Mirror.Child(label: "template", value: _template),
Mirror.Child(label: "path", value: _path),
]
return Mirror(InitCommand(), children: children + requiredTemplateChildren + optionalTemplateChildren)
}
}

View File

@ -1,115 +1,21 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistCore
import TuistGenerator
import TuistLoader
import TuistSupport
enum LintCommandError: FatalError, Equatable {
/// Thrown when neither a workspace or a project is found in the given path.
case manifestNotFound(AbsolutePath)
/// Error type.
var type: ErrorType {
switch self {
case .manifestNotFound:
return .abort
}
}
/// Description
var description: String {
switch self {
case let .manifestNotFound(path):
return "Couldn't find Project.swift nor Workspace.swift at \(path.pathString)"
}
}
}
/// Command that builds a target from the project in the current directory.
class LintCommand: NSObject, Command {
/// Command name.
static var command: String = "lint"
/// Command description.
static var overview: String = "Lints a workspace or a project that check whether they are well configured."
/// Graph linter
private let graphLinter: GraphLinting
private let environmentLinter: EnvironmentLinting
private let manifestLoading: ManifestLoading
private let graphLoader: GraphLoading
let pathArgument: OptionArgument<String>
/// Default constructor.
public required convenience init(parser: ArgumentParser) {
let manifestLoader = ManifestLoader()
let generatorModelLoader = GeneratorModelLoader(manifestLoader: manifestLoader,
manifestLinter: AnyManifestLinter())
self.init(graphLinter: GraphLinter(),
environmentLinter: EnvironmentLinter(),
manifestLoading: manifestLoader,
graphLoader: GraphLoader(modelLoader: generatorModelLoader),
parser: parser)
struct LintCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "lint",
abstract: "Lints a workspace or a project that check whether they are well configured")
}
init(graphLinter: GraphLinting,
environmentLinter: EnvironmentLinting,
manifestLoading: ManifestLoading,
graphLoader: GraphLoading,
parser: ArgumentParser) {
let subParser = parser.add(subparser: LintCommand.command, overview: LintCommand.overview)
self.graphLinter = graphLinter
self.environmentLinter = environmentLinter
self.manifestLoading = manifestLoading
self.graphLoader = graphLoader
pathArgument = subParser.add(option: "--path",
shortName: "-p",
kind: String.self,
usage: "The path to the directory that contains the workspace or project to be linted",
completion: .filename)
}
@Option(
name: .shortAndLong,
help: "The path to the directory that contains the workspace or project to be linted"
)
var path: String?
func run(with arguments: ArgumentParser.Result) throws {
let path = self.path(arguments: arguments)
// Load graph
let manifests = manifestLoading.manifests(at: path)
var graph: Graph!
logger.notice("Loading the dependency graph")
if manifests.contains(.workspace) {
logger.notice("Loading workspace at \(path.pathString)")
(graph, _) = try graphLoader.loadWorkspace(path: path)
} else if manifests.contains(.project) {
logger.notice("Loading project at \(path.pathString)")
(graph, _) = try graphLoader.loadProject(path: path)
} else {
throw LintCommandError.manifestNotFound(path)
}
logger.notice("Running linters")
let config = try graphLoader.loadConfig(path: path)
var issues: [LintingIssue] = []
logger.notice("Linting the environment")
issues.append(contentsOf: try environmentLinter.lint(config: config))
logger.notice("Linting the loaded dependency graph")
issues.append(contentsOf: graphLinter.lint(graph: graph))
if issues.isEmpty {
logger.notice("No linting issues found", metadata: .success)
} else {
try issues.printAndThrowIfNeeded()
}
}
private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
if let path = arguments.get(pathArgument) {
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
} else {
return FileHandler.shared.currentPath
}
func run() throws {
try LintService().run(path: path)
}
}

View File

@ -0,0 +1,20 @@
import ArgumentParser
import Foundation
struct ListCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "list",
abstract: "Lists available scaffold templates",
subcommands: [])
}
@Option(
name: .shortAndLong,
help: "The path where you want to list templates from"
)
var path: String?
func run() throws {
try ListService().run(path: path)
}
}

View File

@ -1,202 +1,157 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistCore
import TuistLoader
import TuistScaffold
import TuistSupport
enum ScaffoldCommandError: FatalError, Equatable {
var type: ErrorType { .abort }
var type: ErrorType {
switch self {
case .templateNotProvided:
return .abort
}
}
case templateNotFound(String)
case templateNotProvided
case nonEmptyDirectory(AbsolutePath)
case attributeNotProvided(String)
var description: String {
switch self {
case let .templateNotFound(template):
return "Could not find template \(template). Make sure it exists at Tuist/Templates/\(template)"
case .templateNotProvided:
return "You must provide template name"
case let .nonEmptyDirectory(path):
return "Can't generate a template in the non-empty directory at path \(path.pathString)."
case let .attributeNotProvided(name):
return "You must provide \(name) option. Add --\(name) desired_value to your command."
}
}
}
class ScaffoldCommand: NSObject, Command {
// MARK: - Attributes
static let command = "scaffold"
static let overview = "Generates new project based on template."
private let listArgument: OptionArgument<Bool>
private let pathArgument: OptionArgument<String>
private let templateArgument: PositionalArgument<String>
private var attributesArguments: [String: OptionArgument<String>] = [:]
private let subParser: ArgumentParser
private let templateLoader: TemplateLoading
private let templatesDirectoryLocator: TemplatesDirectoryLocating
private let templateGenerator: TemplateGenerating
// MARK: - Init
public required convenience init(parser: ArgumentParser) {
self.init(parser: parser,
templateLoader: TemplateLoader(),
templatesDirectoryLocator: TemplatesDirectoryLocator(),
templateGenerator: TemplateGenerator())
struct ScaffoldCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "scaffold",
abstract: "Generates new project based on template",
subcommands: [ListCommand.self])
}
init(parser: ArgumentParser,
templateLoader: TemplateLoading,
templatesDirectoryLocator: TemplatesDirectoryLocating,
templateGenerator: TemplateGenerating) {
subParser = parser.add(subparser: ScaffoldCommand.command, overview: ScaffoldCommand.overview)
listArgument = subParser.add(option: "--list",
shortName: "-l",
kind: Bool.self,
usage: "Lists available scaffold templates",
completion: nil)
templateArgument = subParser.add(positional: "template",
kind: String.self,
optional: true,
usage: "Name of template you want to use",
completion: nil)
pathArgument = subParser.add(option: "--path",
shortName: "-p",
kind: String.self,
usage: "The path to the folder where the template will be generated (Default: Current directory).",
completion: .filename)
self.templateLoader = templateLoader
self.templatesDirectoryLocator = templatesDirectoryLocator
self.templateGenerator = templateGenerator
@Option(
name: .shortAndLong,
help: "The path to the folder where the template will be generated (Default: Current directory)"
)
var path: String?
@Argument(
help: "Name of template you want to use"
)
var template: String
var requiredTemplateOptions: [String: String] = [:]
var optionalTemplateOptions: [String: String?] = [:]
init() {}
// Custom decoding to decode dynamic options
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
template = try container.decode(Argument<String>.self, forKey: .template).wrappedValue
path = try container.decodeIfPresent(Option<String>.self, forKey: .path)?.wrappedValue
try ScaffoldCommand.requiredTemplateOptions.forEach { option in
requiredTemplateOptions[option.name] = try container.decode(Option<String>.self,
forKey: .required(option.name)).wrappedValue
}
try ScaffoldCommand.optionalTemplateOptions.forEach { option in
optionalTemplateOptions[option.name] = try container.decode(Option<String?>.self,
forKey: .optional(option.name)).wrappedValue
}
}
func parse(with parser: ArgumentParser, arguments: [String]) throws -> (ArgumentParser.Result, ArgumentParser) {
guard arguments.count >= 2 else { throw ScaffoldCommandError.templateNotProvided }
// We want to parse only the name of template, not its arguments which will be dynamically added
let templateArguments = Array(arguments.prefix(2))
// Plucking out path argument
let filteredArguments = stride(from: 2, to: arguments.count, by: 2).map {
arguments[$0 ..< min($0 + 2, arguments.count)]
}
.filter {
$0.first == "--path"
}
.flatMap { Array($0) }
// We want to parse only the name of template, not its arguments which will be dynamically added
let resultArguments = try parser.parse(templateArguments + filteredArguments)
if resultArguments.get(listArgument) != nil {
return (try parser.parse(arguments), parser)
}
guard let templateName = resultArguments.get(templateArgument) else { throw ScaffoldCommandError.templateNotProvided }
let path = self.path(arguments: resultArguments)
let directories = try templatesDirectoryLocator.templateDirectories(at: path)
let templateDirectory = try self.templateDirectory(templateDirectories: directories,
template: templateName)
let template = try templateLoader.loadTemplate(at: templateDirectory)
// Dynamically add attributes from template to `subParser`
attributesArguments = template.attributes.reduce([:]) {
var mutableDictionary = $0
mutableDictionary[$1.name] = subParser.add(option: "--\($1.name)",
kind: String.self)
return mutableDictionary
}
return (try parser.parse(arguments), parser)
}
func run(with arguments: ArgumentParser.Result) throws {
let path = self.path(arguments: arguments)
let templateDirectories = try templatesDirectoryLocator.templateDirectories(at: path)
let shouldList = arguments.get(listArgument) ?? false
if shouldList {
try templateDirectories.forEach {
let template = try templateLoader.loadTemplate(at: $0)
logger.info("\($0.basename): \(template.description)")
}
return
}
guard let templateName = arguments.get(templateArgument) else { throw ScaffoldCommandError.templateNotProvided }
let templateDirectory = try self.templateDirectory(templateDirectories: templateDirectories,
template: templateName)
let template = try templateLoader.loadTemplate(at: templateDirectory)
let parsedAttributes = try validateAttributes(attributesArguments,
template: template,
arguments: arguments)
try templateGenerator.generate(template: template,
to: path,
attributes: parsedAttributes)
logger.notice("Template \(templateName) was successfully generated", metadata: .success)
}
// MARK: - Helpers
private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
if let path = arguments.get(pathArgument) {
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
func run() throws {
// Currently, @Argument and subcommand clashes, so we need to handle that ourselves
if template == ListCommand.configuration.commandName {
try ListService().run(path: path)
} else {
return FileHandler.shared.currentPath
try ScaffoldService().run(path: path,
templateName: template,
requiredTemplateOptions: requiredTemplateOptions,
optionalTemplateOptions: optionalTemplateOptions)
}
}
/// Validates if all `attributes` from `template` have been provided
/// If those attributes are optional, they default to `default` if not provided
/// - Returns: Array of parsed attributes
private func validateAttributes(_ attributes: [String: OptionArgument<String>],
template: Template,
arguments: ArgumentParser.Result) throws -> [String: String] {
try template.attributes.reduce([:]) {
var mutableDict = $0
switch $1 {
case let .required(name):
guard
let argument = attributes[name],
let value = arguments.get(argument)
else { throw ScaffoldCommandError.attributeNotProvided(name) }
mutableDict[name] = value
case let .optional(name, default: defaultValue):
guard
let argument = attributes[name],
let value: String = arguments.get(argument)
else {
mutableDict[name] = defaultValue
return mutableDict
}
mutableDict[name] = value
}
return mutableDict
}
}
/// Finds template directory
/// - Parameters:
/// - templateDirectories: Paths of available templates
/// - template: Name of template
/// - Returns: `AbsolutePath` of template directory
private func templateDirectory(templateDirectories: [AbsolutePath], template: String) throws -> AbsolutePath {
guard
let templateDirectory = templateDirectories.first(where: { $0.basename == template })
else { throw ScaffoldCommandError.templateNotFound(template) }
return templateDirectory
}
}
// MARK: - Preprocessing
extension ScaffoldCommand {
static var requiredTemplateOptions: [(name: String, option: Option<String>)] = []
static var optionalTemplateOptions: [(name: String, option: Option<String?>)] = []
/// We do not know template's option in advance -> we need to dynamically add them
static func preprocess(_ arguments: [String]? = nil) throws {
guard
let arguments = arguments,
arguments.count >= 2
else { throw ScaffoldCommandError.templateNotProvided }
guard !configuration.subcommands.contains(where: { $0.configuration.commandName == arguments[1] }) else { return }
// We want to parse only the name of template, not its arguments which will be dynamically added
// Plucking out path argument
let pairedArguments: [[String]] = stride(from: 2, to: arguments.count, by: 2).map {
Array(arguments[$0 ..< min($0 + 2, arguments.count)])
}
let filteredArguments = pairedArguments
.filter {
$0.first == "--path" || $0.first == "-p"
}
.flatMap { $0 }
guard let command = try parseAsRoot([arguments[1]] + filteredArguments) as? ScaffoldCommand else { return }
let (required, optional) = try ScaffoldService().loadTemplateOptions(templateName: command.template,
path: command.path)
ScaffoldCommand.requiredTemplateOptions = required.map {
(name: $0, option: Option<String>(name: .shortAndLong))
}
ScaffoldCommand.optionalTemplateOptions = optional.map {
(name: $0, option: Option<String?>(name: .shortAndLong))
}
}
}
// MARK: - ScaffoldCommand.CodingKeys
extension ScaffoldCommand {
enum CodingKeys: CodingKey {
case template
case path
case required(String)
case optional(String)
var stringValue: String {
switch self {
case .template:
return "template"
case .path:
return "path"
case let .required(required):
return required
case let .optional(optional):
return optional
}
}
// Not used
var intValue: Int? { nil }
init?(intValue _: Int) { nil }
init?(stringValue _: String) { nil }
}
}
/// ArgumentParser library gets the list of options from a mirror
/// Since we do not declare template's options in advance, we need to rewrite the mirror implementation and add them ourselves
extension ScaffoldCommand: CustomReflectable {
var customMirror: Mirror {
let requiredTemplateChildren = ScaffoldCommand.requiredTemplateOptions
.map { Mirror.Child(label: $0.name, value: $0.option) }
let optionalTemplateChildren = ScaffoldCommand.optionalTemplateOptions
.map { Mirror.Child(label: $0.name, value: $0.option) }
let children = [
Mirror.Child(label: "template", value: _template),
Mirror.Child(label: "path", value: _path),
]
return Mirror(ScaffoldCommand(), children: children + requiredTemplateChildren + optionalTemplateChildren)
}
}

View File

@ -1,49 +1,23 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistCore
import TuistSigning
import TuistSupport
class DecryptCommand: NSObject, Command {
// MARK: - Attributes
static let command = "decrypt"
static let overview = "Decrypts all files in Tuist/Signing directory."
private let pathArgument: OptionArgument<String>
private let signingCipher: SigningCiphering
// MARK: - Init
public required convenience init(parser: ArgumentParser) {
self.init(parser: parser, signingCipher: SigningCipher())
struct DecryptCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "decrypt",
abstract: "Decrypts all files in Tuist/Signing directory")
}
init(parser: ArgumentParser,
signingCipher: SigningCiphering) {
let subParser = parser.add(subparser: DecryptCommand.command, overview: DecryptCommand.overview)
pathArgument = subParser.add(option: "--path",
shortName: "-p",
kind: String.self,
usage: "The path to the folder containing the encrypted certificates",
completion: .filename)
self.signingCipher = signingCipher
}
@Option(
name: .shortAndLong,
help: "The path to the folder containing the encrypted certificates"
)
var path: String?
func run(with arguments: ArgumentParser.Result) throws {
let path = self.path(arguments: arguments)
try signingCipher.decryptSigning(at: path)
logger.notice("Successfully decrypted all signing files", metadata: .success)
}
// MARK: - Helpers
private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
if let path = arguments.get(pathArgument) {
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
} else {
return FileHandler.shared.currentPath
}
func run() throws {
try DecryptService().run(path: path)
}
}

View File

@ -1,50 +1,20 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistCore
import TuistSigning
import TuistSupport
class EncryptCommand: NSObject, Command {
// MARK: - Attributes
static let command = "encrypt"
static let overview = "Encrypts all files in Tuist/Signing directory."
private let pathArgument: OptionArgument<String>
private let signingCipher: SigningCiphering
// MARK: - Init
public required convenience init(parser: ArgumentParser) {
self.init(parser: parser, signingCipher: SigningCipher())
struct EncryptCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "encrypt",
abstract: "Encrypts all files in Tuist/Signing directory")
}
init(parser: ArgumentParser,
signingCipher: SigningCiphering) {
let subParser = parser.add(subparser: EncryptCommand.command, overview: EncryptCommand.overview)
pathArgument = subParser.add(option: "--path",
shortName: "-p",
kind: String.self,
usage: "The path to the folder containing the certificates you would like to encrypt",
completion: .filename)
self.signingCipher = signingCipher
}
@Option(
name: .shortAndLong,
help: "The path to the folder containing the certificates you would like to encrypt"
)
var path: String?
func run(with arguments: ArgumentParser.Result) throws {
let path = self.path(arguments: arguments)
try signingCipher.encryptSigning(at: path)
logger.notice("Successfully encrypted all signing files", metadata: .success)
}
// MARK: - Helpers
private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
if let path = arguments.get(pathArgument) {
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
} else {
return FileHandler.shared.currentPath
}
func run() throws {
try EncryptService().run(path: path)
}
}

View File

@ -1,34 +1,14 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistCore
import TuistSigning
import TuistSupport
class SigningCommand: NSObject, Command {
// MARK: - Attributes
static let command = "signing"
static let overview = "A set of commands for signing-related operations. "
let subcommands: [Command]
private let argumentParser: ArgumentParser
// MARK: - Init
public required init(parser: ArgumentParser) {
_ = parser.add(subparser: SigningCommand.command, overview: SigningCommand.overview)
let argumentParser = ArgumentParser(commandName: Self.command, usage: "tuist signing <command> <options>", overview: Self.overview)
let subcommands: [Command.Type] = [EncryptCommand.self, DecryptCommand.self]
self.subcommands = subcommands.map { $0.init(parser: argumentParser) }
self.argumentParser = argumentParser
}
func parse(with _: ArgumentParser, arguments: [String]) throws -> (ArgumentParser.Result, ArgumentParser) {
return (try argumentParser.parse(Array(arguments.dropFirst())), argumentParser)
}
func run(with _: ArgumentParser.Result) throws {
argumentParser.printUsage(on: stdoutStream)
struct SigningCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "signing",
abstract: "A set of commands for signing-related operations",
subcommands: [
EncryptCommand.self,
DecryptCommand.self,
])
}
}

View File

@ -0,0 +1,66 @@
import ArgumentParser
import Foundation
import TuistSupport
public struct TuistCommand: ParsableCommand {
public init() {}
public static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "tuist",
abstract: "Generate, build and test your Xcode projects.",
subcommands: [
GenerateCommand.self,
UpCommand.self,
FocusCommand.self,
EditCommand.self,
DumpCommand.self,
GraphCommand.self,
LintCommand.self,
VersionCommand.self,
BuildCommand.self,
CacheCommand.self,
CreateIssueCommand.self,
ScaffoldCommand.self,
InitCommand.self,
CloudCommand.self,
SigningCommand.self,
])
}
public static func main(_ arguments: [String]? = nil) -> Never {
let errorHandler = ErrorHandler()
let command: ParsableCommand
do {
let processedArguments = Array(processArguments(arguments)?.dropFirst() ?? [])
if processedArguments.first == ScaffoldCommand.configuration.commandName {
try ScaffoldCommand.preprocess(processedArguments)
}
if processedArguments.first == InitCommand.configuration.commandName {
try InitCommand.preprocess(processedArguments)
}
command = try parseAsRoot(processedArguments)
} catch {
logger.error("\(fullMessage(for: error))")
_exit(exitCode(for: error).rawValue)
}
do {
try command.run()
exit()
} catch let error as CleanExit {
_exit(exitCode(for: error).rawValue)
} catch let error as FatalError {
errorHandler.fatal(error: error)
_exit(exitCode(for: error).rawValue)
} catch {
errorHandler.fatal(error: UnhandledError(error: error))
_exit(exitCode(for: error).rawValue)
}
}
// MARK: - Helpers
static func processArguments(_ arguments: [String]? = nil) -> [String]? {
let arguments = arguments ?? Array(ProcessInfo.processInfo.arguments)
return arguments.filter { $0 != "--verbose" }
}
}

View File

@ -1,67 +1,23 @@
import Basic
import ArgumentParser
import Foundation
import SPMUtility
import TuistLoader
import TuistSupport
/// Command that configures the environment to work on the project.
class UpCommand: NSObject, Command {
// MARK: - Attributes
/// Name of the command.
static let command = "up"
/// Description of the command.
static let overview = "Configures the environment for the project."
/// Path to the project directory.
let pathArgument: OptionArgument<String>
/// Instance to load the setup manifest and perform the project setup.
private let setupLoader: SetupLoading
// MARK: - Init
/// Initializes the command with the CLI parser.
///
/// - Parameter parser: CLI parser where the command should register itself.
public required convenience init(parser: ArgumentParser) {
self.init(parser: parser,
setupLoader: SetupLoader())
struct UpCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(
commandName: "up",
abstract: "Configures the environment for the project.",
subcommands: []
)
}
/// Initializes the command with its arguments.
///
/// - Parameters:
/// - parser: CLI parser where the command should register itself.
/// - setupLoader: Instance to load the setup manifest and perform the project setup.
init(parser: ArgumentParser,
setupLoader: SetupLoading) {
let subParser = parser.add(subparser: UpCommand.command, overview: UpCommand.overview)
pathArgument = subParser.add(option: "--path",
shortName: "-p",
kind: String.self,
usage: "The path to the directory that contains the project.",
completion: .filename)
self.setupLoader = setupLoader
}
@Option(
name: .shortAndLong,
help: "The path to the directory that contains the project."
)
var path: String?
/// Runs the command using the result from parsing the command line arguments.
///
/// - Throws: An error if the the configuration of the environment fails.
func run(with arguments: ArgumentParser.Result) throws {
try setupLoader.meet(at: path(arguments: arguments))
}
/// Parses the arguments and returns the path to the directory where
/// the up command should be ran.
///
/// - Parameter arguments: Result from parsing the command line arguments.
/// - Returns: Path to be used for the up command.
private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
guard let path = arguments.get(pathArgument) else {
return FileHandler.shared.currentPath
}
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
func run() throws {
try UpService().run(path: path)
}
}

View File

@ -1,23 +1,14 @@
import ArgumentParser
import Basic
import Foundation
import SPMUtility
import TuistSupport
class VersionCommand: NSObject, Command {
// MARK: - Command
static let command = "version"
static let overview = "Outputs the current version of tuist."
// MARK: - Init
required init(parser: ArgumentParser) {
parser.add(subparser: VersionCommand.command, overview: VersionCommand.overview)
struct VersionCommand: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(commandName: "version",
abstract: "Outputs the current version of tuist")
}
// MARK: - Command
func run(with _: ArgumentParser.Result) {
logger.notice("\(Constants.version)")
func run() throws {
try VersionService().run()
}
}

View File

@ -91,7 +91,7 @@ final class ProjectEditor: ProjectEditing {
}
// To be sure that we are using the same binary of Tuist that invoked `edit`
let tuistPath = AbsolutePath(CommandRegistry.processArguments().first!)
let tuistPath = AbsolutePath(TuistCommand.processArguments()!.first!)
let (project, graph) = projectEditorMapper.map(tuistPath: tuistPath,
sourceRootPath: at,

View File

@ -0,0 +1,20 @@
import Basic
import Foundation
import SPMUtility
import TuistSupport
enum BuildServiceError: FatalError {
// Error description
var description: String {
""
}
// Error type
var type: ErrorType { .abort }
}
final class BuildService {
func run() throws {
logger.notice("Command not available yet")
}
}

View File

@ -0,0 +1,27 @@
import Basic
import Foundation
import TuistSupport
final class CacheService {
/// Cache controller.
private let cacheController: CacheControlling
init(cacheController: CacheControlling = CacheController()) {
self.cacheController = cacheController
}
func run(path: String?) throws {
let path = self.path(path)
try cacheController.cache(path: path)
}
// MARK: - Helpers
private func path(_ path: String?) -> AbsolutePath {
if let path = path {
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
} else {
return FileHandler.shared.currentPath
}
}
}

View File

@ -0,0 +1,12 @@
import Basic
import Foundation
import SPMUtility
import TuistSupport
final class CreateIssueService {
static let createIssueUrl: String = "https://github.com/tuist/tuist/issues/new"
func run() throws {
try System.shared.run("/usr/bin/open", CreateIssueService.createIssueUrl)
}
}

View File

@ -0,0 +1,29 @@
import Basic
import Foundation
import TuistCore
import TuistSigning
import TuistSupport
final class DecryptService {
private let signingCipher: SigningCiphering
init(signingCipher: SigningCiphering = SigningCipher()) {
self.signingCipher = signingCipher
}
func run(path: String?) throws {
let path = self.path(path)
try signingCipher.decryptSigning(at: path)
logger.notice("Successfully decrypted all signing files", metadata: .success)
}
// MARK: - Helpers
private func path(_ path: String?) -> AbsolutePath {
if let path = path {
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
} else {
return FileHandler.shared.currentPath
}
}
}

View File

@ -0,0 +1,24 @@
import Basic
import Foundation
import TuistLoader
import TuistSupport
final class DumpService {
private let manifestLoader: ManifestLoading
init(manifestLoader: ManifestLoading = ManifestLoader()) {
self.manifestLoader = manifestLoader
}
func run(path: String?) throws {
let projectPath: AbsolutePath
if let path = path {
projectPath = AbsolutePath(path, relativeTo: AbsolutePath.current)
} else {
projectPath = AbsolutePath.current
}
let project = try manifestLoader.loadProject(at: projectPath)
let json: JSON = try project.toJSON()
logger.notice("\(json.toString(prettyPrint: true))")
}
}

View File

@ -0,0 +1,54 @@
import Basic
import Foundation
import Signals
import TuistGenerator
import TuistSupport
final class EditService {
private let projectEditor: ProjectEditing
private let opener: Opening
init(projectEditor: ProjectEditing = ProjectEditor(),
opener: Opening = Opener()) {
self.projectEditor = projectEditor
self.opener = opener
}
func run(path: String?,
permanent: Bool) throws {
let path = self.path(path)
let generationDirectory = permanent ? path : EditService.temporaryDirectory.path
let xcodeprojPath = try projectEditor.edit(at: path, in: generationDirectory)
if !permanent {
Signals.trap(signals: [.int, .abrt]) { _ in
// swiftlint:disable:next force_try
try! FileHandler.shared.delete(EditService.temporaryDirectory.path)
exit(0)
}
logger.pretty("Opening Xcode to edit the project. Press \(.keystroke("CTRL + C")) once you are done editing")
try opener.open(path: xcodeprojPath)
} else {
logger.notice("Xcode project generated at \(xcodeprojPath.pathString)", metadata: .success)
}
}
// MARK: - Helpers
private func path(_ path: String?) -> AbsolutePath {
if let path = path {
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
} else {
return FileHandler.shared.currentPath
}
}
private static var _temporaryDirectory: TemporaryDirectory?
private static var temporaryDirectory: TemporaryDirectory {
// swiftlint:disable:next identifier_name
if let _temporaryDirectory = _temporaryDirectory { return _temporaryDirectory }
// swiftlint:disable:next force_try
_temporaryDirectory = try! TemporaryDirectory(removeTreeOnDeinit: true)
return _temporaryDirectory!
}
}

View File

@ -0,0 +1,30 @@
import Basic
import Foundation
import TuistCore
import TuistSigning
import TuistSupport
final class EncryptService {
private let signingCipher: SigningCiphering
init(signingCipher: SigningCiphering = SigningCipher()) {
self.signingCipher = signingCipher
}
func run(path: String?) throws {
let path = self.path(path)
try signingCipher.encryptSigning(at: path)
logger.notice("Successfully encrypted all signing files", metadata: .success)
}
// MARK: - Helpers
private func path(_ path: String?) -> AbsolutePath {
if let path = path {
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
} else {
return FileHandler.shared.currentPath
}
}
}

View File

@ -0,0 +1,32 @@
import Basic
import Foundation
import RxBlocking
import RxSwift
import TuistCache
import TuistCore
import TuistGenerator
import TuistLoader
import TuistSupport
final class FocusService {
/// Generator instance to generate the project workspace.
private let generator: ProjectGenerating
/// Opener instance to run open in the system.
private let opener: Opening
init(generator: ProjectGenerating = ProjectGenerator(),
opener: Opening = Opener()) {
self.generator = generator
self.opener = opener
}
func run() throws {
let path = FileHandler.shared.currentPath
let workspacePath = try generator.generate(path: path,
projectOnly: false)
try opener.open(path: workspacePath)
}
}

View File

@ -0,0 +1,40 @@
import Basic
import TuistGenerator
import TuistLoader
import TuistSupport
final class GenerateService {
// MARK: - Attributes
private let clock: Clock
private let generator: ProjectGenerating
init(generator: ProjectGenerating = ProjectGenerator(),
clock: Clock = WallClock()) {
self.generator = generator
self.clock = clock
}
func run(path: String?,
projectOnly: Bool) throws {
let timer = clock.startTimer()
let path = self.path(path)
try generator.generate(path: path, projectOnly: projectOnly)
let time = String(format: "%.3f", timer.stop())
logger.notice("Project generated.", metadata: .success)
logger.notice("Total time taken: \(time)s")
}
// MARK: - Helpers
private func path(_ path: String?) -> AbsolutePath {
if let path = path {
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
} else {
return FileHandler.shared.currentPath
}
}
}

View File

@ -0,0 +1,34 @@
import Basic
import Foundation
import TuistGenerator
import TuistLoader
import TuistSupport
final class GraphService {
/// Dot graph generator.
private let dotGraphGenerator: DotGraphGenerating
/// Manifest loader.
private let manifestLoader: ManifestLoading
init(dotGraphGenerator: DotGraphGenerating = DotGraphGenerator(modelLoader: GeneratorModelLoader(manifestLoader: ManifestLoader(),
manifestLinter: ManifestLinter())),
manifestLoader: ManifestLoading = ManifestLoader()) {
self.dotGraphGenerator = dotGraphGenerator
self.manifestLoader = manifestLoader
}
func run() throws {
let graph = try dotGraphGenerator.generate(at: FileHandler.shared.currentPath,
manifestLoader: manifestLoader)
let path = FileHandler.shared.currentPath.appending(component: "graph.dot")
if FileHandler.shared.exists(path) {
logger.notice("Deleting existing graph at \(path.pathString)")
try FileHandler.shared.delete(path)
}
try FileHandler.shared.write(graph, path: path, atomically: true)
logger.notice("Graph exported to \(path.pathString)", metadata: .success)
}
}

View File

@ -0,0 +1,202 @@
import Basic
import TuistCore
import TuistLoader
import TuistScaffold
import TuistSupport
enum InitServiceError: FatalError, Equatable {
case ungettableProjectName(AbsolutePath)
case nonEmptyDirectory(AbsolutePath)
case templateNotFound(String)
case templateNotProvided
case attributeNotProvided(String)
case invalidValue(argument: String, error: String)
var type: ErrorType {
switch self {
case .ungettableProjectName, .nonEmptyDirectory, .templateNotFound, .templateNotProvided, .attributeNotProvided, .invalidValue:
return .abort
}
}
var description: String {
switch self {
case let .templateNotFound(template):
return "Could not find template \(template). Make sure it exists at Tuist/Templates/\(template)"
case .templateNotProvided:
return "You must provide template name"
case let .ungettableProjectName(path):
return "Couldn't infer the project name from path \(path.pathString)."
case let .nonEmptyDirectory(path):
return "Can't initialize a project in the non-empty directory at path \(path.pathString)."
case let .attributeNotProvided(name):
return "You must provide \(name) option. Add --\(name) desired_value to your command."
case let .invalidValue(argument: argument, error: error):
return "\(error) for argument \(argument); use --help to print usage"
}
}
}
class InitService {
private let templateLoader: TemplateLoading
private let templatesDirectoryLocator: TemplatesDirectoryLocating
private let templateGenerator: TemplateGenerating
init(templateLoader: TemplateLoading = TemplateLoader(),
templatesDirectoryLocator: TemplatesDirectoryLocating = TemplatesDirectoryLocator(),
templateGenerator: TemplateGenerating = TemplateGenerator()) {
self.templateLoader = templateLoader
self.templatesDirectoryLocator = templatesDirectoryLocator
self.templateGenerator = templateGenerator
}
func loadTemplateOptions(templateName: String,
path: String?) throws -> (required: [String],
optional: [String]) {
let path = self.path(path)
let directories = try templatesDirectoryLocator.templateDirectories(at: path)
let templateDirectory = try self.templateDirectory(templateDirectories: directories,
template: templateName)
let template = try templateLoader.loadTemplate(at: templateDirectory)
return template.attributes.reduce(into: (required: [], optional: [])) { currentValue, attribute in
switch attribute {
case let .optional(name, default: _):
currentValue.optional.append(name)
case let .required(name):
currentValue.required.append(name)
}
}
}
func run(name: String?,
platform: String?,
path: String?,
templateName: String?,
requiredTemplateOptions: [String: String],
optionalTemplateOptions: [String: String?]) throws {
let platform = try self.platform(platform)
let path = self.path(path)
let name = try self.name(name, path: path)
try verifyDirectoryIsEmpty(path: path)
let directories = try templatesDirectoryLocator.templateDirectories(at: path)
if let templateName = templateName {
guard
let templateDirectory = directories.first(where: { $0.basename == templateName })
else { throw InitServiceError.templateNotFound(templateName) }
let template = try templateLoader.loadTemplate(at: templateDirectory)
let parsedAttributes = try parseAttributes(name: name,
platform: platform,
requiredTemplateOptions: requiredTemplateOptions,
optionalTemplateOptions: optionalTemplateOptions,
template: template)
try templateGenerator.generate(template: template,
to: path,
attributes: parsedAttributes)
} else {
guard
let templateDirectory = directories.first(where: { $0.basename == "default" })
else { throw InitServiceError.templateNotFound("default") }
let template = try templateLoader.loadTemplate(at: templateDirectory)
try templateGenerator.generate(template: template,
to: path,
attributes: ["name": name, "platform": platform.caseValue])
}
logger.notice("Project generated at path \(path.pathString).", metadata: .success)
}
// MARK: - Helpers
/// Checks if the given directory is empty, essentially that it doesn't contain any file or directory.
///
/// - Parameter path: Directory to be checked.
/// - Throws: An InitServiceError.nonEmptyDirectory error when the directory is not empty.
private func verifyDirectoryIsEmpty(path: AbsolutePath) throws {
if !path.glob("*").isEmpty {
throw InitServiceError.nonEmptyDirectory(path)
}
}
/// Parses all `attributes` from `template`
/// If those attributes are optional, they default to `default` if not provided
/// - Returns: Array of parsed attributes
private func parseAttributes(name: String,
platform: Platform,
requiredTemplateOptions: [String: String],
optionalTemplateOptions: [String: String?],
template: Template) throws -> [String: String] {
try template.attributes.reduce(into: [:]) { attributesDictionary, attribute in
if attribute.name == "name" {
attributesDictionary[attribute.name] = name
return
}
if attribute.name == "platform" {
attributesDictionary[attribute.name] = platform.caseValue
return
}
switch attribute {
case let .required(name):
guard
let option = requiredTemplateOptions[name]
else { throw ScaffoldServiceError.attributeNotProvided(name) }
attributesDictionary[name] = option
case let .optional(name, default: defaultValue):
guard
let unwrappedOption = optionalTemplateOptions[name],
let option = unwrappedOption
else {
attributesDictionary[name] = defaultValue
return
}
attributesDictionary[name] = option
}
}
}
/// Finds template directory
/// - Parameters:
/// - templateDirectories: Paths of available templates
/// - template: Name of template
/// - Returns: `AbsolutePath` of template directory
private func templateDirectory(templateDirectories: [AbsolutePath], template: String) throws -> AbsolutePath {
guard
let templateDirectory = templateDirectories.first(where: { $0.basename == template })
else { throw InitServiceError.templateNotFound(template) }
return templateDirectory
}
private func name(_ name: String?, path: AbsolutePath) throws -> String {
if let name = name {
return name
} else if let name = path.components.last {
return name
} else {
throw InitServiceError.ungettableProjectName(AbsolutePath.current)
}
}
private func path(_ path: String?) -> AbsolutePath {
if let path = path {
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
} else {
return FileHandler.shared.currentPath
}
}
private func platform(_ platform: String?) throws -> Platform {
if let platformString = platform {
if let platform = Platform(rawValue: platformString) {
return platform
} else {
throw InitServiceError.invalidValue(argument: "platform", error: "Platform should be either ios, tvos, or macos")
}
} else {
return .iOS
}
}
}

View File

@ -0,0 +1,90 @@
import Basic
import Foundation
import TuistCore
import TuistGenerator
import TuistLoader
import TuistSupport
enum LintServiceError: FatalError, Equatable {
/// Thrown when neither a workspace or a project is found in the given path.
case manifestNotFound(AbsolutePath)
/// Error type.
var type: ErrorType {
switch self {
case .manifestNotFound:
return .abort
}
}
/// Description
var description: String {
switch self {
case let .manifestNotFound(path):
return "Couldn't find Project.swift nor Workspace.swift at \(path.pathString)"
}
}
}
final class LintService {
/// Graph linter
private let graphLinter: GraphLinting
private let environmentLinter: EnvironmentLinting
private let manifestLoading: ManifestLoading
private let graphLoader: GraphLoading
init(graphLinter: GraphLinting = GraphLinter(),
environmentLinter: EnvironmentLinting = EnvironmentLinter(),
manifestLoading: ManifestLoading = ManifestLoader(),
graphLoader: GraphLoading = GraphLoader(modelLoader: GeneratorModelLoader(manifestLoader: ManifestLoader(),
manifestLinter: AnyManifestLinter()))) {
self.graphLinter = graphLinter
self.environmentLinter = environmentLinter
self.manifestLoading = manifestLoading
self.graphLoader = graphLoader
}
func run(path: String?) throws {
let path = self.path(path)
// Load graph
let manifests = manifestLoading.manifests(at: path)
var graph: Graph!
logger.notice("Loading the dependency graph")
if manifests.contains(.workspace) {
logger.notice("Loading workspace at \(path.pathString)")
(graph, _) = try graphLoader.loadWorkspace(path: path)
} else if manifests.contains(.project) {
logger.notice("Loading project at \(path.pathString)")
(graph, _) = try graphLoader.loadProject(path: path)
} else {
throw LintServiceError.manifestNotFound(path)
}
logger.notice("Running linters")
let config = try graphLoader.loadConfig(path: path)
var issues: [LintingIssue] = []
logger.notice("Linting the environment")
issues.append(contentsOf: try environmentLinter.lint(config: config))
logger.notice("Linting the loaded dependency graph")
issues.append(contentsOf: graphLinter.lint(graph: graph))
if issues.isEmpty {
logger.notice("No linting issues found", metadata: .success)
} else {
try issues.printAndThrowIfNeeded()
}
}
// MARK: - Helpers
private func path(_ path: String?) -> AbsolutePath {
if let path = path {
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
} else {
return FileHandler.shared.currentPath
}
}
}

View File

@ -0,0 +1,37 @@
import Basic
import Foundation
import TuistLoader
import TuistScaffold
import TuistSupport
class ListService {
private let templatesDirectoryLocator: TemplatesDirectoryLocating
private let templateLoader: TemplateLoading
init(templatesDirectoryLocator: TemplatesDirectoryLocating = TemplatesDirectoryLocator(),
templateLoader: TemplateLoading = TemplateLoader()) {
self.templatesDirectoryLocator = templatesDirectoryLocator
self.templateLoader = templateLoader
}
func run(path: String?) throws {
let path = self.path(path)
let templateDirectories = try templatesDirectoryLocator.templateDirectories(at: path)
try templateDirectories.forEach {
let template = try templateLoader.loadTemplate(at: $0)
logger.info("\($0.basename): \(template.description)")
}
}
// MARK: - Helpers
private func path(_ path: String?) -> AbsolutePath {
if let path = path {
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
} else {
return FileHandler.shared.currentPath
}
}
}

View File

@ -0,0 +1,136 @@
import Basic
import TuistCore
import TuistLoader
import TuistScaffold
import TuistSupport
enum ScaffoldServiceError: FatalError, Equatable {
var type: ErrorType {
switch self {
case .templateNotFound, .nonEmptyDirectory, .attributeNotProvided:
return .abort
}
}
case templateNotFound(String)
case nonEmptyDirectory(AbsolutePath)
case attributeNotProvided(String)
var description: String {
switch self {
case let .templateNotFound(template):
return "Could not find template \(template). Make sure it exists at Tuist/Templates/\(template)"
case let .nonEmptyDirectory(path):
return "Can't generate a template in the non-empty directory at path \(path.pathString)."
case let .attributeNotProvided(name):
return "You must provide \(name) option. Add --\(name) desired_value to your command."
}
}
}
class ScaffoldService {
private let templateLoader: TemplateLoading
private let templatesDirectoryLocator: TemplatesDirectoryLocating
private let templateGenerator: TemplateGenerating
init(templateLoader: TemplateLoading = TemplateLoader(),
templatesDirectoryLocator: TemplatesDirectoryLocating = TemplatesDirectoryLocator(),
templateGenerator: TemplateGenerating = TemplateGenerator()) {
self.templateLoader = templateLoader
self.templatesDirectoryLocator = templatesDirectoryLocator
self.templateGenerator = templateGenerator
}
func loadTemplateOptions(templateName: String,
path: String?) throws -> (required: [String],
optional: [String]) {
let path = self.path(path)
let directories = try templatesDirectoryLocator.templateDirectories(at: path)
let templateDirectory = try self.templateDirectory(templateDirectories: directories,
template: templateName)
let template = try templateLoader.loadTemplate(at: templateDirectory)
return template.attributes.reduce(into: (required: [], optional: [])) { currentValue, attribute in
switch attribute {
case let .optional(name, default: _):
currentValue.optional.append(name)
case let .required(name):
currentValue.required.append(name)
}
}
}
func run(path: String?,
templateName: String,
requiredTemplateOptions: [String: String],
optionalTemplateOptions: [String: String?]) throws {
let path = self.path(path)
let templateDirectories = try templatesDirectoryLocator.templateDirectories(at: path)
let templateDirectory = try self.templateDirectory(templateDirectories: templateDirectories,
template: templateName)
let template = try templateLoader.loadTemplate(at: templateDirectory)
let parsedAttributes = try parseAttributes(requiredTemplateOptions: requiredTemplateOptions,
optionalTemplateOptions: optionalTemplateOptions,
template: template)
try templateGenerator.generate(template: template,
to: path,
attributes: parsedAttributes)
logger.notice("Template \(templateName) was successfully generated", metadata: .success)
}
// MARK: - Helpers
private func path(_ path: String?) -> AbsolutePath {
if let path = path {
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
} else {
return FileHandler.shared.currentPath
}
}
/// Parses all `attributes` from `template`
/// If those attributes are optional, they default to `default` if not provided
/// - Returns: Array of parsed attributes
private func parseAttributes(requiredTemplateOptions: [String: String],
optionalTemplateOptions: [String: String?],
template: Template) throws -> [String: String] {
try template.attributes.reduce(into: [:]) { attributesDictionary, attribute in
switch attribute {
case let .required(name):
guard
let option = requiredTemplateOptions[name]
else { throw ScaffoldServiceError.attributeNotProvided(name) }
attributesDictionary[name] = option
case let .optional(name, default: defaultValue):
guard
let unwrappedOption = optionalTemplateOptions[name],
let option = unwrappedOption
else {
attributesDictionary[name] = defaultValue
return
}
attributesDictionary[name] = option
}
}
}
/// Finds template directory
/// - Parameters:
/// - templateDirectories: Paths of available templates
/// - template: Name of template
/// - Returns: `AbsolutePath` of template directory
private func templateDirectory(templateDirectories: [AbsolutePath], template: String) throws -> AbsolutePath {
guard
let templateDirectory = templateDirectories.first(where: { $0.basename == template })
else { throw ScaffoldServiceError.templateNotFound(template) }
return templateDirectory
}
}

View File

@ -0,0 +1,36 @@
import Basic
import TuistGenerator
import TuistLoader
import TuistSupport
final class UpService {
// MARK: - Attributes
/// Instance to load the setup manifest and perform the project setup.
private let setupLoader: SetupLoading
// MARK: - Init
init(setupLoader: SetupLoading = SetupLoader()) {
self.setupLoader = setupLoader
}
func run(path: String?) throws {
let path = self.path(path)
try setupLoader.meet(at: path)
}
// MARK: - Fileprivate
/// Parses the arguments and returns the path to the directory where
/// the up command should be ran.
///
/// - Parameter path: The path from parsing the command line arguments.
/// - Returns: Path to be used for the up command.
private func path(_ path: String?) -> AbsolutePath {
guard let path = path else {
return FileHandler.shared.currentPath
}
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
}
}

View File

@ -0,0 +1,9 @@
import Basic
import Foundation
import TuistSupport
final class VersionService {
func run() throws {
logger.notice("\(Constants.version)")
}
}

View File

@ -12,28 +12,10 @@ public protocol ErrorHandling: AnyObject {
/// The default implementation of the ErrorHandling protocol
public final class ErrorHandler: ErrorHandling {
// MARK: - Attributes
/// Function to exit the execution of the program.
var exiter: (Int32) -> Void
// MARK: - Init
/// Default error handler initializer.
public convenience init() {
self.init(exiter: { exit($0) })
}
/// Default error handler initializer.
///
/// - Parameters:
/// - exiter: Closure to exit the execution.
init(exiter: @escaping (Int32) -> Void) {
self.exiter = exiter
}
// MARK: - Public
public init() {}
/// When called, this method delegates the error handling
/// to the entity that conforms this protocol.
///
@ -49,6 +31,5 @@ public final class ErrorHandler: ErrorHandling {
"""
logger.error("\(message)")
}
exiter(1)
}
}

View File

@ -7,5 +7,4 @@ if CommandLine.arguments.contains("--verbose") { try? ProcessEnv.setVar("TUIST_V
LogOutput.bootstrap()
import TuistKit
var registry = CommandRegistry()
registry.run()
TuistCommand.main()

View File

@ -4,5 +4,4 @@ import enum TuistSupport.LogOutput
LogOutput.bootstrap()
var registry = CommandRegistry()
registry.run()
TuistCommand.main()

View File

@ -1,51 +0,0 @@
import Foundation
import TuistSupport
import XCTest
@testable import TuistEnvKit
@testable import TuistSupportTesting
final class CommandRegistryTests: XCTestCase {
var subject: CommandRegistry!
var errorHandler: MockErrorHandler!
var commandRunner: MockCommandRunner!
override func setUp() {
super.setUp()
errorHandler = MockErrorHandler()
commandRunner = MockCommandRunner()
}
func test_run_calls_the_runner_when_the_command_is_not_found() {
setupSubject(arguments: ["tuist", "command"], commands: [])
subject.run()
XCTAssertEqual(commandRunner.runCallCount, 1)
}
func test_run_calls_the_right_command() {
setupSubject(arguments: ["tuist", MockCommand.command], commands: [MockCommand.self])
subject.run()
XCTAssertEqual((subject.commands.first! as! MockCommand).runCallCount, 1)
}
func test_run_reports_fatal_errors() {
commandRunner.runStub = MockFatalError()
setupSubject(arguments: ["tuist", "command"], commands: [])
subject.run()
XCTAssertEqual(errorHandler.fatalErrorArgs.count, 1)
}
func test_run_reports_unhandled_errors() {
commandRunner.runStub = NSError(domain: "test", code: 1, userInfo: nil)
setupSubject(arguments: ["tuist", "command"], commands: [])
subject.run()
XCTAssertEqual(errorHandler.fatalErrorArgs.count, 1)
XCTAssertTrue(type(of: errorHandler.fatalErrorArgs.first!) == UnhandledError.self)
}
private func setupSubject(arguments: [String], commands: [Command.Type]) {
subject = CommandRegistry(processArguments: { arguments },
errorHandler: errorHandler,
commandRunner: commandRunner,
commands: commands)
}
}

View File

@ -1,56 +0,0 @@
import Foundation
import TuistSupport
import XCTest
@testable import SPMUtility
@testable import TuistEnvKit
@testable import TuistSupportTesting
final class UpdateCommandTests: TuistUnitTestCase {
var parser: ArgumentParser!
var subject: UpdateCommand!
var updater: MockUpdater!
override func setUp() {
super.setUp()
parser = ArgumentParser(usage: "test", overview: "overview")
updater = MockUpdater()
subject = UpdateCommand(parser: parser,
updater: updater)
}
override func tearDown() {
parser = nil
updater = nil
subject = nil
super.tearDown()
}
func test_command() {
XCTAssertEqual(UpdateCommand.command, "update")
}
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)
XCTAssertPrinterOutputContains("Checking for updates...")
XCTAssertEqual(updateCalls, [true])
}
}

View File

@ -1,43 +1,38 @@
import Basic
import Foundation
import XCTest
@testable import SPMUtility
@testable import TuistEnvKit
@testable import TuistSupport
@testable import TuistSupportTesting
final class BundleCommandErrorTests: XCTestCase {
final class BundleServiceErrorTests: XCTestCase {
func test_type() {
let path = AbsolutePath("/test")
XCTAssertEqual(BundleCommandError.missingVersionFile(path).type, .abort)
XCTAssertEqual(BundleServiceError.missingVersionFile(path).type, .abort)
}
func test_description() {
let path = AbsolutePath("/test")
XCTAssertEqual(BundleCommandError.missingVersionFile(path).description, "Couldn't find a .tuist-version file in the directory \(path.pathString)")
XCTAssertEqual(BundleServiceError.missingVersionFile(path).description, "Couldn't find a .tuist-version file in the directory \(path.pathString)")
}
}
final class BundleCommandTests: TuistUnitTestCase {
var parser: ArgumentParser!
final class BundleServiceTests: TuistUnitTestCase {
var versionsController: MockVersionsController!
var installer: MockInstaller!
var subject: BundleCommand!
var subject: BundleService!
var tmpDir: TemporaryDirectory!
override func setUp() {
super.setUp()
parser = ArgumentParser(usage: "test", overview: "overview")
versionsController = try! MockVersionsController()
installer = MockInstaller()
tmpDir = try! TemporaryDirectory(removeTreeOnDeinit: true)
subject = BundleCommand(parser: parser,
versionsController: versionsController,
subject = BundleService(versionsController: versionsController,
installer: installer)
}
override func tearDown() {
parser = nil
versionsController = nil
installer = nil
subject = nil
@ -45,29 +40,13 @@ final class BundleCommandTests: TuistUnitTestCase {
super.tearDown()
}
func test_init_registers_the_command() {
XCTAssertEqual(parser.subparsers.count, 1)
XCTAssertEqual(parser.subparsers.first?.key, BundleCommand.command)
XCTAssertEqual(parser.subparsers.first?.value.overview, BundleCommand.overview)
}
func test_command() {
XCTAssertEqual(BundleCommand.command, "bundle")
}
func test_overview() {
XCTAssertEqual(BundleCommand.overview, "Bundles the version specified in the .tuist-version file into the .tuist-bin directory")
}
func test_run_throws_when_there_is_no_xmp_version_in_the_directory() throws {
let temporaryPath = try self.temporaryPath()
let result = try parser.parse([])
XCTAssertThrowsSpecific(try subject.run(with: result), BundleCommandError.missingVersionFile(temporaryPath))
XCTAssertThrowsSpecific(try subject.run(), BundleServiceError.missingVersionFile(temporaryPath))
}
func test_run_installs_the_app_if_it_doesnt_exist() throws {
let temporaryPath = try self.temporaryPath()
let result = try parser.parse([])
let tuistVersionPath = temporaryPath.appending(component: Constants.versionFileName)
try "3.2.1".write(to: tuistVersionPath.url, atomically: true, encoding: .utf8)
@ -77,7 +56,7 @@ final class BundleCommandTests: TuistUnitTestCase {
try Data().write(to: versionPath.appending(component: "test").url)
}
try subject.run(with: result)
try subject.run()
let bundledTestFilePath = temporaryPath
.appending(component: Constants.binFolderName)
@ -89,19 +68,17 @@ final class BundleCommandTests: TuistUnitTestCase {
func test_run_doesnt_install_the_app_if_it_already_exists() throws {
let temporaryPath = try self.temporaryPath()
let result = try parser.parse([])
let tuistVersionPath = temporaryPath.appending(component: Constants.versionFileName)
try "3.2.1".write(to: tuistVersionPath.url, atomically: true, encoding: .utf8)
let versionPath = versionsController.path(version: "3.2.1")
try FileHandler.shared.createFolder(versionPath)
try subject.run(with: result)
try subject.run()
XCTAssertEqual(installer.installCallCount, 0)
}
func test_run_prints_the_right_messages() throws {
let result = try parser.parse([])
let temporaryPath = try self.temporaryPath()
let tuistVersionPath = temporaryPath.appending(component: Constants.versionFileName)
let binPath = temporaryPath.appending(component: Constants.binFolderName)
@ -114,7 +91,7 @@ final class BundleCommandTests: TuistUnitTestCase {
try Data().write(to: versionPath.appending(component: "test").url)
}
try subject.run(with: result)
try subject.run()
XCTAssertPrinterOutputContains("""
Bundling the version 3.2.1 in the directory \(binPath.pathString)

View File

@ -2,29 +2,24 @@ import Basic
import Foundation
import TuistSupport
import XCTest
@testable import SPMUtility
@testable import TuistEnvKit
@testable import TuistSupportTesting
final class InstallCommandTests: TuistUnitTestCase {
var parser: ArgumentParser!
final class InstallServiceTests: TuistUnitTestCase {
var versionsController: MockVersionsController!
var installer: MockInstaller!
var subject: InstallCommand!
var subject: InstallService!
override func setUp() {
super.setUp()
parser = ArgumentParser(usage: "test", overview: "overview")
versionsController = try! MockVersionsController()
installer = MockInstaller()
subject = InstallCommand(parser: parser,
versionsController: versionsController,
subject = InstallService(versionsController: versionsController,
installer: installer)
}
override func tearDown() {
parser = nil
versionsController = nil
installer = nil
subject = nil
@ -32,39 +27,21 @@ final class InstallCommandTests: TuistUnitTestCase {
super.tearDown()
}
func test_command() {
XCTAssertEqual(InstallCommand.command, "install")
}
func test_overview() {
XCTAssertEqual(InstallCommand.overview, "Installs a version of tuist")
}
func test_init_registers_the_command() {
XCTAssertEqual(parser.subparsers.count, 1)
XCTAssertEqual(parser.subparsers.first?.key, InstallCommand.command)
XCTAssertEqual(parser.subparsers.first?.value.overview, InstallCommand.overview)
}
func test_run_when_version_is_already_installed() throws {
let result = try parser.parse(["install", "3.2.1"])
versionsController.versionsStub = [InstalledVersion.reference("3.2.1")]
try subject.run(with: result)
try subject.run(version: "3.2.1", force: false)
XCTAssertPrinterOutputContains("Version 3.2.1 already installed, skipping")
}
func test_run() throws {
let result = try parser.parse(["install", "3.2.1"])
versionsController.versionsStub = []
var installArgs: [(version: String, force: Bool)] = []
installer.installStub = { version, force in installArgs.append((version: version, force: force)) }
try subject.run(with: result)
try subject.run(version: "3.2.1", force: false)
XCTAssertEqual(installArgs.count, 1)
XCTAssertEqual(installArgs.first?.version, "3.2.1")
@ -72,14 +49,12 @@ final class InstallCommandTests: TuistUnitTestCase {
}
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)
try subject.run(version: "3.2.1", force: true)
XCTAssertEqual(installArgs.count, 1)
XCTAssertEqual(installArgs.first?.version, "3.2.1")

View File

@ -6,56 +6,44 @@ import XCTest
@testable import TuistEnvKit
@testable import TuistSupportTesting
final class LocalCommandTests: TuistUnitTestCase {
var argumentParser: ArgumentParser!
var subject: LocalCommand!
final class LocalServiceTests: TuistUnitTestCase {
var subject: LocalService!
var versionController: MockVersionsController!
override func setUp() {
super.setUp()
argumentParser = ArgumentParser(usage: "test", overview: "overview")
versionController = try! MockVersionsController()
subject = LocalCommand(parser: argumentParser, versionController: versionController)
subject = LocalService(versionController: versionController)
}
override func tearDown() {
argumentParser = nil
subject = nil
versionController = nil
super.tearDown()
}
func test_command() {
XCTAssertEqual(LocalCommand.command, "local")
}
func test_overview() {
XCTAssertEqual(LocalCommand.overview, "Creates a .tuist-version file to pin the tuist version that should be used in the current directory. If the version is not specified, it prints the local versions")
}
func test_init_registers_the_command() {
XCTAssertEqual(argumentParser.subparsers.count, 1)
XCTAssertEqual(argumentParser.subparsers.first?.key, LocalCommand.command)
XCTAssertEqual(argumentParser.subparsers.first?.value.overview, LocalCommand.overview)
}
func test_run_when_version_argument_is_passed() throws {
// Given
let temporaryPath = try self.temporaryPath()
let result = try argumentParser.parse(["local", "3.2.1"])
try subject.run(with: result)
// When
try subject.run(version: "3.2.1")
// Then
let versionPath = temporaryPath.appending(component: Constants.versionFileName)
XCTAssertEqual(try String(contentsOf: versionPath.url), "3.2.1")
}
func test_run_prints_when_version_argument_is_passed() throws {
// Given
let temporaryPath = try self.temporaryPath()
let result = try argumentParser.parse(["local", "3.2.1"])
try subject.run(with: result)
// When
try subject.run(version: "3.2.1")
// Then
let versionPath = temporaryPath.appending(component: Constants.versionFileName)
XCTAssertPrinterOutputContains("""
@ -65,10 +53,13 @@ final class LocalCommandTests: TuistUnitTestCase {
}
func test_run_prints_when_no_argument_is_passed() throws {
let result = try argumentParser.parse(["local"])
// Given
versionController.semverVersionsStub = [Version(string: "1.2.3")!, Version(string: "3.2.1")!]
try subject.run(with: result)
// When
try subject.run(version: nil)
// Then
XCTAssertPrinterOutputContains("""
The following versions are available in the local environment:
- 3.2.1

View File

@ -2,80 +2,55 @@ import Basic
import Foundation
import TuistSupport
import XCTest
@testable import SPMUtility
@testable import TuistEnvKit
@testable import TuistSupportTesting
final class UninstallCommandTests: TuistUnitTestCase {
var parser: ArgumentParser!
final class UninstallServiceTests: TuistUnitTestCase {
var versionsController: MockVersionsController!
var installer: MockInstaller!
var subject: UninstallCommand!
var subject: UninstallService!
override func setUp() {
super.setUp()
parser = ArgumentParser(usage: "test", overview: "overview")
versionsController = try! MockVersionsController()
installer = MockInstaller()
subject = UninstallCommand(parser: parser,
versionsController: versionsController,
subject = UninstallService(versionsController: versionsController,
installer: installer)
}
override func tearDown() {
parser = nil
versionsController = nil
installer = nil
subject = nil
super.tearDown()
}
func test_command() {
XCTAssertEqual(UninstallCommand.command, "uninstall")
}
func test_overview() {
XCTAssertEqual(UninstallCommand.overview, "Uninstalls a version of tuist")
}
func test_init_registers_the_command() {
XCTAssertEqual(parser.subparsers.count, 1)
XCTAssertEqual(parser.subparsers.first?.key, UninstallCommand.command)
XCTAssertEqual(parser.subparsers.first?.value.overview, UninstallCommand.overview)
}
func test_run_when_version_is_installed() throws {
let result = try parser.parse(["uninstall", "3.2.1"])
versionsController.versionsStub = [InstalledVersion.reference("3.2.1")]
var uninstalledVersion: String?
versionsController.uninstallStub = { uninstalledVersion = $0 }
try subject.run(with: result)
try subject.run(version: "3.2.1")
XCTAssertPrinterOutputContains("Version 3.2.1 uninstalled")
XCTAssertEqual(uninstalledVersion, "3.2.1")
}
func test_run_when_version_is_installed_and_throws() throws {
let result = try parser.parse(["uninstall", "3.2.1"])
versionsController.versionsStub = [InstalledVersion.reference("3.2.1")]
let error = NSError.test()
versionsController.uninstallStub = { _ in throw error }
XCTAssertThrowsError(try subject.run(with: result)) {
XCTAssertThrowsError(try subject.run(version: "3.2.1")) {
XCTAssertEqual($0 as NSError, error)
}
}
func test_run_when_version_is_not_installed() throws {
let result = try parser.parse(["uninstall", "3.2.1"])
versionsController.versionsStub = []
try subject.run(with: result)
try subject.run(version: "3.2.1")
XCTAssertPrinterOutputContains("Version 3.2.1 cannot be uninstalled because it's not installed")
}

View File

@ -0,0 +1,35 @@
import Foundation
import TuistSupport
import XCTest
@testable import TuistEnvKit
@testable import TuistSupportTesting
final class UpdateServiceTests: TuistUnitTestCase {
var subject: UpdateService!
var updater: MockUpdater!
override func setUp() {
super.setUp()
updater = MockUpdater()
subject = UpdateService(updater: updater)
}
override func tearDown() {
updater = nil
subject = nil
super.tearDown()
}
func test_run() throws {
var updateCalls: [Bool] = []
updater.updateStub = { force in
updateCalls.append(force)
}
try subject.run(force: true)
XCTAssertPrinterOutputContains("Checking for updates...")
XCTAssertEqual(updateCalls, [true])
}
}

View File

@ -8,24 +8,20 @@ import XCTest
@testable import TuistLoader
@testable import TuistSupportTesting
final class DumpCommandTests: TuistTestCase {
final class DumpServiceTests: TuistTestCase {
var errorHandler: MockErrorHandler!
var subject: DumpCommand!
var parser: ArgumentParser!
var subject: DumpService!
var manifestLoading: ManifestLoading!
override func setUp() {
super.setUp()
errorHandler = MockErrorHandler()
parser = ArgumentParser.test()
manifestLoading = ManifestLoader()
subject = DumpCommand(manifestLoader: manifestLoading,
parser: parser)
subject = DumpService(manifestLoader: manifestLoading)
}
override func tearDown() {
errorHandler = nil
parser = nil
manifestLoading = nil
subject = nil
super.tearDown()
@ -44,8 +40,7 @@ final class DumpCommandTests: TuistTestCase {
try config.write(toFile: tmpDir.path.appending(component: "Project.swift").pathString,
atomically: true,
encoding: .utf8)
let result = try parser.parse([DumpCommand.command, "-p", tmpDir.path.pathString])
try subject.run(with: result)
try subject.run(path: tmpDir.path.pathString)
let expected = "{\n \"additionalFiles\": [\n\n ],\n \"name\": \"tuist\",\n \"organizationName\": \"tuist\",\n \"packages\": [\n\n ],\n \"schemes\": [\n\n ],\n \"targets\": [\n\n ]\n}\n"
XCTAssertPrinterOutputContains(expected)

View File

@ -1,14 +0,0 @@
import Foundation
import XCTest
@testable import TuistKit
final class BuildCommandTests: XCTestCase {
func test_command() {
XCTAssertEqual(BuildCommand.command, "build")
}
func test_overview() {
XCTAssertEqual(BuildCommand.overview, "Builds a project target.")
}
}

View File

@ -1,33 +0,0 @@
import Basic
import Foundation
import SPMUtility
import TuistSupport
import XCTest
@testable import TuistKit
@testable import TuistSupportTesting
final class CacheCommandTests: TuistUnitTestCase {
var subject: CacheCommand!
var parser: ArgumentParser!
override func setUp() {
super.setUp()
parser = ArgumentParser.test()
subject = CacheCommand(parser: parser)
}
override func tearDown() {
parser = nil
subject = nil
super.tearDown()
}
func test_name() {
XCTAssertEqual(CacheCommand.command, "cache")
}
func test_overview() {
XCTAssertEqual(CacheCommand.overview, "Cache frameworks as .xcframeworks to speed up build times in generated projects")
}
}

View File

@ -1,34 +0,0 @@
import Foundation
import SPMUtility
import XCTest
@testable import TuistKit
@testable import TuistSupportTesting
final class CreateIssueCommandTests: TuistUnitTestCase {
var subject: CreateIssueCommand!
override func setUp() {
super.setUp()
let parser = ArgumentParser.test()
subject = CreateIssueCommand(parser: parser)
}
override func tearDown() {
subject = nil
super.tearDown()
}
func test_command() {
XCTAssertEqual(CreateIssueCommand.command, "create-issue")
}
func test_overview() {
XCTAssertEqual(CreateIssueCommand.overview, "Opens the GitHub page to create a new issue.")
}
func test_run() throws {
system.succeedCommand("/usr/bin/open", CreateIssueCommand.createIssueUrl)
try subject.run(with: ArgumentParser.Result.test())
}
}

View File

@ -1,17 +0,0 @@
import Basic
import Foundation
import SPMUtility
import XcodeProj
import XCTest
@testable import TuistKit
@testable import TuistSupportTesting
final class EditCommandTests: TuistUnitTestCase {
func test_command() {
XCTAssertEqual(EditCommand.command, "edit")
}
func test_overview() {
XCTAssertEqual(EditCommand.overview, "Generates a temporary project to edit the project in the current directory")
}
}

View File

@ -0,0 +1,25 @@
import Foundation
import SPMUtility
import XCTest
@testable import TuistKit
@testable import TuistSupportTesting
final class CreateIssueServiceTests: TuistUnitTestCase {
var subject: CreateIssueService!
override func setUp() {
super.setUp()
subject = CreateIssueService()
}
override func tearDown() {
subject = nil
super.tearDown()
}
func test_run() throws {
system.succeedCommand("/usr/bin/open", CreateIssueService.createIssueUrl)
try subject.run()
}
}

View File

@ -1,6 +1,5 @@
import Basic
import Foundation
import SPMUtility
import TuistLoader
import TuistSupport
import XCTest
@ -9,41 +8,28 @@ import XCTest
@testable import TuistLoaderTesting
@testable import TuistSupportTesting
final class DumpCommandTests: TuistUnitTestCase {
final class DumpServiceTests: TuistUnitTestCase {
var errorHandler: MockErrorHandler!
var subject: DumpCommand!
var parser: ArgumentParser!
var subject: DumpService!
var manifestLoading: ManifestLoading!
override func setUp() {
super.setUp()
errorHandler = MockErrorHandler()
parser = ArgumentParser.test()
manifestLoading = ManifestLoader()
subject = DumpCommand(manifestLoader: manifestLoading,
parser: parser)
subject = DumpService(manifestLoader: manifestLoading)
}
override func tearDown() {
errorHandler = nil
parser = nil
manifestLoading = nil
subject = nil
super.tearDown()
}
func test_name() {
XCTAssertEqual(DumpCommand.command, "dump")
}
func test_overview() {
XCTAssertEqual(DumpCommand.overview, "Outputs the project manifest as a JSON")
}
func test_run_throws_when_file_doesnt_exist() throws {
let tmpDir = try TemporaryDirectory(removeTreeOnDeinit: true)
let result = try parser.parse([DumpCommand.command, "-p", tmpDir.path.pathString])
XCTAssertThrowsSpecific(try subject.run(with: result),
XCTAssertThrowsSpecific(try subject.run(path: tmpDir.path.pathString),
ManifestLoaderError.manifestNotFound(.project, tmpDir.path))
}
@ -52,7 +38,6 @@ final class DumpCommandTests: TuistUnitTestCase {
try "invalid config".write(toFile: tmpDir.path.appending(component: "Project.swift").pathString,
atomically: true,
encoding: .utf8)
let result = try parser.parse([DumpCommand.command, "-p", tmpDir.path.pathString])
XCTAssertThrowsError(try subject.run(with: result))
XCTAssertThrowsError(try subject.run(path: tmpDir.path.pathString))
}
}

View File

@ -1,6 +1,5 @@
import Basic
import Foundation
import SPMUtility
import TuistCore
import TuistLoader
import XcodeProj
@ -10,57 +9,43 @@ import XCTest
@testable import TuistLoaderTesting
@testable import TuistSupportTesting
final class FocusCommandTests: TuistUnitTestCase {
var subject: FocusCommand!
var parser: ArgumentParser!
final class FocusServiceTests: TuistUnitTestCase {
var subject: FocusService!
var opener: MockOpener!
var generator: MockProjectGenerator!
override func setUp() {
super.setUp()
parser = ArgumentParser.test()
opener = MockOpener()
generator = MockProjectGenerator()
subject = FocusCommand(parser: parser,
generator: generator,
subject = FocusService(generator: generator,
opener: opener)
}
override func tearDown() {
parser = nil
opener = nil
generator = nil
subject = nil
super.tearDown()
}
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_theworkspaceGenerationFails() throws {
let result = try parser.parse([FocusCommand.command])
let error = NSError.test()
generator.generateStub = { _, _ in
throw error
}
XCTAssertThrowsError(try subject.run(with: result)) {
XCTAssertThrowsError(try subject.run()) {
XCTAssertEqual($0 as NSError?, error)
}
}
func test_run() throws {
let result = try parser.parse([FocusCommand.command])
let workspacePath = AbsolutePath("/test.xcworkspace")
generator.generateStub = { _, _ in
workspacePath
}
try subject.run(with: result)
try subject.run()
XCTAssertEqual(opener.openArgs.last?.0, workspacePath.pathString)
}

View File

@ -1,6 +1,5 @@
import Basic
import Foundation
import SPMUtility
import TuistCore
import TuistLoader
import TuistSupport
@ -11,48 +10,33 @@ import XCTest
@testable import TuistLoaderTesting
@testable import TuistSupportTesting
final class GenerateCommandTests: TuistUnitTestCase {
var subject: GenerateCommand!
final class GenerateServiceTests: TuistUnitTestCase {
var subject: GenerateService!
var generator: MockProjectGenerator!
var parser: ArgumentParser!
var clock: StubClock!
override func setUp() {
super.setUp()
generator = MockProjectGenerator()
parser = ArgumentParser.test()
clock = StubClock()
generator.generateStub = { _, _ in
AbsolutePath("/Test")
}
subject = GenerateCommand(parser: parser,
generator: generator,
subject = GenerateService(generator: generator,
clock: clock)
}
override func tearDown() {
generator = nil
parser = nil
clock = nil
subject = nil
super.tearDown()
}
func test_command() {
XCTAssertEqual(GenerateCommand.command, "generate")
}
func test_overview() {
XCTAssertEqual(GenerateCommand.overview, "Generates an Xcode workspace to start working on the project.")
}
func test_run() throws {
// Given
let result = try parser.parse([GenerateCommand.command])
// When
try subject.run(with: result)
try subject.testRun()
// Then
XCTAssertPrinterOutputContains("Project generated.")
@ -60,14 +44,13 @@ final class GenerateCommandTests: TuistUnitTestCase {
func test_run_timeIsPrinted() throws {
// Given
let result = try parser.parse([GenerateCommand.command])
clock.assertOnUnexpectedCalls = true
clock.primedTimers = [
0.234,
]
// When
try subject.run(with: result)
try subject.testRun()
// Then
XCTAssertPrinterOutputContains("Total time taken: 0.234s")
@ -76,7 +59,6 @@ final class GenerateCommandTests: TuistUnitTestCase {
func test_run_withRelativePathParameter() throws {
// Given
let temporaryPath = try self.temporaryPath()
let result = try parser.parse([GenerateCommand.command, "--path", "subpath"])
var generationPath: AbsolutePath?
generator.generateStub = { path, _ in
generationPath = path
@ -84,7 +66,7 @@ final class GenerateCommandTests: TuistUnitTestCase {
}
// When
try subject.run(with: result)
try subject.testRun(path: "subpath")
// Then
XCTAssertEqual(generationPath, AbsolutePath("subpath", relativeTo: temporaryPath))
@ -92,7 +74,6 @@ final class GenerateCommandTests: TuistUnitTestCase {
func test_run_withAbsoultePathParameter() throws {
// Given
let result = try parser.parse([GenerateCommand.command, "--path", "/some/path"])
var generationPath: AbsolutePath?
generator.generateStub = { path, _ in
generationPath = path
@ -100,7 +81,7 @@ final class GenerateCommandTests: TuistUnitTestCase {
}
// When
try subject.run(with: result)
try subject.testRun(path: "/some/path")
// Then
XCTAssertEqual(generationPath, AbsolutePath("/some/path"))
@ -109,7 +90,6 @@ final class GenerateCommandTests: TuistUnitTestCase {
func test_run_withoutPathParameter() throws {
// Given
let temporaryPath = try self.temporaryPath()
let result = try parser.parse([GenerateCommand.command])
var generationPath: AbsolutePath?
generator.generateStub = { path, _ in
generationPath = path
@ -117,7 +97,7 @@ final class GenerateCommandTests: TuistUnitTestCase {
}
// When
try subject.run(with: result)
try subject.testRun()
// Then
XCTAssertEqual(generationPath, temporaryPath)
@ -125,15 +105,11 @@ final class GenerateCommandTests: TuistUnitTestCase {
func test_run_withProjectOnlyParameter() throws {
// Given
let arguments = [
[GenerateCommand.command, "--project-only"],
[GenerateCommand.command],
]
let projectOnlyValues = [true, false]
// When
try arguments.forEach {
let result = try parser.parse($0)
try subject.run(with: result)
try projectOnlyValues.forEach {
try subject.testRun(projectOnly: $0)
}
// Then
@ -145,15 +121,22 @@ final class GenerateCommandTests: TuistUnitTestCase {
func test_run_fatalErrors_when_theworkspaceGenerationFails() throws {
// Given
let result = try parser.parse([GenerateCommand.command])
let error = NSError.test()
generator.generateStub = { _, _ in
throw error
}
// When / Then
XCTAssertThrowsError(try subject.run(with: result)) {
XCTAssertThrowsError(try subject.testRun()) {
XCTAssertEqual($0 as NSError, error)
}
}
}
extension GenerateService {
func testRun(path: String? = nil,
projectOnly: Bool = false) throws {
try run(path: path,
projectOnly: projectOnly)
}
}

View File

@ -1,6 +1,5 @@
import Basic
import Foundation
import SPMUtility
import TuistSupport
import XcodeProj
import XCTest
@ -9,38 +8,26 @@ import XCTest
@testable import TuistLoaderTesting
@testable import TuistSupportTesting
final class GraphCommandTests: TuistUnitTestCase {
var subject: GraphCommand!
final class GraphServiceTests: TuistUnitTestCase {
var subject: GraphService!
var dotGraphGenerator: MockDotGraphGenerator!
var manifestLoader: MockManifestLoader!
var parser: ArgumentParser!
override func setUp() {
super.setUp()
dotGraphGenerator = MockDotGraphGenerator()
manifestLoader = MockManifestLoader()
parser = ArgumentParser.test()
subject = GraphCommand(parser: parser,
dotGraphGenerator: dotGraphGenerator,
subject = GraphService(dotGraphGenerator: dotGraphGenerator,
manifestLoader: manifestLoader)
}
override func tearDown() {
dotGraphGenerator = nil
manifestLoader = nil
parser = nil
subject = nil
super.tearDown()
}
func test_command() {
XCTAssertEqual(GraphCommand.command, "graph")
}
func test_overview() {
XCTAssertEqual(GraphCommand.overview, "Generates a dot graph from the workspace or project in the current directory.")
}
func test_run() throws {
// Given
let temporaryPath = try self.temporaryPath()
@ -59,8 +46,7 @@ final class GraphCommandTests: TuistUnitTestCase {
dotGraphGenerator.generateProjectStub = graph
// When
let result = try parser.parse([GraphCommand.command])
try subject.run(with: result)
try subject.run()
// Then
XCTAssertEqual(try FileHandler.shared.readTextFile(graphPath), graph)

View File

@ -1,6 +1,5 @@
import Basic
import Foundation
import SPMUtility
import TuistScaffold
import TuistSupport
import XCTest
@ -10,27 +9,23 @@ import XCTest
@testable import TuistScaffoldTesting
@testable import TuistSupportTesting
final class InitCommandTests: TuistUnitTestCase {
var subject: InitCommand!
var parser: ArgumentParser!
final class InitServiceTests: TuistUnitTestCase {
var subject: InitService!
var templatesDirectoryLocator: MockTemplatesDirectoryLocator!
var templateGenerator: MockTemplateGenerator!
var templateLoader: MockTemplateLoader!
override func setUp() {
super.setUp()
parser = ArgumentParser.test()
templatesDirectoryLocator = MockTemplatesDirectoryLocator()
templateGenerator = MockTemplateGenerator()
templateLoader = MockTemplateLoader()
subject = InitCommand(parser: parser,
subject = InitService(templateLoader: templateLoader,
templatesDirectoryLocator: templatesDirectoryLocator,
templateGenerator: templateGenerator,
templateLoader: templateLoader)
templateGenerator: templateGenerator)
}
override func tearDown() {
parser = nil
subject = nil
templatesDirectoryLocator = nil
templateGenerator = nil
@ -38,29 +33,18 @@ final class InitCommandTests: TuistUnitTestCase {
super.tearDown()
}
func test_name() {
XCTAssertEqual(InitCommand.command, "init")
}
func test_overview() {
XCTAssertEqual(InitCommand.overview, "Bootstraps a project.")
}
func test_fails_when_directory_not_empty() throws {
// Given
let path = FileHandler.shared.currentPath
try FileHandler.shared.touch(path.appending(component: "dummy"))
let result = try parser.parse([InitCommand.command])
// Then
XCTAssertThrowsSpecific(try subject.run(with: result), InitCommandError.nonEmptyDirectory(path))
XCTAssertThrowsSpecific(try subject.testRun(), InitServiceError.nonEmptyDirectory(path))
}
func test_init_fails_when_template_not_found() throws {
let templateName = "template"
let result = try parser.parse([InitCommand.command, "--template", templateName])
XCTAssertThrowsSpecific(try subject.run(with: result), InitCommandError.templateNotFound(templateName))
XCTAssertThrowsSpecific(try subject.testRun(templateName: templateName), InitServiceError.templateNotFound(templateName))
}
func test_init_default_when_no_template() throws {
@ -70,35 +54,50 @@ final class InitCommandTests: TuistUnitTestCase {
[defaultTemplatePath]
}
let expectedAttributes = ["name": "name", "platform": "macOS"]
let result = try parser.parse([InitCommand.command, "--name", "name", "--platform", "macos"])
var generatorAttributes: [String: String] = [:]
templateGenerator.generateStub = { _, _, attributes in
generatorAttributes = attributes
}
// When
try subject.run(with: result)
try subject.testRun(name: "name", platform: "macos")
// Then
XCTAssertEqual(expectedAttributes, generatorAttributes)
}
func test_init_default_platform() throws {
// Given
let defaultTemplatePath = try temporaryPath().appending(component: "default")
templatesDirectoryLocator.templateDirectoriesStub = { _ in
[defaultTemplatePath]
}
let expectedAttributes = ["name": "name", "platform": "iOS"]
let result = try parser.parse([InitCommand.command, "--name", "name"])
var generatorAttributes: [String: String] = [:]
templateGenerator.generateStub = { _, _, attributes in
generatorAttributes = attributes
}
// When
try subject.run(with: result)
try subject.testRun(name: "name")
// Then
XCTAssertEqual(expectedAttributes, generatorAttributes)
}
}
extension InitService {
func testRun(name: String? = nil,
platform: String? = nil,
path: String? = nil,
templateName: String? = nil,
requiredTemplateOptions: [String: String] = [:],
optionalTemplateOptions: [String: String?] = [:]) throws {
try run(name: name,
platform: platform,
path: path,
templateName: templateName,
requiredTemplateOptions: requiredTemplateOptions,
optionalTemplateOptions: optionalTemplateOptions)
}
}

View File

@ -1,6 +1,5 @@
import Basic
import Foundation
import SPMUtility
import TuistCore
import XCTest
@ -10,25 +9,22 @@ import XCTest
@testable import TuistLoaderTesting
@testable import TuistSupportTesting
final class LintCommandTests: TuistUnitTestCase {
var parser: ArgumentParser!
final class LintServiceTests: TuistUnitTestCase {
var graphLinter: MockGraphLinter!
var environmentLinter: MockEnvironmentLinter!
var manifestLoader: MockManifestLoader!
var graphLoader: MockGraphLoader!
var subject: LintCommand!
var subject: LintService!
override func setUp() {
parser = ArgumentParser.test()
graphLinter = MockGraphLinter()
environmentLinter = MockEnvironmentLinter()
manifestLoader = MockManifestLoader()
graphLoader = MockGraphLoader()
subject = LintCommand(graphLinter: graphLinter,
subject = LintService(graphLinter: graphLinter,
environmentLinter: environmentLinter,
manifestLoading: manifestLoader,
graphLoader: graphLoader,
parser: parser)
graphLoader: graphLoader)
super.setUp()
}
@ -41,32 +37,22 @@ final class LintCommandTests: TuistUnitTestCase {
subject = nil
}
func test_command() {
XCTAssertEqual(LintCommand.command, "lint")
}
func test_overview() {
XCTAssertEqual(LintCommand.overview, "Lints a workspace or a project that check whether they are well configured.")
}
func test_run_throws_an_error_when_no_manifests_exist() throws {
// Given
let path = try temporaryPath()
manifestLoader.manifestsAtStub = { _ in Set() }
let result = try parser.parse([LintCommand.command, "--path", path.pathString])
// When
XCTAssertThrowsSpecific(try subject.run(with: result), LintCommandError.manifestNotFound(path))
XCTAssertThrowsSpecific(try subject.run(path: path.pathString), LintServiceError.manifestNotFound(path))
}
func test_run_when_there_are_no_issues_and_project_manifest() throws {
// Given
let path = try temporaryPath()
manifestLoader.manifestsAtStub = { _ in Set([.project]) }
let result = try parser.parse([LintCommand.command, "--path", path.pathString])
// When
try subject.run(with: result)
try subject.run(path: path.pathString)
// Then
XCTAssertPrinterOutputContains("""
@ -83,10 +69,9 @@ final class LintCommandTests: TuistUnitTestCase {
// Given
let path = try temporaryPath()
manifestLoader.manifestsAtStub = { _ in Set([.workspace]) }
let result = try parser.parse([LintCommand.command, "--path", path.pathString])
// When
try subject.run(with: result)
try subject.run(path: path.pathString)
// Then
XCTAssertPrinterOutputContains("""
@ -105,10 +90,9 @@ final class LintCommandTests: TuistUnitTestCase {
manifestLoader.manifestsAtStub = { _ in Set([.workspace]) }
environmentLinter.lintStub = [LintingIssue(reason: "environment", severity: .error)]
graphLinter.lintStub = [LintingIssue(reason: "graph", severity: .error)]
let result = try parser.parse([LintCommand.command, "--path", path.pathString])
// Then
XCTAssertThrowsSpecific(try subject.run(with: result), LintingError())
XCTAssertThrowsSpecific(try subject.run(path: path.pathString), LintingError())
XCTAssertPrinterOutputContains("""
Loading the dependency graph
Loading workspace at \(path.pathString)

View File

@ -0,0 +1,51 @@
import Basic
import XCTest
@testable import TuistCore
@testable import TuistKit
@testable import TuistLoaderTesting
@testable import TuistScaffoldTesting
@testable import TuistSupportTesting
final class ListServiceTests: TuistUnitTestCase {
var subject: ListService!
var templateLoader: MockTemplateLoader!
var templatesDirectoryLocator: MockTemplatesDirectoryLocator!
override func setUp() {
super.setUp()
templateLoader = MockTemplateLoader()
templatesDirectoryLocator = MockTemplatesDirectoryLocator()
subject = ListService(templatesDirectoryLocator: templatesDirectoryLocator,
templateLoader: templateLoader)
}
override func tearDown() {
subject = nil
templateLoader = nil
templatesDirectoryLocator = nil
super.tearDown()
}
func test_lists_available_templates() throws {
// Given
let expectedTemplates = ["template", "customTemplate"]
let expectedOutput = expectedTemplates.map { $0 + ": description" }
templatesDirectoryLocator.templateDirectoriesStub = { _ in
try expectedTemplates.map(self.temporaryPath().appending)
}
templateLoader.loadTemplateStub = { _ in
Template(description: "description")
}
// When
try subject.run(path: nil)
// Then
expectedOutput.forEach {
XCTAssertPrinterContains($0, at: .info, ==)
}
}
}

View File

@ -12,27 +12,23 @@ import XCTest
@testable import TuistScaffoldTesting
@testable import TuistSupportTesting
final class ScaffoldCommandTests: TuistUnitTestCase {
var subject: ScaffoldCommand!
var parser: ArgumentParser!
final class ScaffoldServiceTests: TuistUnitTestCase {
var subject: ScaffoldService!
var templateLoader: MockTemplateLoader!
var templatesDirectoryLocator: MockTemplatesDirectoryLocator!
var templateGenerator: MockTemplateGenerator!
override func setUp() {
super.setUp()
parser = ArgumentParser.test()
templateLoader = MockTemplateLoader()
templatesDirectoryLocator = MockTemplatesDirectoryLocator()
templateGenerator = MockTemplateGenerator()
subject = ScaffoldCommand(parser: parser,
templateLoader: templateLoader,
subject = ScaffoldService(templateLoader: templateLoader,
templatesDirectoryLocator: templatesDirectoryLocator,
templateGenerator: templateGenerator)
}
override func tearDown() {
parser = nil
subject = nil
templateLoader = nil
templatesDirectoryLocator = nil
@ -40,52 +36,35 @@ final class ScaffoldCommandTests: TuistUnitTestCase {
super.tearDown()
}
func test_name() {
XCTAssertEqual(ScaffoldCommand.command, "scaffold")
}
func test_load_template_options() throws {
// Given
templateLoader.loadTemplateStub = { _ in
Template(description: "test",
attributes: [
.required("required"),
.optional("optional", default: ""),
])
}
func test_overview() {
XCTAssertEqual(ScaffoldCommand.overview, "Generates new project based on template.")
templatesDirectoryLocator.templateDirectoriesStub = { _ in
[try self.temporaryPath().appending(component: "template")]
}
let expectedOptions: (required: [String], optional: [String]) = (required: ["required"], optional: ["optional"])
// When
let options = try subject.loadTemplateOptions(templateName: "template",
path: nil)
// Then
XCTAssertEqual(options.required, expectedOptions.required)
XCTAssertEqual(options.optional, expectedOptions.optional)
}
func test_fails_when_template_not_found() throws {
let templateName = "template"
let result = try parser.parse([ScaffoldCommand.command, templateName])
XCTAssertThrowsSpecific(try subject.run(with: result), ScaffoldCommandError.templateNotFound(templateName))
}
func test_adds_attributes_when_parsing() throws {
// Given
templateLoader.loadTemplateStub = { _ in
Template(description: "test",
attributes: [.required("name")])
}
templatesDirectoryLocator.templateDirectoriesStub = { _ in
[try self.temporaryPath().appending(component: "template")]
}
// When
let result = try subject.parse(with: parser,
arguments: [ScaffoldCommand.command, "template", "--name", "test"])
// Then
XCTAssertEqual(try result.0.get("--name"), "test")
}
func test_fails_when_attributes_not_added() throws {
// Given
templateLoader.loadTemplateStub = { _ in
Template(description: "test")
}
templatesDirectoryLocator.templateDirectoriesStub = { _ in
[try self.temporaryPath().appending(component: "template")]
}
// Then
XCTAssertThrowsError(try subject.parse(with: parser,
arguments: [ScaffoldCommand.command, "template", "--name", "Test"]))
XCTAssertThrowsSpecific(try subject.testRun(templateName: templateName),
ScaffoldServiceError.templateNotFound(templateName))
}
func test_fails_when_required_attribute_not_provided() throws {
@ -98,15 +77,9 @@ final class ScaffoldCommandTests: TuistUnitTestCase {
[try self.temporaryPath().appending(component: "template")]
}
let arguments = [ScaffoldCommand.command, "template"]
_ = try subject.parse(with: parser, arguments: arguments)
// When
let result = try parser.parse(arguments)
// Then
XCTAssertThrowsSpecific(try subject.run(with: result),
ScaffoldCommandError.attributeNotProvided("required"))
XCTAssertThrowsSpecific(try subject.testRun(),
ScaffoldServiceError.attributeNotProvided("required"))
}
func test_optional_attribute_is_taken_from_template() throws {
@ -124,12 +97,8 @@ final class ScaffoldCommandTests: TuistUnitTestCase {
generateAttributes = attributes
}
let arguments = [ScaffoldCommand.command, "template"]
_ = try subject.parse(with: parser, arguments: arguments)
let result = try parser.parse(arguments)
// When
try subject.run(with: result)
try subject.testRun()
// Then
XCTAssertEqual(["optional": "optionalValue"],
@ -152,12 +121,9 @@ final class ScaffoldCommandTests: TuistUnitTestCase {
generateAttributes = attributes
}
let arguments = [ScaffoldCommand.command, "template", "--optional", "optionalValue", "--required", "requiredValue"]
_ = try subject.parse(with: parser, arguments: arguments)
let result = try parser.parse(arguments)
// When
try subject.run(with: result)
try subject.testRun(requiredTemplateOptions: ["required": "requiredValue"],
optionalTemplateOptions: ["optional": "optionalValue"])
// Then
XCTAssertEqual(["optional": "optionalValue",
@ -165,3 +131,15 @@ final class ScaffoldCommandTests: TuistUnitTestCase {
generateAttributes)
}
}
extension ScaffoldService {
func testRun(path: String? = nil,
templateName: String = "template",
requiredTemplateOptions: [String: String] = [:],
optionalTemplateOptions: [String: String] = [:]) throws {
try run(path: path,
templateName: templateName,
requiredTemplateOptions: requiredTemplateOptions,
optionalTemplateOptions: optionalTemplateOptions)
}
}

View File

@ -1,52 +1,41 @@
import Basic
import Foundation
import SPMUtility
import TuistCore
import TuistLoader
import TuistSupport
import XcodeProj
import XCTest
@testable import TuistCoreTesting
@testable import TuistKit
@testable import TuistLoaderTesting
@testable import TuistSupportTesting
final class UpCommandTests: TuistUnitTestCase {
var subject: UpCommand!
var parser: ArgumentParser!
final class UpServiceTests: TuistUnitTestCase {
var subject: UpService!
var setupLoader: MockSetupLoader!
override func setUp() {
super.setUp()
parser = ArgumentParser.test()
setupLoader = MockSetupLoader()
subject = UpCommand(parser: parser,
setupLoader: setupLoader)
subject = UpService(setupLoader: setupLoader)
}
override func tearDown() {
subject = nil
parser = nil
setupLoader = nil
super.tearDown()
}
func test_command() {
XCTAssertEqual(UpCommand.command, "up")
}
func test_overview() {
XCTAssertEqual(UpCommand.overview, "Configures the environment for the project.")
}
func test_run_configures_the_environment() throws {
// given
let temporaryPath = try self.temporaryPath()
let result = try parser.parse([UpCommand.command])
var receivedPaths = [String]()
setupLoader.meetStub = { path in
receivedPaths.append(path.pathString)
}
// when
try subject.run(with: result)
try subject.run(path: temporaryPath.pathString)
// then
XCTAssertEqual(receivedPaths, [temporaryPath.pathString])
@ -56,14 +45,13 @@ final class UpCommandTests: TuistUnitTestCase {
func test_run_uses_the_given_path() throws {
// given
let path = AbsolutePath("/path")
let result = try parser.parse([UpCommand.command, "-p", path.pathString])
var receivedPaths = [String]()
setupLoader.meetStub = { path in
receivedPaths.append(path.pathString)
}
// when
try subject.run(with: result)
try subject.run(path: path.pathString)
// then
XCTAssertEqual(receivedPaths, ["/path"])

View File

@ -10,12 +10,11 @@ private struct TestError: FatalError {
final class ErrorHandlerTests: TuistUnitTestCase {
var subject: ErrorHandler!
var exited: Int32?
override func setUp() {
super.setUp()
subject = ErrorHandler { self.exited = $0 }
subject = ErrorHandler()
}
override func tearDown() {
@ -29,12 +28,6 @@ final class ErrorHandlerTests: TuistUnitTestCase {
XCTAssertPrinterErrorContains(error.description)
}
func test_fatalError_exitsWith1() {
let error = TestError(type: .abort)
subject.fatal(error: error)
XCTAssertEqual(exited, 1)
}
func test_fatalError_prints_whenItsSilent() {
let error = TestError(type: .bugSilent)
subject.fatal(error: error)