Migrate info plist generator to a project mapper (#1469)

- The info plist generator is now a project mapper
- This helps keeps project modifications and side effects consistent to some of the others introduced
- Updated `Constants` to create a nested hierarchy for `DerivedDirectory` to allow grouping all related constants
- Updated side effect descriptions such that they are all `CustomStringContvertible` to allow using them in verbose logs

Test Plan:

- Run `tuist generate` within `fixtures/ios_app_with_watchapp2`
- Verify the Info.plist files continue to be generated to the `Derived/InfoPlists` directory
This commit is contained in:
Kas 2020-06-22 06:43:22 +01:00 committed by GitHub
parent e8ecaf6871
commit ac502072bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 400 additions and 332 deletions

View File

@ -12,6 +12,8 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
### Changed
- Use `LD_RUNPATH_SEARCH_PATHS` instead of embedding dynamic frameworks for unit test targets [#1463](https://github.com/tuist/tuist/pull/1463) by [@fortmarek](https://github.com/fortmarek)
- Migrate info plist generator to a project mapper [#1469](https://github.com/tuist/tuist/pull/1469) by [@kwridan](https://github.com/kwridan).
## 1.11.0 - Volare

View File

@ -19,3 +19,9 @@ public struct CommandDescriptor: Equatable {
self.init(command: command)
}
}
extension CommandDescriptor: CustomStringConvertible {
public var description: String {
"execute \(command.joined(separator: " "))"
}
}

View File

@ -0,0 +1,42 @@
import Foundation
import TSCBasic
/// Directory Descriptor
///
/// Describes a folder operation that needs to take place as
/// part of generating a project or workspace.
///
/// - seealso: `SideEffectsDescriptor`
public struct DirectoryDescriptor: Equatable {
public enum State {
case present
case absent
}
/// Path to the directory
public var path: AbsolutePath
/// The desired state of the directory (`.present` creates a fiile, `.absent` deletes a file)
public var state: State
/// Creates a DirectoryDescriptor Descriptor
/// - Parameters:
/// - path: Path to the file
/// - state: The desired state of the file (`.present` creates a fiile, `.absent` deletes a file)
public init(path: AbsolutePath,
state: DirectoryDescriptor.State = .present) {
self.path = path
self.state = state
}
}
extension DirectoryDescriptor: CustomStringConvertible {
public var description: String {
switch state {
case .absent:
return "delete directory \(path.pathString)"
case .present:
return "create directory \(path.pathString)"
}
}
}

View File

@ -35,3 +35,14 @@ public struct FileDescriptor: Equatable {
self.state = state
}
}
extension FileDescriptor: CustomStringConvertible {
public var description: String {
switch state {
case .absent:
return "delete file \(path.pathString)"
case .present:
return "create file \(path.pathString)"
}
}
}

View File

@ -17,6 +17,22 @@ public enum SideEffectDescriptor: Equatable {
/// Create / Remove a file
case file(FileDescriptor)
/// Create / remove a directory
case directory(DirectoryDescriptor)
/// Perform a command
case command(CommandDescriptor)
}
extension SideEffectDescriptor: CustomStringConvertible {
public var description: String {
switch self {
case let .file(fileDescriptor):
return fileDescriptor.description
case let .directory(directoryDescriptor):
return directoryDescriptor.description
case let .command(commandDescriptor):
return commandDescriptor.description
}
}
}

View File

@ -43,15 +43,24 @@ public enum InfoPlist: Equatable {
}
}
// Path to a user defined info.plist file (already exists on disk).
case file(path: AbsolutePath)
// Path to a generated info.plist file (may not exist on disk at the time of project generation).
case generatedFile(path: AbsolutePath)
// User defined dictionary of keys/values for an info.plist file.
case dictionary([String: Value])
// User defined dictionary of keys/values for an info.plist file extending the default set of keys/values
// for the target type.
case extendingDefault(with: [String: Value])
// MARK: - Public
public var path: AbsolutePath? {
switch self {
case let .file(path):
case let .file(path), let .generatedFile(path: path):
return path
default:
return nil

View File

@ -16,11 +16,14 @@ public final class SideEffectDescriptorExecutor: SideEffectDescriptorExecuting {
public func execute(sideEffects: [SideEffectDescriptor]) throws {
for sideEffect in sideEffects {
logger.debug("Side effect: \(sideEffect)")
switch sideEffect {
case let .command(commandDescriptor):
try perform(command: commandDescriptor)
case let .file(fileDescriptor):
try process(file: fileDescriptor)
case let .directory(directoryDescriptor):
try process(directory: directoryDescriptor)
}
}
}
@ -41,6 +44,19 @@ public final class SideEffectDescriptorExecutor: SideEffectDescriptorExecuting {
}
}
private func process(directory: DirectoryDescriptor) throws {
switch directory.state {
case .present:
if !FileHandler.shared.exists(directory.path) {
try FileHandler.shared.createFolder(directory.path)
}
case .absent:
if FileHandler.shared.exists(directory.path) {
try FileHandler.shared.delete(directory.path)
}
}
}
private func perform(command: CommandDescriptor) throws {
try System.shared.run(command.command)
}

View File

@ -1,149 +0,0 @@
import Foundation
import TSCBasic
import TuistCore
import TuistSupport
import XcodeProj
protocol DerivedFileGenerating {
/// Generates the derived files that are associated to the given project.
///
/// - Parameters:
/// - graph: The dependencies graph.
/// - project: Project whose derived files will be generated.
/// - sourceRootPath: Path to the directory in which the Xcode project will be generated.
/// - Throws: An error if the generation of the derived files errors.
/// - Returns: A project that might have got mutated after the generation of derived files, and a
/// function to be called after the project generation to delete the derived files that are not necessary anymore.
func generate(graph: Graph, project: Project, sourceRootPath: AbsolutePath) throws -> (Project, [SideEffectDescriptor])
}
final class DerivedFileGenerator: DerivedFileGenerating {
typealias ProjectTransformation = (project: Project, sideEffects: [SideEffectDescriptor])
fileprivate static let infoPlistsFolderName = "InfoPlists"
/// Info.plist content provider.
let infoPlistContentProvider: InfoPlistContentProviding
/// Initializes the generator with its attributes.
///
/// - Parameters:
/// - infoPlistContentProvider: Info.plist content provider.
init(infoPlistContentProvider: InfoPlistContentProviding = InfoPlistContentProvider()) {
self.infoPlistContentProvider = infoPlistContentProvider
}
func generate(graph: Graph, project: Project, sourceRootPath: AbsolutePath) throws -> (Project, [SideEffectDescriptor]) {
let transformation = try generateInfoPlists(graph: graph, project: project, sourceRootPath: sourceRootPath)
return (transformation.project, transformation.sideEffects)
}
/// Genreates the Info.plist files.
///
/// - Parameters:
/// - graph: The dependencies graph.
/// - project: Project that contains the targets whose Info.plist files will be generated.
/// - sourceRootPath: Path to the directory in which the project is getting generated.
/// - Returns: A set with paths to the Info.plist files that are no longer necessary and therefore need to be removed.
/// - Throws: An error if the encoding of the Info.plist content fails.
func generateInfoPlists(graph: Graph,
project: Project,
sourceRootPath: AbsolutePath) throws -> ProjectTransformation {
let targetsWithGeneratableInfoPlists = project.targets.filter {
if let infoPlist = $0.infoPlist, case InfoPlist.file = infoPlist {
return false
}
return true
}
// Getting the Info.plist files that need to be deleted
let glob = "\(Constants.derivedFolderName)/\(DerivedFileGenerator.infoPlistsFolderName)/*.plist"
let existing = FileHandler.shared.glob(sourceRootPath, glob: glob)
let new: [AbsolutePath] = targetsWithGeneratableInfoPlists.map {
DerivedFileGenerator.infoPlistPath(target: $0, sourceRootPath: sourceRootPath)
}
let toDelete = Set(existing).subtracting(new)
let deletions = toDelete.map {
SideEffectDescriptor.file(FileDescriptor(path: $0, state: .absent))
}
// Generate the Info.plist
let transformation = try project.targets.map { (target) -> (Target, [SideEffectDescriptor]) in
guard targetsWithGeneratableInfoPlists.contains(target),
let infoPlist = target.infoPlist else {
return (target, [])
}
guard let dictionary = infoPlistDictionary(infoPlist: infoPlist,
project: project,
target: target,
graph: graph) else {
return (target, [])
}
let path = DerivedFileGenerator.infoPlistPath(target: target, sourceRootPath: sourceRootPath)
let data = try PropertyListSerialization.data(fromPropertyList: dictionary,
format: .xml,
options: 0)
let sideEffect = SideEffectDescriptor.file(FileDescriptor(path: path, contents: data))
// Override the Info.plist value to point to te generated one
return (target.with(infoPlist: InfoPlist.file(path: path)), [sideEffect])
}
return (project: project.with(targets: transformation.map { $0.0 }),
sideEffects: deletions + transformation.flatMap { $0.1 })
}
private func infoPlistDictionary(infoPlist: InfoPlist,
project: Project,
target: Target,
graph: Graph) -> [String: Any]? {
switch infoPlist {
case let .dictionary(content):
return content.mapValues { $0.value }
case let .extendingDefault(extended):
if let content = infoPlistContentProvider.content(graph: graph,
project: project,
target: target,
extendedWith: extended) {
return content
}
return nil
default:
return nil
}
}
/// Returns the path to the directory that contains all the derived files.
///
/// - Parameter sourceRootPath: Directory where the project will be generated.
/// - Returns: Path to the directory that contains all the derived files.
static func path(sourceRootPath: AbsolutePath) -> AbsolutePath {
sourceRootPath
.appending(component: Constants.derivedFolderName)
}
/// Returns the path to the directory where all generated Info.plist files will be.
///
/// - Parameter sourceRootPath: Directory where the Xcode project gets genreated.
/// - Returns: The path to the directory where all the Info.plist files will be generated.
static func infoPlistsPath(sourceRootPath: AbsolutePath) -> AbsolutePath {
path(sourceRootPath: sourceRootPath)
.appending(component: DerivedFileGenerator.infoPlistsFolderName)
}
/// Returns the path where the derived Info.plist is generated.
///
/// - Parameters:
/// - target: The target the InfoPlist belongs to.
/// - sourceRootPath: The directory where the Xcode project will be generated.
/// - Returns: The path where the derived Info.plist is generated.
static func infoPlistPath(target: Target, sourceRootPath: AbsolutePath) -> AbsolutePath {
infoPlistsPath(sourceRootPath: sourceRootPath)
.appending(component: "\(target.name).plist")
}
}

View File

@ -8,12 +8,11 @@ protocol InfoPlistContentProviding {
/// and product, and extends them with the values provided by the user.
///
/// - Parameters:
/// - graph: The dependencies graph.
/// - project: The project that hosts the target for which the Info.plist content will be returned
/// - target: Target whose Info.plist content will be returned.
/// - extendedWith: Values provided by the user to extend the default ones.
/// - Returns: Content to generate the Info.plist file.
func content(graph: Graph, project: Project, target: Target, extendedWith: [String: InfoPlist.Value]) -> [String: Any]?
func content(project: Project, target: Target, extendedWith: [String: InfoPlist.Value]) -> [String: Any]?
}
final class InfoPlistContentProvider: InfoPlistContentProviding {
@ -22,12 +21,11 @@ final class InfoPlistContentProvider: InfoPlistContentProviding {
/// and product, and extends them with the values provided by the user.
///
/// - Parameters:
/// - graph: The dependencies graph.
/// - project: The project that hosts the target for which the Info.plist content will be returned
/// - target: Target whose Info.plist content will be returned.
/// - extendedWith: Values provided by the user to extend the default ones.
/// - Returns: Content to generate the Info.plist file.
func content(graph: Graph, project: Project, target: Target, extendedWith: [String: InfoPlist.Value]) -> [String: Any]? {
func content(project: Project, target: Target, extendedWith: [String: InfoPlist.Value]) -> [String: Any]? {
if target.product == .staticLibrary || target.product == .dynamicLibrary {
return nil
}
@ -57,16 +55,16 @@ final class InfoPlistContentProvider: InfoPlistContentProviding {
// watchOS app
if target.product == .watch2App, target.platform == .watchOS {
let host = graph.hostTargetNodeFor(path: project.path, name: target.name)
let host = hostTarget(for: target, in: project)
extend(&content, with: watchosApp(name: target.name,
hostAppBundleId: host?.target.bundleId))
hostAppBundleId: host?.bundleId))
}
// watchOS app extension
if target.product == .watch2Extension, target.platform == .watchOS {
let host = graph.hostTargetNodeFor(path: project.path, name: target.name)
let host = hostTarget(for: target, in: project)
extend(&content, with: watchosAppExtension(name: target.name,
hostAppBundleId: host?.target.bundleId))
hostAppBundleId: host?.bundleId))
}
extend(&content, with: extendedWith.unwrappingValues())
@ -223,4 +221,10 @@ final class InfoPlistContentProvider: InfoPlistContentProviding {
fileprivate func extend(_ base: inout [String: Any], with: [String: Any]) {
with.forEach { base[$0.key] = $0.value }
}
private func hostTarget(for target: Target, in project: Project) -> Target? {
project.targets.first {
$0.dependencies.contains(.target(name: target.name))
}
}
}

View File

@ -47,9 +47,6 @@ final class ProjectGenerator: ProjectGenerating {
/// Generator for the project schemes.
let schemesGenerator: SchemesGenerating
/// Generator for the project derived files.
let derivedFileGenerator: DerivedFileGenerating
// MARK: - Init
/// Initializes the project generator with its attributes.
@ -58,15 +55,12 @@ final class ProjectGenerator: ProjectGenerating {
/// - targetGenerator: Generator for the project targets.
/// - configGenerator: Generator for the project configuration.
/// - schemesGenerator: Generator for the project schemes.
/// - derivedFileGenerator: Generator for the project derived files.
init(targetGenerator: TargetGenerating = TargetGenerator(),
configGenerator: ConfigGenerating = ConfigGenerator(),
schemesGenerator: SchemesGenerating = SchemesGenerator(),
derivedFileGenerator: DerivedFileGenerating = DerivedFileGenerator()) {
schemesGenerator: SchemesGenerating = SchemesGenerator()) {
self.targetGenerator = targetGenerator
self.configGenerator = configGenerator
self.schemesGenerator = schemesGenerator
self.derivedFileGenerator = derivedFileGenerator
}
// MARK: - ProjectGenerating
@ -84,10 +78,6 @@ final class ProjectGenerator: ProjectGenerating {
// If the xcodeproj path is not given, we generate it under the source root path.
let xcodeprojPath = xcodeprojPath ?? sourceRootPath.appending(component: "\(project.fileName).xcodeproj")
// Derived files
// TODO: experiment with moving this outside the project generator to avoid needing to mutate the project
let (project, sideEffects) = try derivedFileGenerator.generate(graph: graph, project: project, sourceRootPath: sourceRootPath)
let workspaceData = XCWorkspaceData(children: [])
let workspace = XCWorkspace(data: workspaceData)
let projectConstants = try determineProjectConstants(graph: graph)
@ -137,7 +127,7 @@ final class ProjectGenerator: ProjectGenerating {
xcodeprojPath: xcodeprojPath,
xcodeProj: xcodeProj,
schemeDescriptors: schemes,
sideEffectDescriptors: sideEffects)
sideEffectDescriptors: [])
}
// MARK: - Fileprivate

View File

@ -138,7 +138,9 @@ class TargetLinter: TargetLinting {
private func lintInfoplistExists(target: Target) -> [LintingIssue] {
var issues: [LintingIssue] = []
if let infoPlist = target.infoPlist, let path = infoPlist.path, !FileHandler.shared.exists(path) {
if let infoPlist = target.infoPlist,
case let InfoPlist.file(path: path) = infoPlist,
!FileHandler.shared.exists(path) {
issues.append(LintingIssue(reason: "Info.plist file not found at path \(infoPlist.path!.pathString)", severity: .error))
}
return issues

View File

@ -0,0 +1,24 @@
import Foundation
import TuistCore
import TuistSupport
/// A project mapper that returns side effects to delete the derived directory.
public final class DeleteDerivedDirectoryProjectMapper: ProjectMapping {
private let derivedDirectoryName: String
public init(derivedDirectoryName: String = Constants.DerivedDirectory.name) {
self.derivedDirectoryName = derivedDirectoryName
}
// MARK: - ProjectMapping
public func map(project: Project) throws -> (Project, [SideEffectDescriptor]) {
logger.debug("Determining the /Derived directories that should be deleted within \(project.path)")
let derivedDirectoryPath = project.path.appending(component: derivedDirectoryName)
let directoryDescriptor = DirectoryDescriptor(path: derivedDirectoryPath, state: .absent)
return (project, [
.directory(directoryDescriptor),
])
}
}

View File

@ -0,0 +1,87 @@
import Foundation
import TSCBasic
import TuistCore
import TuistSupport
import XcodeProj
/// A project mapper that generates derived Info.plist files for targets that define it as a dictonary.
public final class GenerateInfoPlistProjectMapper: ProjectMapping {
private let infoPlistContentProvider: InfoPlistContentProviding
private let derivedDirectoryName: String
private let infoPlistsDirectoryName: String
public convenience init(derivedDirectoryName: String = Constants.DerivedDirectory.name,
infoPlistsDirectoryName: String = Constants.DerivedDirectory.infoPlists) {
self.init(infoPlistContentProvider: InfoPlistContentProvider(),
derivedDirectoryName: derivedDirectoryName,
infoPlistsDirectoryName: infoPlistsDirectoryName)
}
init(infoPlistContentProvider: InfoPlistContentProviding,
derivedDirectoryName: String,
infoPlistsDirectoryName: String) {
self.infoPlistContentProvider = infoPlistContentProvider
self.derivedDirectoryName = derivedDirectoryName
self.infoPlistsDirectoryName = infoPlistsDirectoryName
}
// MARK: - ProjectMapping
public func map(project: Project) throws -> (Project, [SideEffectDescriptor]) {
var results = (targets: [Target](), sideEffects: [SideEffectDescriptor]())
results = try project.targets.reduce(into: results) { results, target in
let (updatedTarget, sideEffects) = try map(target: target, project: project)
results.targets.append(updatedTarget)
results.sideEffects.append(contentsOf: sideEffects)
}
return (project.with(targets: results.targets), results.sideEffects)
}
// MARK: - Private
private func map(target: Target, project: Project) throws -> (Target, [SideEffectDescriptor]) {
// There's nothing to do
guard let infoPlist = target.infoPlist else {
return (target, [])
}
// Get the Info.plist that needs to be generated
guard let dictionary = infoPlistDictionary(infoPlist: infoPlist,
project: project,
target: target) else {
return (target, [])
}
let data = try PropertyListSerialization.data(fromPropertyList: dictionary,
format: .xml,
options: 0)
let infoPlistPath = project.path
.appending(component: derivedDirectoryName)
.appending(component: infoPlistsDirectoryName)
.appending(component: "\(target.name).plist")
let sideEffect = SideEffectDescriptor.file(FileDescriptor(path: infoPlistPath, contents: data))
let newTarget = target.with(infoPlist: InfoPlist.generatedFile(path: infoPlistPath))
return (newTarget, [sideEffect])
}
private func infoPlistDictionary(infoPlist: InfoPlist,
project: Project,
target: Target) -> [String: Any]? {
switch infoPlist {
case let .dictionary(content):
return content.mapValues { $0.value }
case let .extendingDefault(extended):
if let content = infoPlistContentProvider.content(project: project,
target: target,
extendedWith: extended) {
return content
}
return nil
default:
return nil
}
}
}

View File

@ -19,6 +19,11 @@ class ProjectMapperProvider: ProjectMapperProviding {
mappers.append(AutogeneratedSchemesProjectMapper())
}
// Info Plist
mappers.append(DeleteDerivedDirectoryProjectMapper())
mappers.append(GenerateInfoPlistProjectMapper())
// Signing
mappers.append(SigningMapper())
return SequentialProjectMapper(mappers: mappers)

View File

@ -37,8 +37,8 @@ public class SigningMapper: ProjectMapping {
try signingCipher.decryptSigning(at: path, keepFiles: true)
defer { try? signingCipher.encryptSigning(at: path, keepFiles: false) }
let derivedDirectory = project.path.appending(component: Constants.derivedFolderName)
let keychainPath = derivedDirectory.appending(component: Constants.signingKeychain)
let derivedDirectory = project.path.appending(component: Constants.DerivedDirectory.name)
let keychainPath = derivedDirectory.appending(component: Constants.DerivedDirectory.signingKeychain)
let (certificates, provisioningProfiles) = try signingMatcher.match(from: project.path)

View File

@ -48,10 +48,10 @@ public final class SigningInteractor: SigningInteracting {
let entryPath = graph.entryPath
guard
let signingDirectory = try signingFilesLocator.locateSigningDirectory(from: entryPath),
let derivedDirectory = rootDirectoryLocator.locate(from: entryPath)?.appending(component: Constants.derivedFolderName)
let derivedDirectory = rootDirectoryLocator.locate(from: entryPath)?.appending(component: Constants.DerivedDirectory.name)
else { return }
let keychainPath = derivedDirectory.appending(component: Constants.signingKeychain)
let keychainPath = derivedDirectory.appending(component: Constants.DerivedDirectory.signingKeychain)
let masterKey = try signingCipher.readMasterKey(at: signingDirectory)
try FileHandler.shared.createFolder(derivedDirectory)

View File

@ -9,10 +9,10 @@ public struct Constants {
public static let bundleName: String = "tuist.zip"
public static let trueValues: [String] = ["1", "true", "TRUE", "yes", "YES"]
public static let tuistDirectoryName: String = "Tuist"
public static let derivedFolderName = "Derived"
public static let helpersDirectoryName: String = "ProjectDescriptionHelpers"
public static let signingDirectoryName: String = "Signing"
public static let signingKeychain = "signing.keychain"
public static let masterKey = "master.key"
public static let encryptedExtension = "encrypted"
public static let templatesDirectoryName: String = "Templates"
@ -20,6 +20,12 @@ public struct Constants {
public static let joinSlackURL: String = "https://slack.tuist.io/"
public static let tuistGeneratedFileName = ".tuist-generated"
public struct DerivedDirectory {
public static let name = "Derived"
public static let infoPlists = "InfoPlists"
public static let signingKeychain = "signing.keychain"
}
public struct EnvironmentVariables {
public static let colouredOutput = "TUIST_COLOURED_OUTPUT"
public static let versionsDirectory = "TUIST_VERSIONS_DIRECTORY"

View File

@ -1,117 +0,0 @@
import Foundation
import TSCBasic
import TuistCore
import TuistCoreTesting
import TuistSupport
import XcodeProj
import XCTest
@testable import TuistGenerator
@testable import TuistSupportTesting
final class DerivedFileGeneratorTests: TuistUnitTestCase {
var infoPlistContentProvider: MockInfoPlistContentProvider!
var subject: DerivedFileGenerator!
override func setUp() {
super.setUp()
infoPlistContentProvider = MockInfoPlistContentProvider()
subject = DerivedFileGenerator(infoPlistContentProvider: infoPlistContentProvider)
}
override func tearDown() {
infoPlistContentProvider = nil
subject = nil
super.tearDown()
}
func test_generate_generatesTheInfoPlistFiles_whenDictionaryInfoPlist() throws {
// Given
let temporaryPath = try self.temporaryPath()
let target = Target.test(name: "Target", infoPlist: InfoPlist.dictionary(["a": "b"]))
let project = Project.test(name: "App", targets: [target])
// When
let (_, sideEffects) = try subject.generate(graph: Graph.test(), project: project, sourceRootPath: temporaryPath)
// Then
let file = try XCTUnwrap(sideEffects.files.first)
let contents = try XCTUnwrap(file.contents)
let content = try PropertyListSerialization.propertyList(from: contents, options: [], format: nil)
XCTAssertTrue(NSDictionary(dictionary: (content as? [String: Any]) ?? [:])
.isEqual(to: ["a": "b"]))
}
func test_generate_generatesTheInfoPlistFiles_whenExtendingDefault() throws {
// Given
let temporaryPath = try self.temporaryPath()
let target = Target.test(name: "Target", infoPlist: InfoPlist.extendingDefault(with: ["a": "b"]))
let project = Project.test(name: "App", targets: [target])
let infoPlistsPath = DerivedFileGenerator.infoPlistsPath(sourceRootPath: temporaryPath)
let path = infoPlistsPath.appending(component: "Target.plist")
infoPlistContentProvider.contentStub = ["test": "value"]
// When
let (_, sideEffects) = try subject.generate(graph: Graph.test(), project: project, sourceRootPath: temporaryPath)
// Then
let file = try XCTUnwrap(sideEffects.files.first)
XCTAssertEqual(file.path, path)
XCTAssertTrue(infoPlistContentProvider.contentArgs.first?.target == target)
XCTAssertTrue(infoPlistContentProvider.contentArgs.first?.extendedWith["a"] == "b")
let writtenData = try XCTUnwrap(file.contents)
let content = try PropertyListSerialization.propertyList(from: writtenData, options: [], format: nil)
XCTAssertTrue(NSDictionary(dictionary: (content as? [String: Any]) ?? [:])
.isEqual(to: ["test": "value"]))
}
func test_generate_returnsABlockToDeleteUnnecessaryInfoPlistFiles() throws {
// Given
let temporaryPath = try self.temporaryPath()
let target = Target.test(infoPlist: InfoPlist.dictionary(["a": "b"]))
let project = Project.test(name: "App", targets: [target])
let infoPlistsPath = DerivedFileGenerator.infoPlistsPath(sourceRootPath: temporaryPath)
try FileHandler.shared.createFolder(infoPlistsPath)
let oldPlistPath = infoPlistsPath.appending(component: "Old.plist")
try FileHandler.shared.touch(oldPlistPath)
// When
let (_, sideEffects) = try subject.generate(graph: Graph.test(),
project: project,
sourceRootPath: temporaryPath)
// Then
let file = try XCTUnwrap(sideEffects.deletions.first)
XCTAssertEqual(file, oldPlistPath)
}
}
private extension Array where Element == SideEffectDescriptor {
var files: [FileDescriptor] {
compactMap {
switch $0 {
case let .file(file):
return file
default:
return nil
}
}
}
var deletions: [AbsolutePath] {
compactMap {
switch $0 {
case let .file(file):
switch file.state {
case .absent:
return file.path
default:
return nil
}
default:
return nil
}
}
}
}

View File

@ -17,8 +17,7 @@ final class InfoPlistContentProviderTests: XCTestCase {
let target = Target.test(platform: .iOS, product: .app)
// When
let got = subject.content(graph: Graph.test(),
project: .empty(),
let got = subject.content(project: .empty(),
target: target,
extendedWith: ["ExtraAttribute": "Value"])
@ -54,8 +53,7 @@ final class InfoPlistContentProviderTests: XCTestCase {
let target = Target.test(platform: .macOS, product: .app)
// When
let got = subject.content(graph: Graph.test(),
project: .empty(),
let got = subject.content(project: .empty(),
target: target,
extendedWith: ["ExtraAttribute": "Value"])
@ -83,8 +81,7 @@ final class InfoPlistContentProviderTests: XCTestCase {
let target = Target.test(platform: .macOS, product: .framework)
// When
let got = subject.content(graph: Graph.test(),
project: .empty(),
let got = subject.content(project: .empty(),
target: target,
extendedWith: ["ExtraAttribute": "Value"])
@ -108,8 +105,7 @@ final class InfoPlistContentProviderTests: XCTestCase {
let target = Target.test(platform: .macOS, product: .staticLibrary)
// When
let got = subject.content(graph: Graph.test(),
project: .empty(),
let got = subject.content(project: .empty(),
target: target,
extendedWith: ["ExtraAttribute": "Value"])
@ -122,8 +118,7 @@ final class InfoPlistContentProviderTests: XCTestCase {
let target = Target.test(platform: .macOS, product: .dynamicLibrary)
// When
let got = subject.content(graph: Graph.test(),
project: .empty(),
let got = subject.content(project: .empty(),
target: target,
extendedWith: ["ExtraAttribute": "Value"])
@ -136,8 +131,7 @@ final class InfoPlistContentProviderTests: XCTestCase {
let target = Target.test(platform: .iOS, product: .bundle)
// When
let got = subject.content(graph: Graph.test(),
project: .empty(),
let got = subject.content(project: .empty(),
target: target,
extendedWith: ["ExtraAttribute": "Value"])
@ -156,8 +150,7 @@ final class InfoPlistContentProviderTests: XCTestCase {
func test_contentPackageType() {
func content(for target: Target) -> [String: Any]? {
subject.content(graph: Graph.test(),
project: .empty(),
subject.content(project: .empty(),
target: target,
extendedWith: [:])
}
@ -178,19 +171,17 @@ final class InfoPlistContentProviderTests: XCTestCase {
product: .watch2App)
let app = Target.test(platform: .iOS,
product: .app,
bundleId: "io.tuist.my.app.id")
bundleId: "io.tuist.my.app.id",
dependencies: [
.target(name: watchApp.name),
])
let project = Project.test(targets: [
app,
watchApp,
])
let graph = Graph.create(project: project, dependencies: [
(target: app, dependencies: [watchApp]),
(target: watchApp, dependencies: []),
])
// When
let got = subject.content(graph: graph,
project: project,
let got = subject.content(project: project,
target: watchApp,
extendedWith: [
"ExtraAttribute": "Value",
@ -225,19 +216,17 @@ final class InfoPlistContentProviderTests: XCTestCase {
product: .watch2Extension)
let watchApp = Target.test(platform: .watchOS,
product: .watch2App,
bundleId: "io.tuist.my.app.id.mywatchapp")
bundleId: "io.tuist.my.app.id.mywatchapp",
dependencies: [
.target(name: watchAppExtension.name),
])
let project = Project.test(targets: [
watchApp,
watchAppExtension,
])
let graph = Graph.create(project: project, dependencies: [
(target: watchApp, dependencies: [watchAppExtension]),
(target: watchAppExtension, dependencies: []),
])
// When
let got = subject.content(graph: graph,
project: project,
let got = subject.content(project: project,
target: watchAppExtension,
extendedWith: [
"ExtraAttribute": "Value",

View File

@ -4,11 +4,11 @@ import TuistCoreTesting
@testable import TuistGenerator
final class MockInfoPlistContentProvider: InfoPlistContentProviding {
var contentArgs: [(graph: Graph, project: Project, target: Target, extendedWith: [String: InfoPlist.Value])] = []
var contentArgs: [(project: Project, target: Target, extendedWith: [String: InfoPlist.Value])] = []
var contentStub: [String: Any]?
func content(graph: Graph, project: Project, target: Target, extendedWith: [String: InfoPlist.Value]) -> [String: Any]? {
contentArgs.append((graph: graph, project: project, target: target, extendedWith: extendedWith))
func content(project: Project, target: Target, extendedWith: [String: InfoPlist.Value]) -> [String: Any]? {
contentArgs.append((project: project, target: target, extendedWith: extendedWith))
return contentStub ?? [:]
}
}

View File

@ -0,0 +1,35 @@
import Foundation
import TSCBasic
import TuistCore
import TuistSupport
import XCTest
@testable import TuistCoreTesting
@testable import TuistGenerator
@testable import TuistSupportTesting
public final class DeleteDerivedDirectoryProjectMapperTests: TuistUnitTestCase {
var subject: DeleteDerivedDirectoryProjectMapper!
override public func setUp() {
super.setUp()
subject = DeleteDerivedDirectoryProjectMapper()
}
override public func tearDown() {
super.tearDown()
subject = nil
}
func test_map_returns_sideEffectsToDeleteDerivedDirectories() throws {
// Given
let projectA = Project.test(path: "/projectA")
// When
let (_, sideEffects) = try subject.map(project: projectA)
// Then
XCTAssertEqual(sideEffects, [
.directory(.init(path: projectA.path.appending(component: Constants.DerivedDirectory.name), state: .absent)),
])
}
}

View File

@ -0,0 +1,87 @@
import Foundation
import TSCBasic
import TuistCore
import TuistSupport
import XCTest
@testable import TuistCoreTesting
@testable import TuistGenerator
@testable import TuistSupportTesting
public final class GenerateInfoPlistProjectMapperTests: TuistUnitTestCase {
var infoPlistContentProvider: MockInfoPlistContentProvider!
var subject: GenerateInfoPlistProjectMapper!
override public func setUp() {
super.setUp()
infoPlistContentProvider = MockInfoPlistContentProvider()
subject = GenerateInfoPlistProjectMapper(infoPlistContentProvider: infoPlistContentProvider,
derivedDirectoryName: Constants.DerivedDirectory.name,
infoPlistsDirectoryName: Constants.DerivedDirectory.infoPlists)
}
override public func tearDown() {
super.tearDown()
infoPlistContentProvider = nil
subject = nil
}
func test_map() throws {
// Given
let targetA = Target.test(name: "A", infoPlist: .dictionary(["A": "A_VALUE"]))
let targetB = Target.test(name: "B", infoPlist: .dictionary(["B": "B_VALUE"]))
let project = Project.test(targets: [targetA, targetB])
// When
let (mappedProject, sideEffects) = try subject.map(project: project)
// Then
XCTAssertEqual(sideEffects.count, 2)
XCTAssertEqual(mappedProject.targets.count, 2)
try XCTAssertSideEffectsCreateDerivedInfoPlist(named: "A.plist",
content: ["A": "A_VALUE"],
projectPath: project.path,
sideEffects: sideEffects)
try XCTAssertSideEffectsCreateDerivedInfoPlist(named: "B.plist",
content: ["B": "B_VALUE"],
projectPath: project.path,
sideEffects: sideEffects)
XCTAssertTargetExistsWithDerivedInfoPlist(named: "A.plist",
project: mappedProject)
XCTAssertTargetExistsWithDerivedInfoPlist(named: "B.plist",
project: mappedProject)
}
// MARK: - Helpers
private func XCTAssertSideEffectsCreateDerivedInfoPlist(named: String,
content: [String: String],
projectPath: AbsolutePath,
sideEffects: [SideEffectDescriptor],
file: StaticString = #file,
line: UInt = #line) throws {
let data = try PropertyListSerialization.data(fromPropertyList: content,
format: .xml,
options: 0)
XCTAssertNotNil(sideEffects.first(where: { sideEffect in
guard case let SideEffectDescriptor.file(file) = sideEffect else { return false }
return file.path == projectPath
.appending(component: Constants.DerivedDirectory.name)
.appending(component: Constants.DerivedDirectory.infoPlists)
.appending(component: named) && file.contents == data
}), file: file, line: line)
}
private func XCTAssertTargetExistsWithDerivedInfoPlist(named: String,
project: Project,
file: StaticString = #file,
line: UInt = #line) {
XCTAssertNotNil(project.targets.first(where: { (target: Target) in
target.infoPlist?.path == project.path
.appending(component: Constants.DerivedDirectory.name)
.appending(component: Constants.DerivedDirectory.infoPlists)
.appending(component: named)
}), file: file, line: line)
}
}

View File

@ -22,7 +22,7 @@ final class SecurityControllerIntegrationTests: TuistTestCase {
func test_import_certificate() throws {
// Given
let keychainPath = try temporaryPath().appending(component: Constants.signingKeychain)
let keychainPath = try temporaryPath().appending(component: Constants.DerivedDirectory.signingKeychain)
let currentDirectory = AbsolutePath(#file.replacingOccurrences(of: "file://", with: "")).removingLastComponent()
let publicKey = currentDirectory.appending(component: "Target.Debug.cer")
@ -54,7 +54,7 @@ final class SecurityControllerIntegrationTests: TuistTestCase {
func test_import_certificate_when_exists() throws {
// Given
let keychainPath = try temporaryPath().appending(component: Constants.signingKeychain)
let keychainPath = try temporaryPath().appending(component: Constants.DerivedDirectory.signingKeychain)
let currentDirectory = AbsolutePath(#file.replacingOccurrences(of: "file://", with: "")).removingLastComponent()
let publicKey = currentDirectory.appending(component: "Target.Debug.cer")

View File

@ -64,7 +64,8 @@ final class SigningInteractorTests: TuistUnitTestCase {
let rootDirectory = try temporaryPath()
rootDirectoryLocator.locateStub = rootDirectory
let keychainDirectory = rootDirectory.appending(components: Constants.derivedFolderName, Constants.signingKeychain)
let keychainDirectory = rootDirectory
.appending(components: Constants.DerivedDirectory.name, Constants.DerivedDirectory.signingKeychain)
var receivedKeychainDirectory: AbsolutePath?
var receivedMasterKey: String?
@ -95,7 +96,8 @@ final class SigningInteractorTests: TuistUnitTestCase {
let rootDirectory = try temporaryPath()
rootDirectoryLocator.locateStub = rootDirectory
let keychainDirectory = rootDirectory.appending(components: Constants.derivedFolderName, Constants.signingKeychain)
let keychainDirectory = rootDirectory
.appending(components: Constants.DerivedDirectory.name, Constants.DerivedDirectory.signingKeychain)
var receivedKeychainDirectory: AbsolutePath?
var receivedMasterKey: String?
@ -125,7 +127,8 @@ final class SigningInteractorTests: TuistUnitTestCase {
let rootDirectory = try temporaryPath()
rootDirectoryLocator.locateStub = rootDirectory
let keychainDirectory = rootDirectory.appending(components: Constants.derivedFolderName, Constants.signingKeychain)
let keychainDirectory = rootDirectory
.appending(components: Constants.DerivedDirectory.name, Constants.DerivedDirectory.signingKeychain)
var receivedKeychainDirectory: AbsolutePath?
var receivedMasterKey: String?

View File

@ -81,8 +81,8 @@ final class SigningMapperTests: TuistUnitTestCase {
path: try temporaryPath(),
targets: [target]
)
let derivedDirectory = project.path.appending(component: Constants.derivedFolderName)
let keychainPath = derivedDirectory.appending(component: Constants.signingKeychain)
let derivedDirectory = project.path.appending(component: Constants.DerivedDirectory.name)
let keychainPath = derivedDirectory.appending(component: Constants.DerivedDirectory.signingKeychain)
let expectedConfigurations: [BuildConfiguration: Configuration] = [
BuildConfiguration(