Add TuistTemplate tests.

This commit is contained in:
Marek Fořt 2020-03-01 15:29:01 +01:00
parent cd5988308c
commit 47a1ff62fd
23 changed files with 635 additions and 93 deletions

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
/// Error that occurs when parsing attributes from input /// Error that occurs when parsing attributes from input
enum ParsingError: Error, CustomStringConvertible { enum ParsingError: Error, CustomStringConvertible, Equatable {
/// Thrown when could not find attribute in input /// Thrown when could not find attribute in input
case attributeNotFound(String) case attributeNotFound(String)
/// Thrown when attributes not provided /// Thrown when attributes not provided
@ -20,13 +20,14 @@ enum ParsingError: Error, CustomStringConvertible {
/// Returns value for `ParsedAttribute` of `name` from user input /// Returns value for `ParsedAttribute` of `name` from user input
/// - Parameters: /// - Parameters:
/// - name: Name of `ParsedAttribute` /// - name: Name of `ParsedAttribute`
/// - arguments: Arguments to get attribute from (usually you should lead this at default)
/// - Returns: Value of `ParsedAttribute` /// - Returns: Value of `ParsedAttribute`
public func getAttribute(for name: String) throws -> String { public func getAttribute(for name: String, arguments: [String] = CommandLine.arguments) throws -> String {
let jsonDecoder = JSONDecoder() let jsonDecoder = JSONDecoder()
guard guard
let attributesIndex = CommandLine.arguments.firstIndex(of: "--attributes"), let attributesIndex = arguments.firstIndex(of: "--attributes") ?? arguments.firstIndex(of: "-a"),
CommandLine.arguments.endIndex > attributesIndex + 1, arguments.endIndex > attributesIndex + 1,
let data = CommandLine.arguments[attributesIndex + 1].data(using: .utf8) let data = arguments[attributesIndex + 1].data(using: .utf8)
else { throw ParsingError.attributesNotProvided } else { throw ParsingError.attributesNotProvided }
let parsedAttributes = try jsonDecoder.decode([ParsedAttribute].self, from: data) let parsedAttributes = try jsonDecoder.decode([ParsedAttribute].self, from: data)

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
/// Parsed attribute from user input /// Parsed attribute from user input
public struct ParsedAttribute: Codable { public struct ParsedAttribute: Codable, Equatable {
public init(name: String, value: String) { public init(name: String, value: String) {
self.name = name self.name = name
self.value = value self.value = value

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
/// Template manifest - used with `tuist scaffold` /// Template manifest - used with `tuist scaffold`
public struct Template: Codable { public struct Template: Codable, Equatable {
/// Description of template /// Description of template
public let description: String public let description: String
/// Attributes to be passed to template /// Attributes to be passed to template
@ -12,19 +12,19 @@ public struct Template: Codable {
public let directories: [String] public let directories: [String]
public init(description: String, public init(description: String,
arguments: [Attribute] = [], attributes: [Attribute] = [],
files: [File] = [], files: [File] = [],
directories: [String] = [], directories: [String] = [],
script: String? = nil) { script: String? = nil) {
self.description = description self.description = description
self.attributes = arguments self.attributes = attributes
self.files = files self.files = files
self.directories = directories self.directories = directories
dumpIfNeeded(self) dumpIfNeeded(self)
} }
/// Enum containing information about how to generate file /// Enum containing information about how to generate file
public enum Contents: Codable { public enum Contents: Codable, Equatable {
/// Static Contents is defined in `Template.swift` and contains a simple `String` /// Static Contents is defined in `Template.swift` and contains a simple `String`
/// Can not contain any additional logic apart from plain `String` from `arguments` /// Can not contain any additional logic apart from plain `String` from `arguments`
case `static`(String) case `static`(String)
@ -66,7 +66,7 @@ public struct Template: Codable {
} }
/// File description for generating /// File description for generating
public struct File: Codable { public struct File: Codable, Equatable {
public let path: String public let path: String
public let contents: Contents public let contents: Contents
@ -77,7 +77,7 @@ public struct Template: Codable {
} }
/// Attribute to be passed to `tuist scaffold` for generating with `Template` /// Attribute to be passed to `tuist scaffold` for generating with `Template`
public enum Attribute: Codable { public enum Attribute: Codable, Equatable {
/// Required attribute with a given name /// Required attribute with a given name
case required(String) case required(String)
/// Optional attribute with a given name and a default value used when attribute not provided by user /// Optional attribute with a given name and a default value used when attribute not provided by user

View File

@ -110,7 +110,7 @@ class InitCommand: NSObject, Command {
let name = try self.name(arguments: arguments, path: path) let name = try self.name(arguments: arguments, path: path)
try verifyDirectoryIsEmpty(path: path) try verifyDirectoryIsEmpty(path: path)
let directories = try templatesDirectoryLocator.templateDirectories() let directories = try templatesDirectoryLocator.templateDirectories(at: FileHandler.shared.currentPath)
if let template = arguments.get(templateArgument) { if let template = arguments.get(templateArgument) {
guard guard
let templateDirectory = directories.first(where: { $0.basename == template }) let templateDirectory = directories.first(where: { $0.basename == template })

View File

@ -83,7 +83,7 @@ class ScaffoldCommand: NSObject, Command {
let path = self.path(arguments: arguments) let path = self.path(arguments: arguments)
try verifyDirectoryIsEmpty(path: path) try verifyDirectoryIsEmpty(path: path)
let directories = try templatesDirectoryLocator.templateDirectories() let directories = try templatesDirectoryLocator.templateDirectories(at: FileHandler.shared.currentPath)
let shouldList = arguments.get(listArgument) ?? false let shouldList = arguments.get(listArgument) ?? false
if shouldList { if shouldList {

View File

@ -2,7 +2,7 @@ import Basic
import Foundation import Foundation
import TuistSupport import TuistSupport
public protocol RootDirectoryLocating { protocol RootDirectoryLocating {
/// Given a path, it finds the root directory by traversing up the hierarchy. /// Given a path, it finds the root directory by traversing up the hierarchy.
/// The root directory is considered the directory that contains a Tuist/ directory or the directory where the /// The root directory is considered the directory that contains a Tuist/ directory or the directory where the
/// git repository is defined if no Tuist/ directory is found. /// git repository is defined if no Tuist/ directory is found.
@ -10,13 +10,13 @@ public protocol RootDirectoryLocating {
func locate(from path: AbsolutePath) -> AbsolutePath? func locate(from path: AbsolutePath) -> AbsolutePath?
} }
public final class RootDirectoryLocator: RootDirectoryLocating { final class RootDirectoryLocator: RootDirectoryLocating {
private let fileHandler: FileHandling = FileHandler.shared private let fileHandler: FileHandling = FileHandler.shared
/// This cache avoids having to traverse the directories hierarchy every time the locate method is called. /// This cache avoids having to traverse the directories hierarchy every time the locate method is called.
fileprivate var cache: [AbsolutePath: AbsolutePath] = [:] fileprivate var cache: [AbsolutePath: AbsolutePath] = [:]
/// Shared instance /// Shared instance
public static var shared: RootDirectoryLocating = RootDirectoryLocator() static var shared: RootDirectoryLocating = RootDirectoryLocator()
/// Constructor /// Constructor
internal init() {} internal init() {}

View File

@ -1,5 +1,5 @@
/// Parsed attribute from user input /// Parsed attribute from user input
public struct ParsedAttribute: Encodable { public struct ParsedAttribute: Encodable, Equatable {
public init(name: String, value: String) { public init(name: String, value: String) {
self.name = name self.name = name
self.value = value self.value = value

View File

@ -7,9 +7,9 @@ public struct Template {
public let directories: [RelativePath] public let directories: [RelativePath]
public init(description: String, public init(description: String,
attributes: [Attribute], attributes: [Attribute] = [],
files: [(path: RelativePath, contents: Contents)], files: [(path: RelativePath, contents: Contents)] = [],
directories: [RelativePath]) { directories: [RelativePath] = []) {
self.description = description self.description = description
self.attributes = attributes self.attributes = attributes
self.files = files self.files = files

View File

@ -3,6 +3,19 @@ import Foundation
import TuistSupport import TuistSupport
import TuistLoader import TuistLoader
enum TemplateGeneratorError: FatalError, Equatable {
var type: ErrorType { .abort }
case attributeNotFound(String)
var description: String {
switch self {
case let .attributeNotFound(attribute):
return "You must provide \(attribute) attribute"
}
}
}
/// Interface for generating content defined in template manifest /// Interface for generating content defined in template manifest
public protocol TemplateGenerating { public protocol TemplateGenerating {
/// Generate files for template manifest at `path` /// Generate files for template manifest at `path`
@ -17,19 +30,16 @@ public protocol TemplateGenerating {
public final class TemplateGenerator: TemplateGenerating { public final class TemplateGenerator: TemplateGenerating {
private let templateLoader: TemplateLoading private let templateLoader: TemplateLoading
private let templateDescriptionHelpersBuilder: TemplateDescriptionHelpersBuilding
public init(templateLoader: TemplateLoading = TemplateLoader(), public init(templateLoader: TemplateLoading = TemplateLoader()) {
templateDescriptionHelpersBuilder: TemplateDescriptionHelpersBuilding = TemplateDescriptionHelpersBuilder()) {
self.templateLoader = templateLoader self.templateLoader = templateLoader
self.templateDescriptionHelpersBuilder = templateDescriptionHelpersBuilder
} }
public func generate(at sourcePath: AbsolutePath, public func generate(at sourcePath: AbsolutePath,
to destinationPath: AbsolutePath, to destinationPath: AbsolutePath,
attributes: [String]) throws { attributes: [String]) throws {
let template = try templateLoader.loadTemplate(at: sourcePath) let template = try templateLoader.loadTemplate(at: sourcePath)
let templateAttributes = getTemplateAttributes(with: attributes, template: template) let templateAttributes = try getTemplateAttributes(with: attributes, template: template)
try generateDirectories(template: template, try generateDirectories(template: template,
templateAttributes: templateAttributes, templateAttributes: templateAttributes,
@ -76,15 +86,17 @@ public final class TemplateGenerator: TemplateGenerating {
} }
} }
private func getTemplateAttributes(with attributes: [String], template: Template) -> [ParsedAttribute] { private func getTemplateAttributes(with attributes: [String], template: Template) throws -> [ParsedAttribute] {
let parsedAttributes = parseAttributes(attributes) let parsedAttributes = parseAttributes(attributes)
return template.attributes.map { return try template.attributes.map {
switch $0 { switch $0 {
case let .optional(name, default: defaultValue): case let .optional(name, default: defaultValue):
let value = parsedAttributes.first(where: { $0.name == name })?.value ?? defaultValue let value = parsedAttributes.first(where: { $0.name == name })?.value ?? defaultValue
return ParsedAttribute(name: name, value: value) return ParsedAttribute(name: name, value: value)
case let .required(name): case let .required(name):
guard let value = parsedAttributes.first(where: { $0.name == name })?.value else { fatalError() } guard
let value = parsedAttributes.first(where: { $0.name == name })?.value
else { throw TemplateGeneratorError.attributeNotFound(name) }
return ParsedAttribute(name: name, value: value) return ParsedAttribute(name: name, value: value)
} }
} }

View File

@ -5,6 +5,21 @@ import TuistSupport
import TemplateDescription import TemplateDescription
import TuistLoader import TuistLoader
public enum TemplateLoaderError: FatalError, Equatable {
public var type: ErrorType { .abort }
case manifestNotFound(AbsolutePath)
case generateFileNotFound(AbsolutePath)
public var description: String {
switch self {
case let .manifestNotFound(manifestPath):
return "Could not find template manifest at \(manifestPath.pathString)"
case let .generateFileNotFound(generateFilePath):
return "Could not find generate file at \(generateFilePath.pathString)"
}
}
}
public protocol TemplateLoading { public protocol TemplateLoading {
/// Load `TuistTemplate.Template` at given `path` /// Load `TuistTemplate.Template` at given `path`
@ -47,7 +62,7 @@ public class TemplateLoader: TemplateLoading {
public func loadTemplate(at path: AbsolutePath) throws -> TuistTemplate.Template { public func loadTemplate(at path: AbsolutePath) throws -> TuistTemplate.Template {
let manifestPath = path.appending(component: "Template.swift") let manifestPath = path.appending(component: "Template.swift")
guard FileHandler.shared.exists(manifestPath) else { guard FileHandler.shared.exists(manifestPath) else {
fatalError() throw TemplateLoaderError.manifestNotFound(manifestPath)
} }
let data = try loadManifestData(at: manifestPath) let data = try loadManifestData(at: manifestPath)
let manifest = try decoder.decode(TemplateDescription.Template.self, from: data) let manifest = try decoder.decode(TemplateDescription.Template.self, from: data)
@ -56,6 +71,9 @@ public class TemplateLoader: TemplateLoading {
} }
public func loadGenerateFile(at path: AbsolutePath, parsedAttributes: [TuistTemplate.ParsedAttribute]) throws -> String { public func loadGenerateFile(at path: AbsolutePath, parsedAttributes: [TuistTemplate.ParsedAttribute]) throws -> String {
guard FileHandler.shared.exists(path) else {
throw TemplateLoaderError.generateFileNotFound(path)
}
var additionalArguments: [String] = [] var additionalArguments: [String] = []
if let attributes = try String(data: encoder.encode(parsedAttributes), encoding: .utf8) { if let attributes = try String(data: encoder.encode(parsedAttributes), encoding: .utf8) {
additionalArguments.append("--attributes") additionalArguments.append("--attributes")

View File

@ -1,7 +1,6 @@
import Basic import Basic
import Foundation import Foundation
import TuistSupport import TuistSupport
import TuistLoader
public protocol TemplatesDirectoryLocating { public protocol TemplatesDirectoryLocating {
/// Returns the path to the tuist built-in templates directory if it exists. /// Returns the path to the tuist built-in templates directory if it exists.
@ -12,27 +11,16 @@ public protocol TemplatesDirectoryLocating {
/// - Returns: Path of templates directory up the three `from` /// - Returns: Path of templates directory up the three `from`
func locate(from path: AbsolutePath) -> AbsolutePath? func locate(from path: AbsolutePath) -> AbsolutePath?
/// - Returns: All available directories with defined templates (custom and built-in) /// - Returns: All available directories with defined templates (custom and built-in)
func templateDirectories() throws -> [AbsolutePath] func templateDirectories(at path: AbsolutePath) throws -> [AbsolutePath]
} }
public final class TemplatesDirectoryLocator: TemplatesDirectoryLocating { public final class TemplatesDirectoryLocator: TemplatesDirectoryLocating {
private let fileHandler: FileHandling = FileHandler.shared private let fileHandler: FileHandling = FileHandler.shared
/// This cache avoids having to traverse the directories hierarchy every time the locate method is called. /// This cache avoids having to traverse the directories hierarchy every time the locate method is called.
private var cache: [AbsolutePath: AbsolutePath] = [:] private var cache: [AbsolutePath: AbsolutePath] = [:]
/// Instance to locate the root directory of the project.
let rootDirectoryLocator: RootDirectoryLocating
/// Default constructor. /// Default constructor.
public convenience init() { public init() { }
self.init(rootDirectoryLocator: RootDirectoryLocator.shared)
}
/// Initializes the locator with its dependencies.
/// - Parameter rootDirectoryLocator: Instance to locate the root directory of the project.
init(rootDirectoryLocator: RootDirectoryLocating) {
self.rootDirectoryLocator = rootDirectoryLocator
}
// MARK: - TemplatesDirectoryLocating // MARK: - TemplatesDirectoryLocating
@ -56,9 +44,7 @@ public final class TemplatesDirectoryLocator: TemplatesDirectoryLocating {
} }
public func locateCustom(at: AbsolutePath) -> AbsolutePath? { public func locateCustom(at: AbsolutePath) -> AbsolutePath? {
guard let rootDirectory = rootDirectoryLocator.locate(from: at) else { return nil } guard let customTemplatesDirectory = locate(from: at) else { return nil }
let customTemplatesDirectory = rootDirectory
.appending(components: Constants.tuistDirectoryName, Constants.templatesDirectoryName)
if !FileHandler.shared.exists(customTemplatesDirectory) { return nil } if !FileHandler.shared.exists(customTemplatesDirectory) { return nil }
return customTemplatesDirectory return customTemplatesDirectory
} }
@ -67,10 +53,10 @@ public final class TemplatesDirectoryLocator: TemplatesDirectoryLocating {
locate(from: path, source: path) locate(from: path, source: path)
} }
public func templateDirectories() throws -> [AbsolutePath] { public func templateDirectories(at path: AbsolutePath) throws -> [AbsolutePath] {
let templatesDirectory = locate() let templatesDirectory = locate()
let templates = try templatesDirectory.map(FileHandler.shared.contentsOfDirectory) ?? [] let templates = try templatesDirectory.map(FileHandler.shared.contentsOfDirectory) ?? []
let customTemplatesDirectory = locateCustom(at: FileHandler.shared.currentPath) let customTemplatesDirectory = locateCustom(at: path)
let customTemplates = try customTemplatesDirectory.map(FileHandler.shared.contentsOfDirectory) ?? [] let customTemplates = try customTemplatesDirectory.map(FileHandler.shared.contentsOfDirectory) ?? []
return (templates + customTemplates).filter { $0.basename != Constants.templateHelpersDirectoryName } return (templates + customTemplates).filter { $0.basename != Constants.templateHelpersDirectoryName }
} }

View File

@ -7,7 +7,7 @@ public final class MockTemplatesDirectoryLocator: TemplatesDirectoryLocating {
public var locateStub: (() -> AbsolutePath?)? public var locateStub: (() -> AbsolutePath?)?
public var locateCustomStub: ((AbsolutePath) -> AbsolutePath?)? public var locateCustomStub: ((AbsolutePath) -> AbsolutePath?)?
public var locateFromStub: ((AbsolutePath) -> AbsolutePath?)? public var locateFromStub: ((AbsolutePath) -> AbsolutePath?)?
public var templateDirectoriesStub: (() throws -> [AbsolutePath])? public var templateDirectoriesStub: ((AbsolutePath) throws -> [AbsolutePath])?
public func locate() -> AbsolutePath? { public func locate() -> AbsolutePath? {
locateStub?() locateStub?()
@ -21,8 +21,8 @@ public final class MockTemplatesDirectoryLocator: TemplatesDirectoryLocating {
locateFromStub?(path) locateFromStub?(path)
} }
public func templateDirectories() throws -> [AbsolutePath] { public func templateDirectories(at path: AbsolutePath) throws -> [AbsolutePath] {
try templateDirectoriesStub?() ?? [] try templateDirectoriesStub?(path) ?? []
} }
} }

View File

@ -1,7 +1,7 @@
import TemplateDescription import TemplateDescription
let nameArgument: Template.Attribute = .required("name") let nameAttribute: Template.Attribute = .required("name")
let platformArgument: Template.Attribute = .optional("platform", default: "iOS") let platformAttribute: Template.Attribute = .optional("platform", default: "iOS")
let setupContent = """ let setupContent = """
import ProjectDescription import ProjectDescription
@ -59,9 +59,9 @@ extension Project {
""" """
let projectsPath = "Projects" let projectsPath = "Projects"
let appPath = projectsPath + "/\(nameArgument)" let appPath = projectsPath + "/\(nameAttribute)"
let kitFrameworkPath = projectsPath + "/\(nameArgument)Kit" let kitFrameworkPath = projectsPath + "/\(nameAttribute)Kit"
let supportFrameworkPath = projectsPath + "/\(nameArgument)Support" let supportFrameworkPath = projectsPath + "/\(nameAttribute)Support"
func directories(for projectPath: String) -> [String] { func directories(for projectPath: String) -> [String] {
[ [
@ -76,10 +76,10 @@ let workspaceContent = """
import ProjectDescription import ProjectDescription
import ProjectDescriptionHelpers import ProjectDescriptionHelpers
let workspace = Workspace(name: "\(nameArgument)", projects: [ let workspace = Workspace(name: "\(nameAttribute)", projects: [
"Projects/\(nameArgument)", "Projects/\(nameAttribute)",
"Projects/\(nameArgument)Kit", "Projects/\(nameAttribute)Kit",
"Projects/\(nameArgument)Support" "Projects/\(nameAttribute)Support"
]) ])
""" """
@ -98,15 +98,15 @@ func testsContent(_ name: String) -> String {
let kitSourceContent = """ let kitSourceContent = """
import Foundation import Foundation
import \(nameArgument)Support import \(nameAttribute)Support
public final class \(nameArgument)Kit {} public final class \(nameAttribute)Kit {}
""" """
let supportSourceContent = """ let supportSourceContent = """
import Foundation import Foundation
public final class \(nameArgument)Support {} public final class \(nameAttribute)Support {}
""" """
let playgroundContent = """ let playgroundContent = """
@ -193,10 +193,10 @@ graph.dot
""" """
let template = Template( let template = Template(
description: "Custom \(nameArgument)", description: "Custom \(nameAttribute)",
arguments: [ attributes: [
nameArgument, nameAttribute,
platformArgument, platformAttribute,
], ],
files: [ files: [
.static(path: "Setup.swift", .static(path: "Setup.swift",
@ -213,23 +213,23 @@ let template = Template(
generateFilePath: "SupportFrameworkProject.swift"), generateFilePath: "SupportFrameworkProject.swift"),
.generated(path: appPath + "/Sources/AppDelegate.swift", .generated(path: appPath + "/Sources/AppDelegate.swift",
generateFilePath: "AppDelegate.swift"), generateFilePath: "AppDelegate.swift"),
.static(path: appPath + "/Tests/\(nameArgument)Tests.swift", .static(path: appPath + "/Tests/\(nameAttribute)Tests.swift",
contents: testsContent("\(nameArgument)")), contents: testsContent("\(nameAttribute)")),
.static(path: kitFrameworkPath + "/Sources/\(nameArgument)Kit.swift", .static(path: kitFrameworkPath + "/Sources/\(nameAttribute)Kit.swift",
contents: kitSourceContent), contents: kitSourceContent),
.static(path: kitFrameworkPath + "/Tests/\(nameArgument)KitTests.swift", .static(path: kitFrameworkPath + "/Tests/\(nameAttribute)KitTests.swift",
contents: testsContent("\(nameArgument)Kit")), contents: testsContent("\(nameAttribute)Kit")),
.static(path: supportFrameworkPath + "/Sources/\(nameArgument)Support.swift", .static(path: supportFrameworkPath + "/Sources/\(nameAttribute)Support.swift",
contents: supportSourceContent), contents: supportSourceContent),
.static(path: supportFrameworkPath + "/Tests/\(nameArgument)SupportTests.swift", .static(path: supportFrameworkPath + "/Tests/\(nameAttribute)SupportTests.swift",
contents: testsContent("\(nameArgument)Support")), contents: testsContent("\(nameAttribute)Support")),
.static(path: kitFrameworkPath + "/Playgrounds/\(nameArgument)Kit.playground" + "/Contents.swift", .static(path: kitFrameworkPath + "/Playgrounds/\(nameAttribute)Kit.playground" + "/Contents.swift",
contents: playgroundContent), contents: playgroundContent),
.generated(path: kitFrameworkPath + "/Playgrounds/\(nameArgument)Kit.playground" + "/contents.xcplayground", .generated(path: kitFrameworkPath + "/Playgrounds/\(nameAttribute)Kit.playground" + "/contents.xcplayground",
generateFilePath: "Playground.swift"), generateFilePath: "Playground.swift"),
.static(path: supportFrameworkPath + "/Playgrounds/\(nameArgument)Support.playground" + "/Contents.swift", .static(path: supportFrameworkPath + "/Playgrounds/\(nameAttribute)Support.playground" + "/Contents.swift",
contents: playgroundContent), contents: playgroundContent),
.generated(path: supportFrameworkPath + "/Playgrounds/\(nameArgument)Support.playground" + "/contents.xcplayground", .generated(path: supportFrameworkPath + "/Playgrounds/\(nameAttribute)Support.playground" + "/contents.xcplayground",
generateFilePath: "Playground.swift"), generateFilePath: "Playground.swift"),
.static(path: "TuistConfig.swift", .static(path: "TuistConfig.swift",
contents: tuistConfigContent), contents: tuistConfigContent),
@ -238,8 +238,8 @@ let template = Template(
], ],
directories: [ directories: [
"Tuist/ProjectDescriptionHelpers", "Tuist/ProjectDescriptionHelpers",
supportFrameworkPath + "/Playgrounds/\(nameArgument)Support.playground", supportFrameworkPath + "/Playgrounds/\(nameAttribute)Support.playground",
kitFrameworkPath + "/Playgrounds/\(nameArgument)Kit.playground", kitFrameworkPath + "/Playgrounds/\(nameAttribute)Kit.playground",
] ]
+ directories(for: appPath) + directories(for: appPath)
+ directories(for: kitFrameworkPath) + directories(for: kitFrameworkPath)

View File

@ -0,0 +1,64 @@
import TuistSupportTesting
import XCTest
@testable import TemplateDescription
class ParsedAttributeTests: XCTestCase {
func test_parsed_attribute_codable() throws {
// Given
let parsedAttribute = ParsedAttribute(name: "name", value: "value name")
// Then
XCTAssertCodable(parsedAttribute)
}
func test_getAttributes_when_attribute_present() throws {
// Given
let parsedAttributes: [ParsedAttribute] = [
ParsedAttribute(name: "a", value: "a value"),
ParsedAttribute(name: "b", value: "b value")
]
let encoder = JSONEncoder()
let parsedAttributesString = String(data: try encoder.encode(parsedAttributes), encoding: .utf8)
let arguments = ["tuist", "something", "--attributes", parsedAttributesString ?? ""]
// Then
XCTAssertEqual(try getAttribute(for: "a", arguments: arguments), "a value")
}
func test_getAttributes_when_attribute_present_and_short_option() throws {
// Given
let parsedAttributes: [ParsedAttribute] = [
ParsedAttribute(name: "a", value: "a value"),
ParsedAttribute(name: "b", value: "b value")
]
let encoder = JSONEncoder()
let parsedAttributesString = String(data: try encoder.encode(parsedAttributes), encoding: .utf8)
let arguments = ["tuist", "something", "-a", parsedAttributesString ?? ""]
// Then
XCTAssertEqual(try getAttribute(for: "a", arguments: arguments), "a value")
}
func test_getAttributes_error_when_attribute_not_present() throws {
// Given
let parsedAttributes: [ParsedAttribute] = [
ParsedAttribute(name: "b", value: "b value")
]
let encoder = JSONEncoder()
let parsedAttributesString = String(data: try encoder.encode(parsedAttributes), encoding: .utf8)
let arguments = ["tuist", "something", "-a", parsedAttributesString ?? ""]
// Then
XCTAssertThrowsSpecific(try getAttribute(for: "a", arguments: arguments), ParsingError.attributeNotFound("a"))
}
func test_getAttributes_error_when_attributes_not_provided() throws {
// Given
let arguments = ["tuist", "something", "--not-attributes"]
// Then
XCTAssertThrowsSpecific(try getAttribute(for: "a", arguments: arguments), ParsingError.attributesNotProvided)
}
}

View File

@ -0,0 +1,28 @@
import TemplateDescription
import TuistSupportTesting
import XCTest
class TemplateTests: XCTestCase {
func test_template_codable() throws {
// Given
let template = Template(
description: "",
attributes: [
.required("name"),
.optional("aName", default: "defaultName"),
.optional("bName", default: ""),
],
files: [
.static(path: "static.swift", contents: "content"),
.generated(path: "generated.swift", generateFilePath: "generate.swift")
],
directories: [
"{{ name }}",
"directory"
])
// Then
XCTAssertCodable(template)
}
}

View File

@ -56,7 +56,7 @@ final class InitCommandTests: TuistUnitTestCase {
// Given // Given
let templateName = "template" let templateName = "template"
let templatePath = try temporaryPath().appending(component: templateName) let templatePath = try temporaryPath().appending(component: templateName)
templatesDirectoryLocator.templateDirectoriesStub = { templatesDirectoryLocator.templateDirectoriesStub = { _ in
[templatePath] [templatePath]
} }
var generateSourcePath: AbsolutePath? var generateSourcePath: AbsolutePath?
@ -81,7 +81,7 @@ final class InitCommandTests: TuistUnitTestCase {
func test_init_default_when_no_template() throws { func test_init_default_when_no_template() throws {
// Given // Given
let defaultTemplatePath = try temporaryPath().appending(component: "default") let defaultTemplatePath = try temporaryPath().appending(component: "default")
templatesDirectoryLocator.templateDirectoriesStub = { templatesDirectoryLocator.templateDirectoriesStub = { _ in
[defaultTemplatePath] [defaultTemplatePath]
} }
let attributes = ["--name", "name", "--platform", "macos"] let attributes = ["--name", "name", "--platform", "macos"]
@ -100,7 +100,7 @@ final class InitCommandTests: TuistUnitTestCase {
func test_init_default_platform() throws { func test_init_default_platform() throws {
let defaultTemplatePath = try temporaryPath().appending(component: "default") let defaultTemplatePath = try temporaryPath().appending(component: "default")
templatesDirectoryLocator.templateDirectoriesStub = { templatesDirectoryLocator.templateDirectoriesStub = { _ in
[defaultTemplatePath] [defaultTemplatePath]
} }
let attributes = ["--name", "name"] let attributes = ["--name", "name"]

View File

@ -60,7 +60,7 @@ final class ScaffoldCommandTests: TuistUnitTestCase {
// Given // Given
let templateName = "template" let templateName = "template"
let templatePath = try temporaryPath().appending(component: templateName) let templatePath = try temporaryPath().appending(component: templateName)
templatesDirectoryLocator.templateDirectoriesStub = { templatesDirectoryLocator.templateDirectoriesStub = { _ in
[templatePath] [templatePath]
} }
var generateSourcePath: AbsolutePath? var generateSourcePath: AbsolutePath?

View File

@ -0,0 +1,94 @@
import Basic
import Foundation
import TuistSupport
import XCTest
@testable import TuistTemplate
@testable import TuistSupportTesting
final class TemplateLoaderTests: TuistTestCase {
var subject: TemplateLoader!
override func setUp() {
super.setUp()
subject = TemplateLoader()
}
override func tearDown() {
super.tearDown()
subject = nil
}
func test_loadTemplate() throws {
// Given
let temporaryPath = try self.temporaryPath()
let content = """
import TemplateDescription
let template = Template(
description: "Template description"
)
"""
let manifestPath = temporaryPath.appending(component: "Template.swift")
try content.write(to: manifestPath.url,
atomically: true,
encoding: .utf8)
// When
let got = try subject.loadTemplate(at: temporaryPath)
// Then
XCTAssertEqual(got.description, "Template description")
}
func test_loadGenerateFile() throws {
let temporaryPath = try self.temporaryPath()
let content = """
import Foundation
import TemplateDescription
let content = Content {
let name = try getAttribute(for: "name")
return "name: \\(name)"
}
"""
let expectedContent = "name: test name"
let generateFilePath = temporaryPath.appending(component: "Generate.swift")
try content.write(to: generateFilePath.url,
atomically: true,
encoding: .utf8)
// When
let got = try subject.loadGenerateFile(at: generateFilePath,
parsedAttributes: [ParsedAttribute(name: "name", value: "test name")])
// Then
XCTAssertEqual(got, expectedContent)
}
func test_load_invalidFormat() throws {
// Given
let temporaryPath = try self.temporaryPath()
let content = """
import ABC
let template
"""
let manifestPath = temporaryPath.appending(component: "Template.swift")
try content.write(to: manifestPath.url,
atomically: true,
encoding: .utf8)
// When / Then
XCTAssertThrowsError(
try subject.loadTemplate(at: temporaryPath)
)
}
func test_load_missingManifest() throws {
let temporaryPath = try self.temporaryPath()
XCTAssertThrowsSpecific(try subject.loadTemplate(at: temporaryPath),
TemplateLoaderError.manifestNotFound(temporaryPath.appending(components: "Template.swift")))
}
}

View File

@ -0,0 +1,106 @@
import Basic
import Foundation
import TuistSupport
import XCTest
@testable import TuistTemplate
@testable import TuistSupportTesting
final class TemplatesDirectoryLocatorIntegrationTests: TuistTestCase {
var subject: TemplatesDirectoryLocator!
override func setUp() {
super.setUp()
subject = TemplatesDirectoryLocator()
}
override func tearDown() {
subject = nil
super.tearDown()
}
func test_locate_when_a_templates_and_git_directory_exists() throws {
// Given
let temporaryDirectory = try temporaryPath()
try createFolders(["this/is/a/very/nested/directory", "this/is/Tuist/Templates", "this/.git"])
// When
let got = subject.locate(from: temporaryDirectory.appending(RelativePath("this/is/a/very/nested/directory")))
// Then
XCTAssertEqual(got, temporaryDirectory.appending(RelativePath("this/is/Tuist/Templates")))
}
func test_locate_when_a_templates_directory_exists() throws {
// Given
let temporaryDirectory = try temporaryPath()
try createFolders(["this/is/a/very/nested/directory", "this/is/Tuist/Templates"])
// When
let got = subject.locate(from: temporaryDirectory.appending(RelativePath("this/is/a/very/nested/directory")))
// Then
XCTAssertEqual(got, temporaryDirectory.appending(RelativePath("this/is/Tuist/Templates")))
}
func test_locate_when_a_git_directory_exists() throws {
// Given
let temporaryDirectory = try temporaryPath()
try createFolders(["this/is/a/very/nested/directory", "this/.git"])
// When
let got = subject.locate(from: temporaryDirectory.appending(RelativePath("this/is/a/very/nested/directory")))
// Then
XCTAssertEqual(got, temporaryDirectory.appending(RelativePath("this/Tuist/Templates")))
}
func test_locate_when_multiple_tuist_directories_exists() throws {
// Given
let temporaryDirectory = try temporaryPath()
try createFolders(["this/is/a/very/nested/Tuist/", "this/is/Tuist/"])
let paths = [
"this/is/a/very/directory",
"this/is/a/very/nested/directory",
]
// When
let got = paths.map {
subject.locate(from: temporaryDirectory.appending(RelativePath($0)))
}
// Then
XCTAssertEqual(got, [
"this/is/Tuist/Templates",
"this/is/a/very/nested/Tuist/Templates",
].map { temporaryDirectory.appending(RelativePath($0)) })
}
func test_locate_when_templates_directory_exist() throws {
// Given
let temporaryDirectory = try temporaryPath()
try createFolders(["this/is/a/very/nested/directory", "this/Templates"])
// When
let got = subject.locate(from: temporaryDirectory.appending(RelativePath("this/is/a/very/nested/directory")))
// Then
XCTAssertEqual(got, temporaryDirectory.appending(RelativePath("this/Templates")))
}
func test_locate_all_templates() throws {
// Given
let temporaryDirectory = try temporaryPath()
try createFolders(["this/is/a/directory", "this/Templates/template_one", "this/Templates/template_two"])
// When
let got = try subject.templateDirectories(at: temporaryDirectory.appending(RelativePath("this/is/a/directory")))
// Then
XCTAssertEqual([
AbsolutePath(#file.replacingOccurrences(of: "file://", with: ""))
.appending(RelativePath("../../../../Templates/default")),
temporaryDirectory.appending(RelativePath("this/Templates/template_one")),
temporaryDirectory.appending(RelativePath("this/Templates/template_two")),
], got)
}
}

View File

@ -0,0 +1,234 @@
import Basic
import Foundation
import TuistSupport
import XCTest
@testable import TuistTemplateTesting
@testable import TuistSupportTesting
@testable import TuistTemplate
final class TemplateGeneratorTests: TuistTestCase {
var subject: TemplateGenerator!
var templateLoader: MockTemplateLoader!
override func setUp() {
super.setUp()
templateLoader = MockTemplateLoader()
subject = TemplateGenerator(templateLoader: templateLoader)
}
override func tearDown() {
super.tearDown()
subject = nil
templateLoader = nil
}
func test_directories_are_generated() throws {
// Given
let directories = [RelativePath("a"), RelativePath("a/b"), RelativePath("c")]
templateLoader.loadTemplateStub = { _ in
Template(description: "",
directories: directories)
}
let destinationPath = try temporaryPath()
let expectedDirectories = directories.map(destinationPath.appending)
// When
try subject.generate(at: try temporaryPath(),
to: destinationPath,
attributes: [])
// Then
XCTAssertTrue(expectedDirectories.allSatisfy(FileHandler.shared.exists))
}
func test_directories_with_attributes() throws {
// Given
let name = "GenName"
let bName = "BName"
let defaultName = "defaultName"
let directories = [RelativePath("{{ name }}"), RelativePath("{{ aName }}"), RelativePath("{{ bName }}")]
templateLoader.loadTemplateStub = { _ in
Template(description: "",
attributes: [
.required("name"),
.optional("aName", default: defaultName),
.optional("bName", default: ""),
],
directories: directories)
}
let destinationPath = try temporaryPath()
let expectedDirectories = [name, defaultName, bName].map(destinationPath.appending)
// When
try subject.generate(at: try temporaryPath(),
to: destinationPath,
attributes: ["--name", name, "--bName", bName])
// Then
XCTAssertTrue(expectedDirectories.allSatisfy(FileHandler.shared.exists))
}
func test_fails_when_required_attribute_not_provided() throws {
// Given
templateLoader.loadTemplateStub = { _ in
Template(description: "",
attributes: [.required("required")]
)
}
// Then
XCTAssertThrowsSpecific(try subject.generate(at: try temporaryPath(),
to: try temporaryPath(),
attributes: []),
TemplateGeneratorError.attributeNotFound("required"))
}
func test_files_are_generated() throws {
// Given
let files: [(path: RelativePath, contents: Template.Contents)] = [
(path: RelativePath("a"), contents: .static("aContent")),
(path: RelativePath("b"), contents: .static("bContent")),
]
templateLoader.loadTemplateStub = { _ in
Template(description: "",
files: files)
}
let destinationPath = try temporaryPath()
let expectedFiles: [(AbsolutePath, String)] = files.compactMap {
let content: String
switch $0.contents {
case let .static(staticContent):
content = staticContent
case .generated:
XCTFail("Unexpected type")
return nil
}
return (destinationPath.appending($0.path), content)
}
// When
try subject.generate(at: try temporaryPath(),
to: destinationPath,
attributes: [])
// Then
try expectedFiles.forEach {
XCTAssertEqual(try FileHandler.shared.readTextFile($0.0), $0.1)
}
}
func test_files_are_generated_with_attributes() throws {
// Given
templateLoader.loadTemplateStub = { _ in
Template(description: "",
attributes: [
.required("name"),
.required("contentName"),
.required("directoryName"),
.required("fileName"),
],
files: [
(path: RelativePath("{{ name }}"), contents: .static("{{ contentName }}")),
(path: RelativePath("{{ directoryName }}/{{ fileName }}"), contents: .static("bContent")),
],
directories: [RelativePath("{{ directoryName }}")])
}
let name = "test name"
let contentName = "test content"
let directoryName = "test directory"
let fileName = "test file"
let destinationPath = try temporaryPath()
let expectedFiles: [(AbsolutePath, String)] = [
(destinationPath.appending(component: name), contentName),
(destinationPath.appending(components: directoryName, fileName), "bContent")
]
// When
try subject.generate(at: try temporaryPath(),
to: destinationPath,
attributes: [
"--name", name,
"--contentName", contentName,
"--directoryName", directoryName,
"--fileName", fileName
])
// Then
try expectedFiles.forEach {
XCTAssertEqual(try FileHandler.shared.readTextFile($0.0), $0.1)
}
}
func test_generated_files() throws {
// Given
let sourcePath = try temporaryPath()
let destinationPath = try temporaryPath()
let name = "test name"
let aContent = "test a content"
let bContent = "test b content"
templateLoader.loadTemplateStub = { _ in
Template(description: "",
attributes: [.required("name")],
files: [
(path: RelativePath("a"), contents: .generated(sourcePath.appending(component: "a"))),
(path: RelativePath("b/{{ name }}"), contents: .generated(sourcePath.appending(components: "b")))
],
directories: [RelativePath("b")])
}
templateLoader.loadGenerateFileStub = { filePath, _ in
if filePath == destinationPath.appending(components: "a") {
return aContent
} else if filePath == destinationPath.appending(components: "b") {
return bContent
} else {
XCTFail("unexpected path \(filePath)")
return ""
}
}
let expectedFiles: [(AbsolutePath, String)] = [
(destinationPath.appending(component: "a"), aContent),
(destinationPath.appending(components: "b", name), bContent)
]
// When
try subject.generate(at: sourcePath,
to: destinationPath,
attributes: ["--name", name])
// Then
try expectedFiles.forEach {
XCTAssertEqual(try FileHandler.shared.readTextFile($0.0), $0.1)
}
}
func test_attributes_passed_to_generated_file() throws {
// Given
let sourcePath = try temporaryPath()
let destinationPath = try temporaryPath()
var parsedAttributes: [ParsedAttribute] = []
templateLoader.loadGenerateFileStub = { filePath, attributes in
if filePath == sourcePath.appending(component: "a") {
parsedAttributes = attributes
} else {
XCTFail("unexpected path \(filePath)")
}
return ""
}
templateLoader.loadTemplateStub = { _ in
Template(description: "",
attributes: [.required("name")],
files: [(path: RelativePath("a"),
contents: .generated(sourcePath.appending(component: "a")))])
}
let expectedParsedAttributes: [ParsedAttribute] = [ParsedAttribute(name: "name", value: "attribute name")]
// When
try subject.generate(at: sourcePath,
to: destinationPath,
attributes: ["--name", "attribute name"])
// Then
XCTAssertEqual(parsedAttributes, expectedParsedAttributes)
}
}

View File

@ -1,19 +1,18 @@
import ProjectDescription
import TemplateDescription import TemplateDescription
let nameArgument: Template.Attribute = .required("name") let nameAttribute: Template.Attribute = .required("name")
let platformArgument: Template.Attribute = .optional("platform", default: "ios") let platformAttribute: Template.Attribute = .optional("platform", default: "ios")
let testContents = """ let testContents = """
// this is test \(nameArgument) content // this is test \(nameAttribute) content
""" """
let template = Template( let template = Template(
description: "Custom \(nameArgument)", description: "Custom \(nameAttribute)",
arguments: [ attributes: [
nameArgument, nameArgument,
platformArgument platformAttribute
], ],
files: [ files: [
.static(path: "custom_dir/custom.swift", .static(path: "custom_dir/custom.swift",