Add TuistTemplate tests.
This commit is contained in:
parent
cd5988308c
commit
47a1ff62fd
|
@ -1,7 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
/// 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
|
||||
case attributeNotFound(String)
|
||||
/// Thrown when attributes not provided
|
||||
|
@ -20,13 +20,14 @@ enum ParsingError: Error, CustomStringConvertible {
|
|||
/// Returns value for `ParsedAttribute` of `name` from user input
|
||||
/// - Parameters:
|
||||
/// - name: Name of `ParsedAttribute`
|
||||
/// - arguments: Arguments to get attribute from (usually you should lead this at default)
|
||||
/// - 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()
|
||||
guard
|
||||
let attributesIndex = CommandLine.arguments.firstIndex(of: "--attributes"),
|
||||
CommandLine.arguments.endIndex > attributesIndex + 1,
|
||||
let data = CommandLine.arguments[attributesIndex + 1].data(using: .utf8)
|
||||
let attributesIndex = arguments.firstIndex(of: "--attributes") ?? arguments.firstIndex(of: "-a"),
|
||||
arguments.endIndex > attributesIndex + 1,
|
||||
let data = arguments[attributesIndex + 1].data(using: .utf8)
|
||||
else { throw ParsingError.attributesNotProvided }
|
||||
|
||||
let parsedAttributes = try jsonDecoder.decode([ParsedAttribute].self, from: data)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
/// Parsed attribute from user input
|
||||
public struct ParsedAttribute: Codable {
|
||||
public struct ParsedAttribute: Codable, Equatable {
|
||||
public init(name: String, value: String) {
|
||||
self.name = name
|
||||
self.value = value
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
/// Template manifest - used with `tuist scaffold`
|
||||
public struct Template: Codable {
|
||||
public struct Template: Codable, Equatable {
|
||||
/// Description of template
|
||||
public let description: String
|
||||
/// Attributes to be passed to template
|
||||
|
@ -12,19 +12,19 @@ public struct Template: Codable {
|
|||
public let directories: [String]
|
||||
|
||||
public init(description: String,
|
||||
arguments: [Attribute] = [],
|
||||
attributes: [Attribute] = [],
|
||||
files: [File] = [],
|
||||
directories: [String] = [],
|
||||
script: String? = nil) {
|
||||
self.description = description
|
||||
self.attributes = arguments
|
||||
self.attributes = attributes
|
||||
self.files = files
|
||||
self.directories = directories
|
||||
dumpIfNeeded(self)
|
||||
}
|
||||
|
||||
/// 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`
|
||||
/// Can not contain any additional logic apart from plain `String` from `arguments`
|
||||
case `static`(String)
|
||||
|
@ -66,7 +66,7 @@ public struct Template: Codable {
|
|||
}
|
||||
|
||||
/// File description for generating
|
||||
public struct File: Codable {
|
||||
public struct File: Codable, Equatable {
|
||||
public let path: String
|
||||
public let contents: Contents
|
||||
|
||||
|
@ -77,7 +77,7 @@ public struct Template: Codable {
|
|||
}
|
||||
|
||||
/// 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
|
||||
case required(String)
|
||||
/// Optional attribute with a given name and a default value used when attribute not provided by user
|
||||
|
|
|
@ -110,7 +110,7 @@ class InitCommand: NSObject, Command {
|
|||
let name = try self.name(arguments: arguments, 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) {
|
||||
guard
|
||||
let templateDirectory = directories.first(where: { $0.basename == template })
|
||||
|
|
|
@ -83,7 +83,7 @@ class ScaffoldCommand: NSObject, Command {
|
|||
let path = self.path(arguments: arguments)
|
||||
try verifyDirectoryIsEmpty(path: path)
|
||||
|
||||
let directories = try templatesDirectoryLocator.templateDirectories()
|
||||
let directories = try templatesDirectoryLocator.templateDirectories(at: FileHandler.shared.currentPath)
|
||||
|
||||
let shouldList = arguments.get(listArgument) ?? false
|
||||
if shouldList {
|
||||
|
|
|
@ -2,7 +2,7 @@ import Basic
|
|||
import Foundation
|
||||
import TuistSupport
|
||||
|
||||
public protocol RootDirectoryLocating {
|
||||
protocol RootDirectoryLocating {
|
||||
/// 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
|
||||
/// git repository is defined if no Tuist/ directory is found.
|
||||
|
@ -10,13 +10,13 @@ public protocol RootDirectoryLocating {
|
|||
func locate(from path: AbsolutePath) -> AbsolutePath?
|
||||
}
|
||||
|
||||
public final class RootDirectoryLocator: RootDirectoryLocating {
|
||||
final class RootDirectoryLocator: RootDirectoryLocating {
|
||||
private let fileHandler: FileHandling = FileHandler.shared
|
||||
/// This cache avoids having to traverse the directories hierarchy every time the locate method is called.
|
||||
fileprivate var cache: [AbsolutePath: AbsolutePath] = [:]
|
||||
|
||||
/// Shared instance
|
||||
public static var shared: RootDirectoryLocating = RootDirectoryLocator()
|
||||
static var shared: RootDirectoryLocating = RootDirectoryLocator()
|
||||
|
||||
/// Constructor
|
||||
internal init() {}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/// Parsed attribute from user input
|
||||
public struct ParsedAttribute: Encodable {
|
||||
public struct ParsedAttribute: Encodable, Equatable {
|
||||
public init(name: String, value: String) {
|
||||
self.name = name
|
||||
self.value = value
|
||||
|
|
|
@ -7,9 +7,9 @@ public struct Template {
|
|||
public let directories: [RelativePath]
|
||||
|
||||
public init(description: String,
|
||||
attributes: [Attribute],
|
||||
files: [(path: RelativePath, contents: Contents)],
|
||||
directories: [RelativePath]) {
|
||||
attributes: [Attribute] = [],
|
||||
files: [(path: RelativePath, contents: Contents)] = [],
|
||||
directories: [RelativePath] = []) {
|
||||
self.description = description
|
||||
self.attributes = attributes
|
||||
self.files = files
|
||||
|
|
|
@ -3,6 +3,19 @@ import Foundation
|
|||
import TuistSupport
|
||||
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
|
||||
public protocol TemplateGenerating {
|
||||
/// Generate files for template manifest at `path`
|
||||
|
@ -17,19 +30,16 @@ public protocol TemplateGenerating {
|
|||
|
||||
public final class TemplateGenerator: TemplateGenerating {
|
||||
private let templateLoader: TemplateLoading
|
||||
private let templateDescriptionHelpersBuilder: TemplateDescriptionHelpersBuilding
|
||||
|
||||
public init(templateLoader: TemplateLoading = TemplateLoader(),
|
||||
templateDescriptionHelpersBuilder: TemplateDescriptionHelpersBuilding = TemplateDescriptionHelpersBuilder()) {
|
||||
public init(templateLoader: TemplateLoading = TemplateLoader()) {
|
||||
self.templateLoader = templateLoader
|
||||
self.templateDescriptionHelpersBuilder = templateDescriptionHelpersBuilder
|
||||
}
|
||||
|
||||
public func generate(at sourcePath: AbsolutePath,
|
||||
to destinationPath: AbsolutePath,
|
||||
attributes: [String]) throws {
|
||||
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,
|
||||
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)
|
||||
return template.attributes.map {
|
||||
return try template.attributes.map {
|
||||
switch $0 {
|
||||
case let .optional(name, default: defaultValue):
|
||||
let value = parsedAttributes.first(where: { $0.name == name })?.value ?? defaultValue
|
||||
return ParsedAttribute(name: name, value: value)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,21 @@ import TuistSupport
|
|||
import TemplateDescription
|
||||
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 {
|
||||
/// Load `TuistTemplate.Template` at given `path`
|
||||
|
@ -47,7 +62,7 @@ public class TemplateLoader: TemplateLoading {
|
|||
public func loadTemplate(at path: AbsolutePath) throws -> TuistTemplate.Template {
|
||||
let manifestPath = path.appending(component: "Template.swift")
|
||||
guard FileHandler.shared.exists(manifestPath) else {
|
||||
fatalError()
|
||||
throw TemplateLoaderError.manifestNotFound(manifestPath)
|
||||
}
|
||||
let data = try loadManifestData(at: manifestPath)
|
||||
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 {
|
||||
guard FileHandler.shared.exists(path) else {
|
||||
throw TemplateLoaderError.generateFileNotFound(path)
|
||||
}
|
||||
var additionalArguments: [String] = []
|
||||
if let attributes = try String(data: encoder.encode(parsedAttributes), encoding: .utf8) {
|
||||
additionalArguments.append("--attributes")
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import TuistSupport
|
||||
import TuistLoader
|
||||
|
||||
public protocol TemplatesDirectoryLocating {
|
||||
/// Returns the path to the tuist built-in templates directory if it exists.
|
||||
|
@ -12,7 +11,7 @@ public protocol TemplatesDirectoryLocating {
|
|||
/// - Returns: Path of templates directory up the three `from`
|
||||
func locate(from path: AbsolutePath) -> AbsolutePath?
|
||||
/// - 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 {
|
||||
|
@ -20,19 +19,8 @@ public final class TemplatesDirectoryLocator: TemplatesDirectoryLocating {
|
|||
/// This cache avoids having to traverse the directories hierarchy every time the locate method is called.
|
||||
private var cache: [AbsolutePath: AbsolutePath] = [:]
|
||||
|
||||
/// Instance to locate the root directory of the project.
|
||||
let rootDirectoryLocator: RootDirectoryLocating
|
||||
|
||||
/// Default constructor.
|
||||
public convenience 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
|
||||
}
|
||||
public init() { }
|
||||
|
||||
// MARK: - TemplatesDirectoryLocating
|
||||
|
||||
|
@ -56,9 +44,7 @@ public final class TemplatesDirectoryLocator: TemplatesDirectoryLocating {
|
|||
}
|
||||
|
||||
public func locateCustom(at: AbsolutePath) -> AbsolutePath? {
|
||||
guard let rootDirectory = rootDirectoryLocator.locate(from: at) else { return nil }
|
||||
let customTemplatesDirectory = rootDirectory
|
||||
.appending(components: Constants.tuistDirectoryName, Constants.templatesDirectoryName)
|
||||
guard let customTemplatesDirectory = locate(from: at) else { return nil }
|
||||
if !FileHandler.shared.exists(customTemplatesDirectory) { return nil }
|
||||
return customTemplatesDirectory
|
||||
}
|
||||
|
@ -67,10 +53,10 @@ public final class TemplatesDirectoryLocator: TemplatesDirectoryLocating {
|
|||
locate(from: path, source: path)
|
||||
}
|
||||
|
||||
public func templateDirectories() throws -> [AbsolutePath] {
|
||||
public func templateDirectories(at path: AbsolutePath) throws -> [AbsolutePath] {
|
||||
let templatesDirectory = locate()
|
||||
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) ?? []
|
||||
return (templates + customTemplates).filter { $0.basename != Constants.templateHelpersDirectoryName }
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ public final class MockTemplatesDirectoryLocator: TemplatesDirectoryLocating {
|
|||
public var locateStub: (() -> AbsolutePath?)?
|
||||
public var locateCustomStub: ((AbsolutePath) -> AbsolutePath?)?
|
||||
public var locateFromStub: ((AbsolutePath) -> AbsolutePath?)?
|
||||
public var templateDirectoriesStub: (() throws -> [AbsolutePath])?
|
||||
public var templateDirectoriesStub: ((AbsolutePath) throws -> [AbsolutePath])?
|
||||
|
||||
public func locate() -> AbsolutePath? {
|
||||
locateStub?()
|
||||
|
@ -21,8 +21,8 @@ public final class MockTemplatesDirectoryLocator: TemplatesDirectoryLocating {
|
|||
locateFromStub?(path)
|
||||
}
|
||||
|
||||
public func templateDirectories() throws -> [AbsolutePath] {
|
||||
try templateDirectoriesStub?() ?? []
|
||||
public func templateDirectories(at path: AbsolutePath) throws -> [AbsolutePath] {
|
||||
try templateDirectoriesStub?(path) ?? []
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import TemplateDescription
|
||||
|
||||
let nameArgument: Template.Attribute = .required("name")
|
||||
let platformArgument: Template.Attribute = .optional("platform", default: "iOS")
|
||||
let nameAttribute: Template.Attribute = .required("name")
|
||||
let platformAttribute: Template.Attribute = .optional("platform", default: "iOS")
|
||||
|
||||
let setupContent = """
|
||||
import ProjectDescription
|
||||
|
@ -59,9 +59,9 @@ extension Project {
|
|||
"""
|
||||
|
||||
let projectsPath = "Projects"
|
||||
let appPath = projectsPath + "/\(nameArgument)"
|
||||
let kitFrameworkPath = projectsPath + "/\(nameArgument)Kit"
|
||||
let supportFrameworkPath = projectsPath + "/\(nameArgument)Support"
|
||||
let appPath = projectsPath + "/\(nameAttribute)"
|
||||
let kitFrameworkPath = projectsPath + "/\(nameAttribute)Kit"
|
||||
let supportFrameworkPath = projectsPath + "/\(nameAttribute)Support"
|
||||
|
||||
func directories(for projectPath: String) -> [String] {
|
||||
[
|
||||
|
@ -76,10 +76,10 @@ let workspaceContent = """
|
|||
import ProjectDescription
|
||||
import ProjectDescriptionHelpers
|
||||
|
||||
let workspace = Workspace(name: "\(nameArgument)", projects: [
|
||||
"Projects/\(nameArgument)",
|
||||
"Projects/\(nameArgument)Kit",
|
||||
"Projects/\(nameArgument)Support"
|
||||
let workspace = Workspace(name: "\(nameAttribute)", projects: [
|
||||
"Projects/\(nameAttribute)",
|
||||
"Projects/\(nameAttribute)Kit",
|
||||
"Projects/\(nameAttribute)Support"
|
||||
])
|
||||
"""
|
||||
|
||||
|
@ -98,15 +98,15 @@ func testsContent(_ name: String) -> String {
|
|||
|
||||
let kitSourceContent = """
|
||||
import Foundation
|
||||
import \(nameArgument)Support
|
||||
import \(nameAttribute)Support
|
||||
|
||||
public final class \(nameArgument)Kit {}
|
||||
public final class \(nameAttribute)Kit {}
|
||||
"""
|
||||
|
||||
let supportSourceContent = """
|
||||
import Foundation
|
||||
|
||||
public final class \(nameArgument)Support {}
|
||||
public final class \(nameAttribute)Support {}
|
||||
"""
|
||||
|
||||
let playgroundContent = """
|
||||
|
@ -193,10 +193,10 @@ graph.dot
|
|||
"""
|
||||
|
||||
let template = Template(
|
||||
description: "Custom \(nameArgument)",
|
||||
arguments: [
|
||||
nameArgument,
|
||||
platformArgument,
|
||||
description: "Custom \(nameAttribute)",
|
||||
attributes: [
|
||||
nameAttribute,
|
||||
platformAttribute,
|
||||
],
|
||||
files: [
|
||||
.static(path: "Setup.swift",
|
||||
|
@ -213,23 +213,23 @@ let template = Template(
|
|||
generateFilePath: "SupportFrameworkProject.swift"),
|
||||
.generated(path: appPath + "/Sources/AppDelegate.swift",
|
||||
generateFilePath: "AppDelegate.swift"),
|
||||
.static(path: appPath + "/Tests/\(nameArgument)Tests.swift",
|
||||
contents: testsContent("\(nameArgument)")),
|
||||
.static(path: kitFrameworkPath + "/Sources/\(nameArgument)Kit.swift",
|
||||
.static(path: appPath + "/Tests/\(nameAttribute)Tests.swift",
|
||||
contents: testsContent("\(nameAttribute)")),
|
||||
.static(path: kitFrameworkPath + "/Sources/\(nameAttribute)Kit.swift",
|
||||
contents: kitSourceContent),
|
||||
.static(path: kitFrameworkPath + "/Tests/\(nameArgument)KitTests.swift",
|
||||
contents: testsContent("\(nameArgument)Kit")),
|
||||
.static(path: supportFrameworkPath + "/Sources/\(nameArgument)Support.swift",
|
||||
.static(path: kitFrameworkPath + "/Tests/\(nameAttribute)KitTests.swift",
|
||||
contents: testsContent("\(nameAttribute)Kit")),
|
||||
.static(path: supportFrameworkPath + "/Sources/\(nameAttribute)Support.swift",
|
||||
contents: supportSourceContent),
|
||||
.static(path: supportFrameworkPath + "/Tests/\(nameArgument)SupportTests.swift",
|
||||
contents: testsContent("\(nameArgument)Support")),
|
||||
.static(path: kitFrameworkPath + "/Playgrounds/\(nameArgument)Kit.playground" + "/Contents.swift",
|
||||
.static(path: supportFrameworkPath + "/Tests/\(nameAttribute)SupportTests.swift",
|
||||
contents: testsContent("\(nameAttribute)Support")),
|
||||
.static(path: kitFrameworkPath + "/Playgrounds/\(nameAttribute)Kit.playground" + "/Contents.swift",
|
||||
contents: playgroundContent),
|
||||
.generated(path: kitFrameworkPath + "/Playgrounds/\(nameArgument)Kit.playground" + "/contents.xcplayground",
|
||||
.generated(path: kitFrameworkPath + "/Playgrounds/\(nameAttribute)Kit.playground" + "/contents.xcplayground",
|
||||
generateFilePath: "Playground.swift"),
|
||||
.static(path: supportFrameworkPath + "/Playgrounds/\(nameArgument)Support.playground" + "/Contents.swift",
|
||||
.static(path: supportFrameworkPath + "/Playgrounds/\(nameAttribute)Support.playground" + "/Contents.swift",
|
||||
contents: playgroundContent),
|
||||
.generated(path: supportFrameworkPath + "/Playgrounds/\(nameArgument)Support.playground" + "/contents.xcplayground",
|
||||
.generated(path: supportFrameworkPath + "/Playgrounds/\(nameAttribute)Support.playground" + "/contents.xcplayground",
|
||||
generateFilePath: "Playground.swift"),
|
||||
.static(path: "TuistConfig.swift",
|
||||
contents: tuistConfigContent),
|
||||
|
@ -238,8 +238,8 @@ let template = Template(
|
|||
],
|
||||
directories: [
|
||||
"Tuist/ProjectDescriptionHelpers",
|
||||
supportFrameworkPath + "/Playgrounds/\(nameArgument)Support.playground",
|
||||
kitFrameworkPath + "/Playgrounds/\(nameArgument)Kit.playground",
|
||||
supportFrameworkPath + "/Playgrounds/\(nameAttribute)Support.playground",
|
||||
kitFrameworkPath + "/Playgrounds/\(nameAttribute)Kit.playground",
|
||||
]
|
||||
+ directories(for: appPath)
|
||||
+ directories(for: kitFrameworkPath)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -56,7 +56,7 @@ final class InitCommandTests: TuistUnitTestCase {
|
|||
// Given
|
||||
let templateName = "template"
|
||||
let templatePath = try temporaryPath().appending(component: templateName)
|
||||
templatesDirectoryLocator.templateDirectoriesStub = {
|
||||
templatesDirectoryLocator.templateDirectoriesStub = { _ in
|
||||
[templatePath]
|
||||
}
|
||||
var generateSourcePath: AbsolutePath?
|
||||
|
@ -81,7 +81,7 @@ final class InitCommandTests: TuistUnitTestCase {
|
|||
func test_init_default_when_no_template() throws {
|
||||
// Given
|
||||
let defaultTemplatePath = try temporaryPath().appending(component: "default")
|
||||
templatesDirectoryLocator.templateDirectoriesStub = {
|
||||
templatesDirectoryLocator.templateDirectoriesStub = { _ in
|
||||
[defaultTemplatePath]
|
||||
}
|
||||
let attributes = ["--name", "name", "--platform", "macos"]
|
||||
|
@ -100,7 +100,7 @@ final class InitCommandTests: TuistUnitTestCase {
|
|||
|
||||
func test_init_default_platform() throws {
|
||||
let defaultTemplatePath = try temporaryPath().appending(component: "default")
|
||||
templatesDirectoryLocator.templateDirectoriesStub = {
|
||||
templatesDirectoryLocator.templateDirectoriesStub = { _ in
|
||||
[defaultTemplatePath]
|
||||
}
|
||||
let attributes = ["--name", "name"]
|
||||
|
|
|
@ -60,7 +60,7 @@ final class ScaffoldCommandTests: TuistUnitTestCase {
|
|||
// Given
|
||||
let templateName = "template"
|
||||
let templatePath = try temporaryPath().appending(component: templateName)
|
||||
templatesDirectoryLocator.templateDirectoriesStub = {
|
||||
templatesDirectoryLocator.templateDirectoriesStub = { _ in
|
||||
[templatePath]
|
||||
}
|
||||
var generateSourcePath: AbsolutePath?
|
||||
|
|
|
@ -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")))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,19 +1,18 @@
|
|||
import ProjectDescription
|
||||
import TemplateDescription
|
||||
|
||||
let nameArgument: Template.Attribute = .required("name")
|
||||
let platformArgument: Template.Attribute = .optional("platform", default: "ios")
|
||||
let nameAttribute: Template.Attribute = .required("name")
|
||||
let platformAttribute: Template.Attribute = .optional("platform", default: "ios")
|
||||
|
||||
let testContents = """
|
||||
// this is test \(nameArgument) content
|
||||
// this is test \(nameAttribute) content
|
||||
|
||||
"""
|
||||
|
||||
let template = Template(
|
||||
description: "Custom \(nameArgument)",
|
||||
arguments: [
|
||||
description: "Custom \(nameAttribute)",
|
||||
attributes: [
|
||||
nameArgument,
|
||||
platformArgument
|
||||
platformAttribute
|
||||
],
|
||||
files: [
|
||||
.static(path: "custom_dir/custom.swift",
|
||||
|
|
Loading…
Reference in New Issue