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
/// 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)

View File

@ -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

View File

@ -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

View File

@ -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 })

View File

@ -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 {

View File

@ -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() {}

View File

@ -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

View File

@ -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

View File

@ -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)
}
}

View File

@ -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")

View File

@ -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,27 +11,16 @@ 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 {
private let fileHandler: FileHandling = FileHandler.shared
/// 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 }
}

View File

@ -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) ?? []
}
}

View File

@ -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)

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
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"]

View File

@ -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?

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
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",