integrate XCTest with Spectre for nicer tests in Xcode

This commit is contained in:
Yonas Kolb
2018-06-08 15:12:42 +10:00
parent 9ff48c843d
commit 0e1f039055
12 changed files with 1191 additions and 1150 deletions
+5 -5
View File
@@ -42,8 +42,8 @@
"repositoryURL": "https://github.com/onevcat/Rainbow.git",
"state": {
"branch": null,
"revision": "f69961599ad524251d677fbec9e4bac57385d6fc",
"version": "3.1.1"
"revision": "797a68d0a642609424b08f11eb56974a54d5f6e2",
"version": "3.1.4"
}
},
{
@@ -57,11 +57,11 @@
},
{
"package": "Spectre",
"repositoryURL": "https://github.com/kylef/Spectre.git",
"repositoryURL": "https://github.com/yonaskolb/Spectre.git",
"state": {
"branch": null,
"revision": "e34d5687e1e9d865e3527dd58bc2f7464ef6d936",
"version": "0.8.0"
"revision": "2b520ec417fc515c70e7093ae3ee321c3237e215",
"version": "0.8.1"
}
},
{
+1 -1
View File
@@ -14,7 +14,7 @@ let package = Package(
.package(url: "https://github.com/kylef/Commander.git", from: "0.8.0"),
.package(url: "https://github.com/jpsim/Yams.git", from: "0.3.6"),
.package(url: "https://github.com/yonaskolb/JSONUtilities.git", from: "3.3.0"),
.package(url: "https://github.com/kylef/Spectre.git", from: "0.8.0"),
.package(url: "https://github.com/yonaskolb/Spectre.git", from: "0.8.1"),
.package(url: "https://github.com/onevcat/Rainbow.git", from: "3.0.0"),
.package(url: "https://github.com/xcodeswift/xcproj.git", from: "4.1.0")
],
-9
View File
@@ -1,9 +0,0 @@
@testable import XcodeGenKitTests
import XCTest
XCTMain([
testCase(GeneratorTests.allTests),
testCase(SpecLoadingTests.allTests),
testCase(FixtureTests.allTests),
testCase(ProjectSpecTests.allTests),
])
-81
View File
@@ -1,81 +0,0 @@
import PathKit
import ProjectSpec
import Spectre
import XcodeGenKit
import xcproj
let fixturePath = Path(#file).parent().parent() + "Fixtures"
func generateXcodeProject(specPath: Path, projectPath: Path) throws -> XcodeProj {
let project = try Project(path: specPath)
let generator = ProjectGenerator(project: project)
let xcodeProject = try generator.generateXcodeProject()
let oldProject = try XcodeProj(path: projectPath)
let pbxProjPath = projectPath + XcodeProj.pbxprojPath(projectPath)
let oldProjectString: String = try pbxProjPath.read()
try xcodeProject.write(path: projectPath, override: true)
let newProjectString: String = try pbxProjPath.read()
let newProject = try XcodeProj(path: projectPath)
let stringDiff = newProjectString != oldProjectString
if newProject != oldProject || stringDiff {
var message = "\(projectPath.string) has changed. If change is legitimate commit the change and run test again"
if stringDiff {
message += ":\n\n\(pbxProjPath):\n\(prettyFirstDifferenceBetweenStrings(oldProjectString, newProjectString))"
}
throw failure(message)
}
return newProject
}
func fixtureTests() {
describe("Test Project") {
var xcodeProject: XcodeProj?
$0.it("generates") {
xcodeProject = try generateXcodeProject(specPath: fixturePath + "TestProject/project.yml", projectPath: fixturePath + "TestProject/Project.xcodeproj")
}
$0.it("generates variant group") {
guard let xcodeProject = xcodeProject else { return }
func getFileReferences(_ path: String) -> [ObjectReference<PBXFileReference>] {
return xcodeProject.pbxproj.objects.fileReferences.objectReferences.filter { $0.object.path == path }
}
func getVariableGroups(_ name: String?) -> [PBXVariantGroup] {
return xcodeProject.pbxproj.objects.variantGroups.referenceValues.filter { $0.name == name }
}
let resourceName = "LocalizedStoryboard.storyboard"
let baseResource = "Base.lproj/LocalizedStoryboard.storyboard"
let localizedResource = "en.lproj/LocalizedStoryboard.strings"
guard let variableGroup = getVariableGroups(resourceName).first else { throw failure("Couldn't find the variable group") }
do {
let refs = getFileReferences(baseResource)
try expect(refs.count) == 1
try expect(variableGroup.children.filter { $0 == refs.first?.reference }.count) == 1
}
do {
let refs = getFileReferences(localizedResource)
try expect(refs.count) == 1
try expect(variableGroup.children.filter { $0 == refs.first?.reference }.count) == 1
}
}
$0.it("generates scheme execution actions") {
guard let xcodeProject = xcodeProject else { return }
let frameworkScheme = xcodeProject.sharedData?.schemes.first { $0.name == "Framework" }
try expect(frameworkScheme?.buildAction?.preActions.first?.scriptText) == "echo Starting Framework Build"
try expect(frameworkScheme?.buildAction?.preActions.first?.title) == "Run Script"
try expect(frameworkScheme?.buildAction?.preActions.first?.environmentBuildable?.blueprintName) == "Framework_iOS"
try expect(frameworkScheme?.buildAction?.preActions.first?.environmentBuildable?.buildableName) == "Framework.framework"
}
}
}
@@ -0,0 +1,47 @@
import PathKit
import ProjectSpec
import Spectre
import XcodeGenKit
import xcproj
import XCTest
import Yams
extension Project {
func generateXcodeProject(file: String = #file, line: Int = #line) throws -> XcodeProj {
return try doThrowing(file: file, line: line) {
let generator = ProjectGenerator(project: self)
return try generator.generateXcodeProject()
}
}
func generatePbxProj(file: String = #file, line: Int = #line) throws -> PBXProj {
return try doThrowing(file: file, line: line) {
let xcodeProject = try generateXcodeProject().pbxproj
try xcodeProject.validate()
return xcodeProject
}
}
}
extension PBXProj {
// validates that a PBXProj is correct
// TODO: Use xclint?
func validate() throws {
let mainGroup = try getMainGroup()
func validateGroup(_ group: PBXGroup) throws {
let hasDuplicatedChildren = group.children.count != Set(group.children).count
if hasDuplicatedChildren {
throw failure("Group \"\(group.nameOrPath)\" has duplicated children:\n - \(group.children.sorted().joined(separator: "\n - "))")
}
for child in group.children {
if let group = objects.groups.getReference(child) {
try validateGroup(group)
}
}
}
try validateGroup(mainGroup)
}
}
@@ -0,0 +1,83 @@
import PathKit
import ProjectSpec
import Spectre
import XcodeGenKit
import xcproj
import XCTest
class ProjectFixtureTests: XCTestCase {
func testProjectFixture() {
describe {
var xcodeProject: XcodeProj?
$0.it("generates") {
xcodeProject = try generateXcodeProject(specPath: fixturePath + "TestProject/project.yml", projectPath: fixturePath + "TestProject/Project.xcodeproj")
}
$0.it("generates variant group") {
guard let xcodeProject = xcodeProject else { return }
func getFileReferences(_ path: String) -> [ObjectReference<PBXFileReference>] {
return xcodeProject.pbxproj.objects.fileReferences.objectReferences.filter { $0.object.path == path }
}
func getVariableGroups(_ name: String?) -> [PBXVariantGroup] {
return xcodeProject.pbxproj.objects.variantGroups.referenceValues.filter { $0.name == name }
}
let resourceName = "LocalizedStoryboard.storyboard"
let baseResource = "Base.lproj/LocalizedStoryboard.storyboard"
let localizedResource = "en.lproj/LocalizedStoryboard.strings"
guard let variableGroup = getVariableGroups(resourceName).first else { throw failure("Couldn't find the variable group") }
do {
let refs = getFileReferences(baseResource)
try expect(refs.count) == 1
try expect(variableGroup.children.filter { $0 == refs.first?.reference }.count) == 1
}
do {
let refs = getFileReferences(localizedResource)
try expect(refs.count) == 1
try expect(variableGroup.children.filter { $0 == refs.first?.reference }.count) == 1
}
}
$0.it("generates scheme execution actions") {
guard let xcodeProject = xcodeProject else { return }
let frameworkScheme = xcodeProject.sharedData?.schemes.first { $0.name == "Framework" }
try expect(frameworkScheme?.buildAction?.preActions.first?.scriptText) == "echo Starting Framework Build"
try expect(frameworkScheme?.buildAction?.preActions.first?.title) == "Run Script"
try expect(frameworkScheme?.buildAction?.preActions.first?.environmentBuildable?.blueprintName) == "Framework_iOS"
try expect(frameworkScheme?.buildAction?.preActions.first?.environmentBuildable?.buildableName) == "Framework.framework"
}
}
}
}
fileprivate func generateXcodeProject(specPath: Path, projectPath: Path, file: String = #file, line: Int = #line) throws -> XcodeProj {
let project = try Project(path: specPath)
let generator = ProjectGenerator(project: project)
let xcodeProject = try generator.generateXcodeProject()
let oldProject = try XcodeProj(path: projectPath)
let pbxProjPath = projectPath + XcodeProj.pbxprojPath(projectPath)
let oldProjectString: String = try pbxProjPath.read()
try xcodeProject.write(path: projectPath, override: true)
let newProjectString: String = try pbxProjPath.read()
let newProject = try XcodeProj(path: projectPath)
let stringDiff = newProjectString != oldProjectString
if newProject != oldProject || stringDiff {
var message = "\(projectPath.string) has changed. If change is legitimate commit the change and run test again"
if stringDiff {
message += ":\n\n\(pbxProjPath):\n\(prettyFirstDifferenceBetweenStrings(oldProjectString, newProjectString))"
}
throw failure(message, file: file, line: line)
}
return newProject
}
@@ -3,54 +3,44 @@ import ProjectSpec
import Spectre
import XcodeGenKit
import xcproj
import XCTest
import Yams
func projectGeneratorTests() {
fileprivate let app = Target(
name: "MyApp",
type: .application,
platform: .iOS,
settings: Settings(buildSettings: ["SETTING_1": "VALUE"]),
dependencies: [Dependency(type: .target, reference: "MyFramework")]
)
func getXcodeProject(_ project: Project) throws -> XcodeProj {
let generator = ProjectGenerator(project: project)
return try generator.generateXcodeProject()
}
fileprivate let framework = Target(
name: "MyFramework",
type: .framework,
platform: .iOS,
settings: Settings(buildSettings: ["SETTING_2": "VALUE"])
)
func getPbxProj(_ project: Project) throws -> PBXProj {
let xcodeProject = try getXcodeProject(project).pbxproj
try xcodeProject.validate()
return xcodeProject
}
fileprivate let uiTest = Target(
name: "MyAppUITests",
type: .uiTestBundle,
platform: .iOS,
settings: Settings(buildSettings: ["SETTING_3": "VALUE"]),
dependencies: [Dependency(type: .target, reference: "MyApp")]
)
describe("Project Generator") {
fileprivate let targets = [app, framework, uiTest]
let application = Target(
name: "MyApp",
type: .application,
platform: .iOS,
settings: Settings(buildSettings: ["SETTING_1": "VALUE"]),
dependencies: [Dependency(type: .target, reference: "MyFramework")]
)
class ProjectGeneratorTests: XCTestCase {
let framework = Target(
name: "MyFramework",
type: .framework,
platform: .iOS,
settings: Settings(buildSettings: ["SETTING_2": "VALUE"])
)
func testOptions() throws {
let uiTest = Target(
name: "MyAppUITests",
type: .uiTestBundle,
platform: .iOS,
settings: Settings(buildSettings: ["SETTING_3": "VALUE"]),
dependencies: [Dependency(type: .target, reference: "MyApp")]
)
let targets = [application, framework, uiTest]
$0.describe("Options") {
describe {
$0.it("generates bundle id") {
let options = SpecOptions(bundleIdPrefix: "com.test")
let project = Project(basePath: "", name: "test", targets: [framework], options: options)
let pbxProj = try getPbxProj(project)
let pbxProj = try project.generatePbxProj()
guard let target = pbxProj.objects.nativeTargets.first?.value,
let buildConfigList = target.buildConfigurationList,
let buildConfigs = pbxProj.objects.configurationLists.getReference(buildConfigList),
@@ -64,7 +54,7 @@ func projectGeneratorTests() {
$0.it("clears setting presets") {
let options = SpecOptions(settingPresets: .none)
let project = Project(basePath: "", name: "test", targets: [framework], options: options)
let pbxProj = try getPbxProj(project)
let pbxProj = try project.generatePbxProj()
let allSettings = pbxProj.objects.buildConfigurations.referenceValues.reduce([:]) { $0.merged($1.buildSettings) }.keys.sorted()
try expect(allSettings) == ["SETTING_2"]
}
@@ -72,7 +62,7 @@ func projectGeneratorTests() {
$0.it("generates development language") {
let options = SpecOptions(developmentLanguage: "de")
let project = Project(basePath: "", name: "test", options: options)
let pbxProj = try getPbxProj(project)
let pbxProj = try project.generatePbxProj()
guard let pbxProject = pbxProj.objects.projects.first?.value else {
throw failure("Could't find PBXProject")
}
@@ -100,7 +90,7 @@ func projectGeneratorTests() {
$0.it("uses the default configuration name") {
let options = SpecOptions(defaultConfig: "Bconfig")
let project = Project(basePath: "", name: "test", configs: [Config(name: "Aconfig"), Config(name: "Bconfig")], targets: [framework], options: options)
let pbxProject = try getPbxProj(project)
let pbxProject = try project.generatePbxProj()
guard let projectConfigListReference = pbxProject.objects.projects.values.first?.buildConfigurationList,
let defaultConfigurationName = pbxProject.objects.configurationLists[projectConfigListReference]?.defaultConfigurationName
@@ -111,12 +101,14 @@ func projectGeneratorTests() {
try expect(defaultConfigurationName) == "Bconfig"
}
}
}
$0.describe("Config") {
func testConfigGenerator() {
describe {
$0.it("generates config defaults") {
let project = Project(basePath: "", name: "test")
let pbxProj = try getPbxProj(project)
let pbxProj = try project.generatePbxProj()
let configs = pbxProj.objects.buildConfigurations.referenceValues
try expect(configs.count) == 2
try expect(configs).contains(name: "Debug")
@@ -129,7 +121,7 @@ func projectGeneratorTests() {
name: "test",
configs: [Config(name: "config1"), Config(name: "config2")]
)
let pbxProj = try getPbxProj(project)
let pbxProj = try project.generatePbxProj()
let configs = pbxProj.objects.buildConfigurations.referenceValues
try expect(configs.count) == 2
try expect(configs).contains(name: "config1")
@@ -142,7 +134,7 @@ func projectGeneratorTests() {
name: "test",
configs: [Config(name: "config")]
)
let pbxProj = try getPbxProj(project)
let pbxProj = try project.generatePbxProj()
guard let config = pbxProj.objects.buildConfigurations.first?.value else {
throw failure("configuration not found")
}
@@ -193,35 +185,37 @@ func projectGeneratorTests() {
try expect(buildSettings["SETTING2"] as? String) == "VALUE2"
}
}
}
$0.describe("Targets") {
func testTargets() {
describe {
let project = Project(basePath: "", name: "test", targets: targets)
$0.it("generates targets") {
let pbxProject = try getPbxProj(project)
let pbxProject = try project.generatePbxProj()
let nativeTargets = pbxProject.objects.nativeTargets.referenceValues
try expect(nativeTargets.count) == 3
try expect(nativeTargets.contains { $0.name == application.name }).beTrue()
try expect(nativeTargets.contains { $0.name == app.name }).beTrue()
try expect(nativeTargets.contains { $0.name == framework.name }).beTrue()
try expect(nativeTargets.contains { $0.name == uiTest.name }).beTrue()
}
$0.it("generates target attributes") {
var appTargetWithAttributes = application
var appTargetWithAttributes = app
appTargetWithAttributes.settings.buildSettings["DEVELOPMENT_TEAM"] = "123"
appTargetWithAttributes.attributes = ["ProvisioningStyle": "Automatic"]
var testTargetWithAttributes = uiTest
testTargetWithAttributes.settings.buildSettings["CODE_SIGN_STYLE"] = "Manual"
var project = Project(basePath: "", name: "test", targets: [appTargetWithAttributes, framework, testTargetWithAttributes])
let pbxProject = try getPbxProj(project)
let project = Project(basePath: "", name: "test", targets: [appTargetWithAttributes, framework, testTargetWithAttributes])
let pbxProject = try project.generatePbxProj()
guard let targetAttributes = pbxProject.objects.projects.referenceValues.first?.attributes["TargetAttributes"] as? [String: [String: Any]] else {
throw failure("Couldn't find Project TargetAttributes")
}
guard let appTarget = pbxProject.objects.targets(named: application.name).first else {
guard let appTarget = pbxProject.objects.targets(named: app.name).first else {
throw failure("Couldn't find App Target")
}
@@ -239,7 +233,7 @@ func projectGeneratorTests() {
let target = Target(name: "Target", type: .application, platform: .watchOS, deploymentTarget: "2.0")
let project = Project(basePath: "", name: "", targets: [target], options: .init(deploymentTarget: DeploymentTarget(iOS: "10.0", watchOS: "3.0")))
let pbxProject = try getPbxProj(project)
let pbxProject = try project.generatePbxProj()
guard let projectConfigListReference = pbxProject.objects.projects.values.first?.buildConfigurationList,
let projectConfigReference = pbxProject.objects.configurationLists[projectConfigListReference]?.buildConfigurations.first,
@@ -265,20 +259,20 @@ func projectGeneratorTests() {
}
$0.it("generates dependencies") {
let pbxProject = try getPbxProj(project)
let pbxProject = try project.generatePbxProj()
let nativeTargets = pbxProject.objects.nativeTargets.objectReferences
let dependencies = pbxProject.objects.targetDependencies.objectReferences
try expect(dependencies.count) == 2
try expect(dependencies[0].object.target) == nativeTargets.first { $0.object.name == framework.name }!.reference
try expect(dependencies[1].object.target) == nativeTargets.first { $0.object.name == application.name }!.reference
try expect(dependencies[1].object.target) == nativeTargets.first { $0.object.name == app.name }!.reference
}
$0.it("generates run scripts") {
var scriptSpec = project
scriptSpec.targets[0].prebuildScripts = [BuildScript(script: .script("script1"))]
scriptSpec.targets[0].postbuildScripts = [BuildScript(script: .script("script2"))]
let pbxProject = try getPbxProj(scriptSpec)
let pbxProject = try scriptSpec.generatePbxProj()
guard let nativeTarget = pbxProject.objects.nativeTargets.referenceValues
.first(where: { $0.buildPhases.count >= 2 }) else {
@@ -316,15 +310,17 @@ func projectGeneratorTests() {
targets: [target1, target2]
)
_ = try getPbxProj(project)
_ = try project.generatePbxProj()
}
}
}
$0.describe("Schemes") {
func testSchemes() {
describe {
let buildTarget = Scheme.BuildTarget(target: application.name)
let buildTarget = Scheme.BuildTarget(target: app.name)
$0.it("generates scheme") {
let preAction = Scheme.ExecutionAction(name: "Script", script: "echo Starting", settingsTarget: application.name)
let preAction = Scheme.ExecutionAction(name: "Script", script: "echo Starting", settingsTarget: app.name)
let scheme = Scheme(
name: "MyScheme",
build: Scheme.Build(targets: [buildTarget], preActions: [preAction])
@@ -332,12 +328,12 @@ func projectGeneratorTests() {
let project = Project(
basePath: "",
name: "test",
targets: [application, framework],
targets: [app, framework],
schemes: [scheme]
)
let xcodeProject = try getXcodeProject(project)
let xcodeProject = try project.generateXcodeProject()
guard let target = xcodeProject.pbxproj.objects.nativeTargets.objectReferences
.first(where: { $0.object.name == application.name }) else {
.first(where: { $0.object.name == app.name }) else {
throw failure("Target not found")
}
guard let xcscheme = xcodeProject.sharedData?.schemes.first else {
@@ -391,20 +387,17 @@ func projectGeneratorTests() {
let project = Project(
basePath: "",
name: "test",
targets: [application, framework],
targets: [app, framework],
schemes: [scheme]
)
let xcodeProject = try getXcodeProject(project)
guard let target = xcodeProject.pbxproj.objects.nativeTargets.objectReferences
.first(where: { $0.object.name == application.name }) else {
throw failure("Target not found")
}
let xcodeProject = try project.generateXcodeProject()
guard let xcscheme = xcodeProject.sharedData?.schemes.first else {
throw failure("Scheme not found")
}
try expect(xcodeProject.pbxproj.objects.nativeTargets.objectReferences
.contains(where: { $0.object.name == app.name })).beTrue()
try expect(xcscheme.launchAction?.environmentVariables) == runVariables
try expect(xcscheme.testAction?.environmentVariables).to.beNil()
try expect(xcscheme.profileAction?.environmentVariables).to.beNil()
@@ -412,7 +405,7 @@ func projectGeneratorTests() {
$0.it("generates target schemes from config variant") {
let configVariants = ["Test", "Production"]
var target = application
var target = app
target.scheme = TargetScheme(configVariants: configVariants)
let configs: [Config] = [
Config(name: "Test Debug", type: .debug),
@@ -422,12 +415,12 @@ func projectGeneratorTests() {
]
let project = Project(basePath: "", name: "test", configs: configs, targets: [target, framework])
let xcodeProject = try getXcodeProject(project)
let xcodeProject = try project.generateXcodeProject()
try expect(xcodeProject.sharedData?.schemes.count) == 2
guard let nativeTarget = xcodeProject.pbxproj.objects.nativeTargets.objectReferences
.first(where: { $0.object.name == application.name }) else {
.first(where: { $0.object.name == app.name }) else {
throw failure("Target not found")
}
guard let xcscheme = xcodeProject.sharedData?.schemes
@@ -448,11 +441,11 @@ func projectGeneratorTests() {
$0.it("generates environment variables for target schemes") {
let variables: [XCScheme.EnvironmentVariable] = [XCScheme.EnvironmentVariable(variable: "env", value: "var", enabled: false)]
var target = application
var target = app
target.scheme = TargetScheme(environmentVariables: variables)
let project = Project(basePath: "", name: "test", targets: [target, framework])
let xcodeProject = try getXcodeProject(project)
let xcodeProject = try project.generateXcodeProject()
try expect(xcodeProject.sharedData?.schemes.count) == 1
@@ -466,14 +459,14 @@ func projectGeneratorTests() {
}
$0.it("generates pre and post actions for target schemes") {
var target = application
var target = app
target.scheme = TargetScheme(
preActions: [.init(name: "Run", script: "do")],
postActions: [.init(name: "Run2", script: "post", settingsTarget: "MyApp")]
)
let project = Project(basePath: "", name: "test", targets: [target, framework])
let xcodeProject = try getXcodeProject(project)
let xcodeProject = try project.generateXcodeProject()
try expect(xcodeProject.sharedData?.schemes.count) == 1
@@ -491,553 +484,6 @@ func projectGeneratorTests() {
try expect(xcscheme.testAction?.postActions.first?.environmentBuildable?.blueprintName) == "MyApp"
}
}
$0.describe("Sources") {
let directoryPath = Path("TestDirectory")
let outOfRootPath = Path("OtherDirectory")
func createDirectories(_ directories: String) throws {
let yaml = try Yams.load(yaml: directories)!
func getFiles(_ file: Any, path: Path) -> [Path] {
if let array = file as? [Any] {
return array.flatMap { getFiles($0, path: path) }
} else if let string = file as? String {
return [path + string]
} else if let dictionary = file as? [String: Any] {
var array: [Path] = []
for (key, value) in dictionary {
array += getFiles(value, path: path + key)
}
return array
} else {
return []
}
}
let files = getFiles(yaml, path: directoryPath).filter { $0.extension != nil }
for file in files {
try file.parent().mkpath()
try file.write("")
}
}
func removeDirectories() {
try? directoryPath.delete()
try? outOfRootPath.delete()
}
$0.before {
removeDirectories()
}
$0.after {
removeDirectories()
}
$0.it("generates source groups") {
let directories = """
Sources:
A:
- a.swift
- B:
- b.swift
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try getPbxProj(project)
try pbxProj.expectFile(paths: ["Sources", "A", "a.swift"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["Sources", "A", "B", "b.swift"], buildPhase: .sources)
}
$0.it("supports frameworks in sources") {
let directories = """
Sources:
- Foo.framework
- Bar.swift
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try getPbxProj(project)
try pbxProj.expectFile(paths: ["Sources", "Bar.swift"], buildPhase: .sources)
let buildPhase = pbxProj.objects.copyFilesBuildPhases.referenceValues.first
try expect(buildPhase?.dstSubfolderSpec) == .frameworks
let fileReference = pbxProj.getFileReference(paths: ["Sources", "Foo.framework"],
names: ["Sources", "Foo.framework"])?.reference ?? ""
let buildFile = pbxProj.objects.buildFiles.objectReferences
.first(where: { $0.object.fileRef == fileReference })?.reference ?? ""
try expect(buildPhase?.files.count) == 1
try expect(buildPhase?.files.contains(buildFile)) == true
}
$0.it("generates core data models") {
let directories = """
Sources:
model.xcdatamodeld:
- model.xcdatamodel
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try getPbxProj(project)
guard let fileReference = pbxProj.objects.fileReferences.first(where: { $0.value.nameOrPath == "model.xcdatamodel" }) else {
throw failure("Couldn't find model file reference")
}
guard let versionGroup = pbxProj.objects.versionGroups.values.first else {
throw failure("Couldn't find version group")
}
try expect(versionGroup.currentVersion) == fileReference.key
try expect(versionGroup.children) == [fileReference.key]
try expect(versionGroup.path) == "model.xcdatamodeld"
try expect(fileReference.value.path) == "model.xcdatamodel"
}
$0.it("handles duplicate names") {
let directories = """
Sources:
- a.swift
- a:
- a.swift
- a:
- a.swift
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"])
let project = Project(
basePath: directoryPath,
name: "Test",
targets: [target],
fileGroups: ["Sources"]
)
let pbxProj = try getPbxProj(project)
try pbxProj.expectFile(paths: ["Sources", "a.swift"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["Sources", "a", "a.swift"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["Sources", "a", "a", "a.swift"], buildPhase: .sources)
}
$0.it("renames sources") {
let directories = """
Sources:
- a.swift
OtherSource:
- b.swift
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
TargetSource(path: "Sources", name: "NewSource"),
TargetSource(path: "OtherSource/b.swift", name: "c.swift"),
])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try getPbxProj(project)
try pbxProj.expectFile(paths: ["Sources", "a.swift"], names: ["NewSource", "a.swift"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["OtherSource", "b.swift"], names: ["OtherSource", "c.swift"], buildPhase: .sources)
}
$0.it("excludes sources") {
let directories = """
Sources:
- A:
- a.swift
- B:
- b.swift
- b.ignored
- a.ignored
- B:
- b.swift
- D:
- d.h
- d.m
- E:
- e.jpg
- e.h
- e.m
- F:
- f.swift
- G:
- H:
- h.swift
- types:
- a.swift
- a.m
- a.h
- a.x
- numbers:
- file1.a
- file2.a
- file3.a
- file4.a
- partial:
- file_part
- ignore.file
- a.ignored
- project.xcodeproj:
- project.pbxproj
"""
try createDirectories(directories)
let excludes = [
"B",
"d.m",
"E/F/*.swift",
"G/H/",
"types/*.[hx]",
"numbers/file[2-3].a",
"partial/*_part",
"ignore.file",
"*.ignored",
"*.xcodeproj",
// not supported
// "**/*.ignored",
]
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [TargetSource(path: "Sources", excludes: excludes)])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try getPbxProj(project)
try pbxProj.expectFile(paths: ["Sources", "A", "a.swift"])
try pbxProj.expectFile(paths: ["Sources", "D", "d.h"])
try pbxProj.expectFile(paths: ["Sources", "D", "d.m"])
try pbxProj.expectFile(paths: ["Sources", "E", "e.jpg"])
try pbxProj.expectFile(paths: ["Sources", "E", "e.m"])
try pbxProj.expectFile(paths: ["Sources", "E", "e.h"])
try pbxProj.expectFile(paths: ["Sources", "types", "a.swift"])
try pbxProj.expectFile(paths: ["Sources", "numbers", "file1.a"])
try pbxProj.expectFile(paths: ["Sources", "numbers", "file4.a"])
try pbxProj.expectFileMissing(paths: ["Sources", "B", "b.swift"])
try pbxProj.expectFileMissing(paths: ["Sources", "E", "F", "f.swift"])
try pbxProj.expectFileMissing(paths: ["Sources", "G", "H", "h.swift"])
try pbxProj.expectFileMissing(paths: ["Sources", "types", "a.h"])
try pbxProj.expectFileMissing(paths: ["Sources", "types", "a.x"])
try pbxProj.expectFileMissing(paths: ["Sources", "numbers", "file2.a"])
try pbxProj.expectFileMissing(paths: ["Sources", "numbers", "file3.a"])
try pbxProj.expectFileMissing(paths: ["Sources", "partial", "file_part"])
try pbxProj.expectFileMissing(paths: ["Sources", "a.ignored"])
try pbxProj.expectFileMissing(paths: ["Sources", "ignore.file"])
try pbxProj.expectFileMissing(paths: ["Sources", "project.xcodeproj"])
}
$0.it("generates file sources") {
let directories = """
Sources:
A:
- a.swift
- Assets.xcassets
- B:
- b.swift
- c.jpg
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
"Sources/A/a.swift",
"Sources/A/B/b.swift",
"Sources/A/Assets.xcassets",
"Sources/A/B/c.jpg",
])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try getPbxProj(project)
try pbxProj.expectFile(paths: ["Sources/A", "a.swift"], names: ["A", "a.swift"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["Sources/A/B", "b.swift"], names: ["B", "b.swift"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["Sources/A/B", "c.jpg"], names: ["B", "c.jpg"], buildPhase: .resources)
try pbxProj.expectFile(paths: ["Sources/A", "Assets.xcassets"], names: ["A", "Assets.xcassets"], buildPhase: .resources)
}
$0.it("generates shared sources") {
let directories = """
Sources:
A:
- a.swift
- B:
- b.swift
- c.jpg
"""
try createDirectories(directories)
let target1 = Target(name: "Test1", type: .framework, platform: .iOS, sources: ["Sources"])
let target2 = Target(name: "Test2", type: .framework, platform: .tvOS, sources: ["Sources"])
let project = Project(basePath: directoryPath, name: "Test", targets: [target1, target2])
_ = try getPbxProj(project)
// TODO: check there are build files for both targets
}
$0.it("generates intermediate groups") {
let directories = """
Sources:
A:
- b.swift
F:
- G:
- h.swift
"""
try createDirectories(directories)
let outOfSourceFile = outOfRootPath + "C/D/e.swift"
try outOfSourceFile.parent().mkpath()
try outOfSourceFile.write("")
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
"Sources/A/b.swift",
"Sources/F/G/h.swift",
"../OtherDirectory/C/D/e.swift",
])
let options = SpecOptions(createIntermediateGroups: true)
let project = Project(basePath: directoryPath, name: "Test", targets: [target], options: options)
let pbxProj = try getPbxProj(project)
try pbxProj.expectFile(paths: ["Sources", "A", "b.swift"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["Sources", "F", "G", "h.swift"], buildPhase: .sources)
try pbxProj.expectFile(paths: [(outOfRootPath + "C/D").string, "e.swift"], names: ["D", "e.swift"], buildPhase: .sources)
}
$0.it("generates folder references") {
let directories = """
Sources:
A:
- a.resource
- b.resource
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
TargetSource(path: "Sources/A", type: .folder),
])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try getPbxProj(project)
try pbxProj.expectFile(paths: ["Sources/A"], names: ["A"], buildPhase: .resources)
try pbxProj.expectFileMissing(paths: ["Sources", "A", "a.swift"])
}
$0.it("adds files to correct build phase") {
let directories = """
A:
- file.swift
- file.xcassets
- file.h
- Info.plist
- file.xcconfig
B:
- file.swift
- file.xcassets
- file.h
- Info.plist
- file.xcconfig
C:
- file.swift
- file.m
- file.mm
- file.cpp
- file.c
- file.S
- file.h
- file.hh
- file.hpp
- file.ipp
- file.tpp
- file.hxx
- file.def
- file.xcconfig
- file.entitlements
- file.gpx
- file.apns
- file.123
- file.xcassets
- Info.plist
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .framework, platform: .iOS, sources: [
TargetSource(path: "A", buildPhase: .resources),
TargetSource(path: "B", buildPhase: .none),
TargetSource(path: "C", buildPhase: nil),
])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try getPbxProj(project)
try pbxProj.expectFile(paths: ["A", "file.swift"], buildPhase: .resources)
try pbxProj.expectFile(paths: ["A", "file.xcassets"], buildPhase: .resources)
try pbxProj.expectFile(paths: ["A", "file.h"], buildPhase: .resources)
try pbxProj.expectFile(paths: ["A", "Info.plist"], buildPhase: .none)
try pbxProj.expectFile(paths: ["A", "file.xcconfig"], buildPhase: .resources)
try pbxProj.expectFile(paths: ["B", "file.swift"], buildPhase: .none)
try pbxProj.expectFile(paths: ["B", "file.xcassets"], buildPhase: .none)
try pbxProj.expectFile(paths: ["B", "file.h"], buildPhase: .none)
try pbxProj.expectFile(paths: ["B", "Info.plist"], buildPhase: .none)
try pbxProj.expectFile(paths: ["B", "file.xcconfig"], buildPhase: .none)
try pbxProj.expectFile(paths: ["C", "file.swift"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["C", "file.m"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["C", "file.mm"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["C", "file.cpp"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["C", "file.c"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["C", "file.S"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["C", "file.h"], buildPhase: .headers)
try pbxProj.expectFile(paths: ["C", "file.hh"], buildPhase: .headers)
try pbxProj.expectFile(paths: ["C", "file.hpp"], buildPhase: .headers)
try pbxProj.expectFile(paths: ["C", "file.ipp"], buildPhase: .headers)
try pbxProj.expectFile(paths: ["C", "file.tpp"], buildPhase: .headers)
try pbxProj.expectFile(paths: ["C", "file.hxx"], buildPhase: .headers)
try pbxProj.expectFile(paths: ["C", "file.def"], buildPhase: .headers)
try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: .none)
try pbxProj.expectFile(paths: ["C", "file.entitlements"], buildPhase: .none)
try pbxProj.expectFile(paths: ["C", "file.gpx"], buildPhase: .none)
try pbxProj.expectFile(paths: ["C", "file.apns"], buildPhase: .none)
try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: .none)
try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: .none)
try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: .none)
try pbxProj.expectFile(paths: ["C", "file.xcassets"], buildPhase: .resources)
try pbxProj.expectFile(paths: ["C", "file.123"], buildPhase: .resources)
try pbxProj.expectFile(paths: ["C", "Info.plist"], buildPhase: .none)
}
$0.it("duplicate TargetSource is included once in sources build phase") {
let directories = """
Sources:
A:
- a.swift
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
"Sources/A/a.swift",
"Sources/A/a.swift",
])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try getPbxProj(project)
try pbxProj.expectFile(paths: ["Sources/A", "a.swift"], names: ["A", "a.swift"], buildPhase: .sources)
let sourcesBuildPhase = pbxProj.objects.buildPhases
.first(where: { $0.1.buildPhase == BuildPhase.sources })!
.value
try expect(sourcesBuildPhase.files.count) == 1
}
}
}
}
extension PBXProj {
// validates that a PBXProj is correct
// TODO: Use xclint?
func validate() throws {
let mainGroup = try getMainGroup()
func validateGroup(_ group: PBXGroup) throws {
let hasDuplicatedChildren = group.children.count != Set(group.children).count
if hasDuplicatedChildren {
throw failure("Group \"\(group.nameOrPath)\" has duplicated children:\n - \(group.children.sorted().joined(separator: "\n - "))")
}
for child in group.children {
if let group = objects.groups.getReference(child) {
try validateGroup(group)
}
}
}
try validateGroup(mainGroup)
}
}
extension PBXProj {
/// expect a file within groups of the paths, using optional different names
func expectFile(paths: [String], names: [String]? = nil, buildPhase: TargetSource.BuildPhase? = nil) throws {
guard let fileReference = getFileReference(paths: paths, names: names ?? paths) else {
var error = "Could not find file at path \(paths.joined(separator: "/").quoted)"
if let names = names, names != paths {
error += " and name \(names.joined(separator: "/").quoted)"
}
throw failure(error)
}
if let buildPhase = buildPhase {
let buildFile = objects.buildFiles.objectReferences
.first(where: { $0.object.fileRef == fileReference.reference })
let actualBuildPhase = buildFile
.flatMap { buildFile in objects.buildPhases.referenceValues.first { $0.files.contains(buildFile.reference) } }?.buildPhase
var error: String?
if let buildPhase = buildPhase.buildPhase {
if actualBuildPhase != buildPhase {
if let actualBuildPhase = actualBuildPhase {
error = "is in the \(actualBuildPhase.rawValue) build phase instead of the expected \(buildPhase.rawValue.quoted)"
} else {
error = "isn't in a build phase when it's expected to be in \(buildPhase.rawValue.quoted)"
}
}
} else if let actualBuildPhase = actualBuildPhase {
error = "is in the \(actualBuildPhase.rawValue.quoted) build phase when it's expected to not be in any"
}
if let error = error {
throw failure("File \(paths.joined(separator: "/").quoted) \(error)")
}
}
}
/// expect a missing file within groups of the paths, using optional different names
func expectFileMissing(paths: [String], names: [String]? = nil) throws {
let names = names ?? paths
if getFileReference(paths: paths, names: names) != nil {
throw failure("Found unexpected file at path \(paths.joined(separator: "/").quoted) and name \(paths.joined(separator: "/").quoted)")
}
}
func getFileReference(paths: [String], names: [String]) -> ObjectReference<PBXFileReference>? {
guard let project = objects.projects.first?.value else { return nil }
guard let mainGroup = objects.groups.getReference(project.mainGroup) else { return nil }
return getFileReference(group: mainGroup, paths: paths, names: names)
}
func getMainGroup() throws -> PBXGroup {
guard let project = objects.projects.first?.value else {
throw failure("Couldn't find project")
}
guard let mainGroup = objects.groups.getReference(project.mainGroup) else {
throw failure("Couldn't find main group")
}
return mainGroup
}
private func getFileReference(group: PBXGroup, paths: [String], names: [String]) -> ObjectReference<PBXFileReference>? {
guard !paths.isEmpty else { return nil }
let path = paths.first!
let name = names.first!
let restOfPath = Array(paths.dropFirst())
let restOfName = Array(names.dropFirst())
if restOfPath.isEmpty {
let fileReferences: [ObjectReference<PBXFileReference>] = group.children.compactMap { reference in
if let fileReference = self.objects.fileReferences.getReference(reference) {
return ObjectReference(reference: reference, object: fileReference)
} else {
return nil
}
}
return fileReferences.first { $0.object.path == path && $0.object.nameOrPath == name }
} else {
let groups = group.children.compactMap { self.objects.groups.getReference($0) }
guard let group = groups.first(where: { $0.path == path && $0.nameOrPath == name }) else { return nil }
return getFileReference(group: group, paths: restOfPath, names: restOfName)
}
}
}
+43 -39
View File
@@ -3,46 +3,31 @@ import ProjectSpec
import Spectre
import XcodeGenKit
import xcproj
import XCTest
func projectSpecTests() {
class ProjectSpecTests: XCTestCase {
describe("ProjectSpec") {
func testTargetType() {
describe {
let framework = Target(
name: "MyFramework",
type: .framework,
platform: .iOS,
settings: Settings(buildSettings: ["SETTING_2": "VALUE"])
)
let staticLibrary = Target(
name: "MyStaticLibrary",
type: .staticLibrary,
platform: .iOS,
settings: Settings(buildSettings: ["SETTING_2": "VALUE"])
)
let dynamicLibrary = Target(
name: "MyDynamicLibrary",
type: .dynamicLibrary,
platform: .iOS,
settings: Settings(buildSettings: ["SETTING_2": "VALUE"])
)
func expectValidationError(_ project: Project, _ expectedError: SpecValidationError.ValidationError) throws {
do {
try project.validate()
} catch let error as SpecValidationError {
if !error.errors
.contains(where: { $0.description == expectedError.description }) {
throw failure("Supposed to fail with:\n\(expectedError)\nbut got:\n\(error.errors.map { $0.description }.joined(separator: "\n"))")
}
return
} catch {
throw failure("Supposed to fail with \"\(expectedError)\"")
}
throw failure("Supposed to fail with \"\(expectedError)\"")
}
$0.describe("Types") {
let framework = Target(
name: "MyFramework",
type: .framework,
platform: .iOS,
settings: Settings(buildSettings: ["SETTING_2": "VALUE"])
)
let staticLibrary = Target(
name: "MyStaticLibrary",
type: .staticLibrary,
platform: .iOS,
settings: Settings(buildSettings: ["SETTING_2": "VALUE"])
)
let dynamicLibrary = Target(
name: "MyDynamicLibrary",
type: .dynamicLibrary,
platform: .iOS,
settings: Settings(buildSettings: ["SETTING_2": "VALUE"])
)
$0.it("is a framework when it has the right extension") {
try expect(framework.type.isFramework).to.beTrue()
}
@@ -52,8 +37,10 @@ func projectSpecTests() {
try expect(dynamicLibrary.type.isLibrary).to.beTrue()
}
}
}
$0.describe("Deployment Target") {
func testDeploymentTarget() {
describe {
$0.it("has correct build setting") {
try expect(Platform.iOS.deploymentTargetSetting) == "IPHONEOS_DEPLOYMENT_TARGET"
@@ -77,8 +64,10 @@ func projectSpecTests() {
try expect(Version(2.1).deploymentTarget) == "2.1"
}
}
}
$0.describe("Validation") {
func testValidation() {
describe {
let baseProject = Project(basePath: "", name: "", configs: [Config(name: "invalid")])
let invalidSettings = Settings(
@@ -176,3 +165,18 @@ func projectSpecTests() {
}
}
}
fileprivate func expectValidationError(_ project: Project, _ expectedError: SpecValidationError.ValidationError, file: String = #file, line: Int = #line) throws {
do {
try project.validate()
} catch let error as SpecValidationError {
if !error.errors
.contains(where: { $0.description == expectedError.description }) {
throw failure("Supposed to fail with:\n\(expectedError)\nbut got:\n\(error.errors.map { $0.description }.joined(separator: "\n"))", file: file, line: line)
}
return
} catch {
throw failure("Supposed to fail with \"\(expectedError)\"", file: file, line: line)
}
throw failure("Supposed to fail with \"\(expectedError)\"", file: file, line: line)
}
@@ -0,0 +1,541 @@
import PathKit
import ProjectSpec
import Spectre
import XcodeGenKit
import xcproj
import XCTest
import Yams
class SourceGeneratorTests: XCTestCase {
func testSourceGenerator() {
describe {
let directoryPath = Path("TestDirectory")
let outOfRootPath = Path("OtherDirectory")
func createDirectories(_ directories: String) throws {
let yaml = try Yams.load(yaml: directories)!
func getFiles(_ file: Any, path: Path) -> [Path] {
if let array = file as? [Any] {
return array.flatMap { getFiles($0, path: path) }
} else if let string = file as? String {
return [path + string]
} else if let dictionary = file as? [String: Any] {
var array: [Path] = []
for (key, value) in dictionary {
array += getFiles(value, path: path + key)
}
return array
} else {
return []
}
}
let files = getFiles(yaml, path: directoryPath).filter { $0.extension != nil }
for file in files {
try file.parent().mkpath()
try file.write("")
}
}
func removeDirectories() {
try? directoryPath.delete()
try? outOfRootPath.delete()
}
$0.before {
removeDirectories()
}
$0.after {
removeDirectories()
}
$0.it("generates source groups") {
let directories = """
Sources:
A:
- a.swift
- B:
- b.swift
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try project.generatePbxProj()
try pbxProj.expectFile(paths: ["Sources", "A", "a.swift"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["Sources", "A", "B", "b.swift"], buildPhase: .sources)
}
$0.it("supports frameworks in sources") {
let directories = """
Sources:
- Foo.framework
- Bar.swift
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try project.generatePbxProj()
try pbxProj.expectFile(paths: ["Sources", "Bar.swift"], buildPhase: .sources)
let buildPhase = pbxProj.objects.copyFilesBuildPhases.referenceValues.first
try expect(buildPhase?.dstSubfolderSpec) == .frameworks
let fileReference = pbxProj.getFileReference(
paths: ["Sources", "Foo.framework"],
names: ["Sources", "Foo.framework"]
)?.reference ?? ""
let buildFile = pbxProj.objects.buildFiles.objectReferences
.first(where: { $0.object.fileRef == fileReference })?.reference ?? ""
try expect(buildPhase?.files.count) == 1
try expect(buildPhase?.files.contains(buildFile)) == true
}
$0.it("generates core data models") {
let directories = """
Sources:
model.xcdatamodeld:
- model.xcdatamodel
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try project.generatePbxProj()
guard let fileReference = pbxProj.objects.fileReferences.first(where: { $0.value.nameOrPath == "model.xcdatamodel" }) else {
throw failure("Couldn't find model file reference")
}
guard let versionGroup = pbxProj.objects.versionGroups.values.first else {
throw failure("Couldn't find version group")
}
try expect(versionGroup.currentVersion) == fileReference.key
try expect(versionGroup.children) == [fileReference.key]
try expect(versionGroup.path) == "model.xcdatamodeld"
try expect(fileReference.value.path) == "model.xcdatamodel"
}
$0.it("handles duplicate names") {
let directories = """
Sources:
- a.swift
- a:
- a.swift
- a:
- a.swift
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"])
let project = Project(
basePath: directoryPath,
name: "Test",
targets: [target],
fileGroups: ["Sources"]
)
let pbxProj = try project.generatePbxProj()
try pbxProj.expectFile(paths: ["Sources", "a.swift"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["Sources", "a", "a.swift"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["Sources", "a", "a", "a.swift"], buildPhase: .sources)
}
$0.it("renames sources") {
let directories = """
Sources:
- a.swift
OtherSource:
- b.swift
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
TargetSource(path: "Sources", name: "NewSource"),
TargetSource(path: "OtherSource/b.swift", name: "c.swift"),
])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try project.generatePbxProj()
try pbxProj.expectFile(paths: ["Sources", "a.swift"], names: ["NewSource", "a.swift"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["OtherSource", "b.swift"], names: ["OtherSource", "c.swift"], buildPhase: .sources)
}
$0.it("excludes sources") {
let directories = """
Sources:
- A:
- a.swift
- B:
- b.swift
- b.ignored
- a.ignored
- B:
- b.swift
- D:
- d.h
- d.m
- E:
- e.jpg
- e.h
- e.m
- F:
- f.swift
- G:
- H:
- h.swift
- types:
- a.swift
- a.m
- a.h
- a.x
- numbers:
- file1.a
- file2.a
- file3.a
- file4.a
- partial:
- file_part
- ignore.file
- a.ignored
- project.xcodeproj:
- project.pbxproj
"""
try createDirectories(directories)
let excludes = [
"B",
"d.m",
"E/F/*.swift",
"G/H/",
"types/*.[hx]",
"numbers/file[2-3].a",
"partial/*_part",
"ignore.file",
"*.ignored",
"*.xcodeproj",
// not supported
// "**/*.ignored",
]
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [TargetSource(path: "Sources", excludes: excludes)])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try project.generatePbxProj()
try pbxProj.expectFile(paths: ["Sources", "A", "a.swift"])
try pbxProj.expectFile(paths: ["Sources", "D", "d.h"])
try pbxProj.expectFile(paths: ["Sources", "D", "d.m"])
try pbxProj.expectFile(paths: ["Sources", "E", "e.jpg"])
try pbxProj.expectFile(paths: ["Sources", "E", "e.m"])
try pbxProj.expectFile(paths: ["Sources", "E", "e.h"])
try pbxProj.expectFile(paths: ["Sources", "types", "a.swift"])
try pbxProj.expectFile(paths: ["Sources", "numbers", "file1.a"])
try pbxProj.expectFile(paths: ["Sources", "numbers", "file4.a"])
try pbxProj.expectFileMissing(paths: ["Sources", "B", "b.swift"])
try pbxProj.expectFileMissing(paths: ["Sources", "E", "F", "f.swift"])
try pbxProj.expectFileMissing(paths: ["Sources", "G", "H", "h.swift"])
try pbxProj.expectFileMissing(paths: ["Sources", "types", "a.h"])
try pbxProj.expectFileMissing(paths: ["Sources", "types", "a.x"])
try pbxProj.expectFileMissing(paths: ["Sources", "numbers", "file2.a"])
try pbxProj.expectFileMissing(paths: ["Sources", "numbers", "file3.a"])
try pbxProj.expectFileMissing(paths: ["Sources", "partial", "file_part"])
try pbxProj.expectFileMissing(paths: ["Sources", "a.ignored"])
try pbxProj.expectFileMissing(paths: ["Sources", "ignore.file"])
try pbxProj.expectFileMissing(paths: ["Sources", "project.xcodeproj"])
}
$0.it("generates file sources") {
let directories = """
Sources:
A:
- a.swift
- Assets.xcassets
- B:
- b.swift
- c.jpg
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
"Sources/A/a.swift",
"Sources/A/B/b.swift",
"Sources/A/Assets.xcassets",
"Sources/A/B/c.jpg",
])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try project.generatePbxProj()
try pbxProj.expectFile(paths: ["Sources/A", "a.swift"], names: ["A", "a.swift"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["Sources/A/B", "b.swift"], names: ["B", "b.swift"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["Sources/A/B", "c.jpg"], names: ["B", "c.jpg"], buildPhase: .resources)
try pbxProj.expectFile(paths: ["Sources/A", "Assets.xcassets"], names: ["A", "Assets.xcassets"], buildPhase: .resources)
}
$0.it("generates shared sources") {
let directories = """
Sources:
A:
- a.swift
- B:
- b.swift
- c.jpg
"""
try createDirectories(directories)
let target1 = Target(name: "Test1", type: .framework, platform: .iOS, sources: ["Sources"])
let target2 = Target(name: "Test2", type: .framework, platform: .tvOS, sources: ["Sources"])
let project = Project(basePath: directoryPath, name: "Test", targets: [target1, target2])
_ = try project.generatePbxProj()
// TODO: check there are build files for both targets
}
$0.it("generates intermediate groups") {
let directories = """
Sources:
A:
- b.swift
F:
- G:
- h.swift
"""
try createDirectories(directories)
let outOfSourceFile = outOfRootPath + "C/D/e.swift"
try outOfSourceFile.parent().mkpath()
try outOfSourceFile.write("")
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
"Sources/A/b.swift",
"Sources/F/G/h.swift",
"../OtherDirectory/C/D/e.swift",
])
let options = SpecOptions(createIntermediateGroups: true)
let project = Project(basePath: directoryPath, name: "Test", targets: [target], options: options)
let pbxProj = try project.generatePbxProj()
try pbxProj.expectFile(paths: ["Sources", "A", "b.swift"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["Sources", "F", "G", "h.swift"], buildPhase: .sources)
try pbxProj.expectFile(paths: [(outOfRootPath + "C/D").string, "e.swift"], names: ["D", "e.swift"], buildPhase: .sources)
}
$0.it("generates folder references") {
let directories = """
Sources:
A:
- a.resource
- b.resource
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
TargetSource(path: "Sources/A", type: .folder),
])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try project.generatePbxProj()
try pbxProj.expectFile(paths: ["Sources/A"], names: ["A"], buildPhase: .resources)
try pbxProj.expectFileMissing(paths: ["Sources", "A", "a.swift"])
}
$0.it("adds files to correct build phase") {
let directories = """
A:
- file.swift
- file.xcassets
- file.h
- Info.plist
- file.xcconfig
B:
- file.swift
- file.xcassets
- file.h
- Info.plist
- file.xcconfig
C:
- file.swift
- file.m
- file.mm
- file.cpp
- file.c
- file.S
- file.h
- file.hh
- file.hpp
- file.ipp
- file.tpp
- file.hxx
- file.def
- file.xcconfig
- file.entitlements
- file.gpx
- file.apns
- file.123
- file.xcassets
- Info.plist
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .framework, platform: .iOS, sources: [
TargetSource(path: "A", buildPhase: .resources),
TargetSource(path: "B", buildPhase: .none),
TargetSource(path: "C", buildPhase: nil),
])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try project.generatePbxProj()
try pbxProj.expectFile(paths: ["A", "file.swift"], buildPhase: .resources)
try pbxProj.expectFile(paths: ["A", "file.xcassets"], buildPhase: .resources)
try pbxProj.expectFile(paths: ["A", "file.h"], buildPhase: .resources)
try pbxProj.expectFile(paths: ["A", "Info.plist"], buildPhase: .none)
try pbxProj.expectFile(paths: ["A", "file.xcconfig"], buildPhase: .resources)
try pbxProj.expectFile(paths: ["B", "file.swift"], buildPhase: .none)
try pbxProj.expectFile(paths: ["B", "file.xcassets"], buildPhase: .none)
try pbxProj.expectFile(paths: ["B", "file.h"], buildPhase: .none)
try pbxProj.expectFile(paths: ["B", "Info.plist"], buildPhase: .none)
try pbxProj.expectFile(paths: ["B", "file.xcconfig"], buildPhase: .none)
try pbxProj.expectFile(paths: ["C", "file.swift"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["C", "file.m"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["C", "file.mm"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["C", "file.cpp"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["C", "file.c"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["C", "file.S"], buildPhase: .sources)
try pbxProj.expectFile(paths: ["C", "file.h"], buildPhase: .headers)
try pbxProj.expectFile(paths: ["C", "file.hh"], buildPhase: .headers)
try pbxProj.expectFile(paths: ["C", "file.hpp"], buildPhase: .headers)
try pbxProj.expectFile(paths: ["C", "file.ipp"], buildPhase: .headers)
try pbxProj.expectFile(paths: ["C", "file.tpp"], buildPhase: .headers)
try pbxProj.expectFile(paths: ["C", "file.hxx"], buildPhase: .headers)
try pbxProj.expectFile(paths: ["C", "file.def"], buildPhase: .headers)
try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: .none)
try pbxProj.expectFile(paths: ["C", "file.entitlements"], buildPhase: .none)
try pbxProj.expectFile(paths: ["C", "file.gpx"], buildPhase: .none)
try pbxProj.expectFile(paths: ["C", "file.apns"], buildPhase: .none)
try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: .none)
try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: .none)
try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: .none)
try pbxProj.expectFile(paths: ["C", "file.xcassets"], buildPhase: .resources)
try pbxProj.expectFile(paths: ["C", "file.123"], buildPhase: .resources)
try pbxProj.expectFile(paths: ["C", "Info.plist"], buildPhase: .none)
}
$0.it("duplicate TargetSource is included once in sources build phase") {
let directories = """
Sources:
A:
- a.swift
"""
try createDirectories(directories)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
"Sources/A/a.swift",
"Sources/A/a.swift",
])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try project.generatePbxProj()
try pbxProj.expectFile(paths: ["Sources/A", "a.swift"], names: ["A", "a.swift"], buildPhase: .sources)
let sourcesBuildPhase = pbxProj.objects.buildPhases
.first(where: { $0.1.buildPhase == BuildPhase.sources })!
.value
try expect(sourcesBuildPhase.files.count) == 1
}
}
}
}
extension PBXProj {
/// expect a file within groups of the paths, using optional different names
func expectFile(paths: [String], names: [String]? = nil, buildPhase: TargetSource.BuildPhase? = nil, file: String = #file, line: Int = #line) throws {
guard let fileReference = getFileReference(paths: paths, names: names ?? paths) else {
var error = "Could not find file at path \(paths.joined(separator: "/").quoted)"
if let names = names, names != paths {
error += " and name \(names.joined(separator: "/").quoted)"
}
throw failure(error, file: file, line: line)
}
if let buildPhase = buildPhase {
let buildFile = objects.buildFiles.objectReferences
.first(where: { $0.object.fileRef == fileReference.reference })
let actualBuildPhase = buildFile
.flatMap { buildFile in objects.buildPhases.referenceValues.first { $0.files.contains(buildFile.reference) } }?.buildPhase
var error: String?
if let buildPhase = buildPhase.buildPhase {
if actualBuildPhase != buildPhase {
if let actualBuildPhase = actualBuildPhase {
error = "is in the \(actualBuildPhase.rawValue) build phase instead of the expected \(buildPhase.rawValue.quoted)"
} else {
error = "isn't in a build phase when it's expected to be in \(buildPhase.rawValue.quoted)"
}
}
} else if let actualBuildPhase = actualBuildPhase {
error = "is in the \(actualBuildPhase.rawValue.quoted) build phase when it's expected to not be in any"
}
if let error = error {
throw failure("File \(paths.joined(separator: "/").quoted) \(error)", file: file, line: line)
}
}
}
/// expect a missing file within groups of the paths, using optional different names
func expectFileMissing(paths: [String], names: [String]? = nil, file: String = #file, line: Int = #line) throws {
let names = names ?? paths
if getFileReference(paths: paths, names: names) != nil {
throw failure("Found unexpected file at path \(paths.joined(separator: "/").quoted) and name \(paths.joined(separator: "/").quoted)", file: file, line: line)
}
}
func getFileReference(paths: [String], names: [String], file: String = #file, line: Int = #line) -> ObjectReference<PBXFileReference>? {
guard let project = objects.projects.first?.value else { return nil }
guard let mainGroup = objects.groups.getReference(project.mainGroup) else { return nil }
return getFileReference(group: mainGroup, paths: paths, names: names)
}
func getMainGroup(function: String = #function, file: String = #file, line: Int = #line) throws -> PBXGroup {
guard let project = objects.projects.first?.value else {
throw failure("Couldn't find project", file: file, line: line)
}
guard let mainGroup = objects.groups.getReference(project.mainGroup) else {
throw failure("Couldn't find main group", file: file, line: line)
}
return mainGroup
}
private func getFileReference(group: PBXGroup, paths: [String], names: [String]) -> ObjectReference<PBXFileReference>? {
guard !paths.isEmpty else { return nil }
let path = paths.first!
let name = names.first!
let restOfPath = Array(paths.dropFirst())
let restOfName = Array(names.dropFirst())
if restOfPath.isEmpty {
let fileReferences: [ObjectReference<PBXFileReference>] = group.children.compactMap { reference in
if let fileReference = self.objects.fileReferences.getReference(reference) {
return ObjectReference(reference: reference, object: fileReference)
} else {
return nil
}
}
return fileReferences.first { $0.object.path == path && $0.object.nameOrPath == name }
} else {
let groups = group.children.compactMap { self.objects.groups.getReference($0) }
guard let group = groups.first(where: { $0.path == path && $0.nameOrPath == name }) else { return nil }
return getFileReference(group: group, paths: restOfPath, names: restOfName)
}
}
}
+386 -375
View File
@@ -4,417 +4,428 @@ import ProjectSpec
import Spectre
import XcodeGenKit
import xcproj
import XCTest
func projectLoadingTests() {
class SpecLoadingTests: XCTestCase {
@discardableResult
func getProjectSpec(_ project: [String: Any]) throws -> Project {
var projectDictionary: [String: Any] = ["name": "test"]
for (key, value) in project {
projectDictionary[key] = value
}
return try Project(basePath: "", jsonDictionary: projectDictionary)
}
func testSpecLoader() {
describe {
$0.it("merges includes") {
let path = fixturePath + "include_test.yml"
let project = try Project(path: path)
func expectSpecError(_ project: [String: Any], _ expectedError: SpecParsingError) throws {
try expectError(expectedError) {
try getProjectSpec(project)
}
}
try expect(project.name) == "NewName"
try expect(project.settingGroups) == [
"test": Settings(dictionary: ["MY_SETTING1": "NEW VALUE", "MY_SETTING2": "VALUE2", "MY_SETTING3": "VALUE3"]),
"new": Settings(dictionary: ["MY_SETTING": "VALUE"]),
"toReplace": Settings(dictionary: ["MY_SETTING2": "VALUE2"]),
]
try expect(project.targets) == [
Target(name: "IncludedTargetNew", type: .application, platform: .tvOS, sources: ["NewSource"]),
Target(name: "NewTarget", type: .application, platform: .iOS),
]
}
func expectTargetError(_ target: [String: Any], _ expectedError: SpecParsingError) throws {
try expectError(expectedError) {
_ = try Target(name: "test", jsonDictionary: target)
}
}
let validTarget: [String: Any] = ["type": "application", "platform": "iOS"]
let invalid = "invalid"
describe("Spec Loader") {
$0.it("merges includes") {
let path = fixturePath + "include_test.yml"
let project = try Project(path: path)
try expect(project.name) == "NewName"
try expect(project.settingGroups) == [
"test": Settings(dictionary: ["MY_SETTING1": "NEW VALUE", "MY_SETTING2": "VALUE2", "MY_SETTING3": "VALUE3"]),
"new": Settings(dictionary: ["MY_SETTING": "VALUE"]),
"toReplace": Settings(dictionary: ["MY_SETTING2": "VALUE2"]),
]
try expect(project.targets) == [
Target(name: "IncludedTargetNew", type: .application, platform: .tvOS, sources: ["NewSource"]),
Target(name: "NewTarget", type: .application, platform: .iOS),
]
}
$0.it("parses yaml types") {
let path = fixturePath + "yaml.yml"
let dictionary = try loadYamlDictionary(path: path)
let expectedDictionary: [String: Any] = [
"true": true,
"false": false,
"yes": true,
"no": false,
"yesQuote": "YES",
"noQuote": "NO",
"int": 1,
"intQuote": "1",
"float": 3.2,
"floatQuote": "10.10",
"string": "hello",
"stringQuote": "hello",
"space": " ",
"empty": "",
"emptyQuote": "",
"emptyDictionary": [String: Any](),
"arrayLiteral": [1, 2],
"arrayList": [1, 2],
]
for (key, expectedValue) in expectedDictionary {
guard let parsedValue = dictionary[key] else {
throw failure("\(key) does not exist")
$0.it("parses yaml types") {
let path = fixturePath + "yaml.yml"
let dictionary = try loadYamlDictionary(path: path)
let expectedDictionary: [String: Any] = [
"true": true,
"false": false,
"yes": true,
"no": false,
"yesQuote": "YES",
"noQuote": "NO",
"int": 1,
"intQuote": "1",
"float": 3.2,
"floatQuote": "10.10",
"string": "hello",
"stringQuote": "hello",
"space": " ",
"empty": "",
"emptyQuote": "",
"emptyDictionary": [String: Any](),
"arrayLiteral": [1, 2],
"arrayList": [1, 2],
]
for (key, expectedValue) in expectedDictionary {
guard let parsedValue = dictionary[key] else {
throw failure("\(key) does not exist")
}
if String(describing: expectedValue) != String(describing: parsedValue) {
throw failure("\(key): \(parsedValue) does not equal \(expectedValue)")
}
}
if String(describing: expectedValue) != String(describing: parsedValue) {
throw failure("\(key): \(parsedValue) does not equal \(expectedValue)")
if !(dictionary as NSDictionary).isEqual(expectedDictionary) {
throw failure("parsed yaml types don't match:\n\nParsed:\n\t\(dictionary.map { "\($0.key): \($0.value)" }.joined(separator: "\n\t"))\nExpected:\n\t\(expectedDictionary.map { "\($0.key): \($0.value)" }.joined(separator: "\n\t"))")
}
}
if !(dictionary as NSDictionary).isEqual(expectedDictionary) {
throw failure("parsed yaml types don't match:\n\nParsed:\n\t\(dictionary.map { "\($0.key): \($0.value)" }.joined(separator: "\n\t"))\nExpected:\n\t\(expectedDictionary.map { "\($0.key): \($0.value)" }.joined(separator: "\n\t"))")
}
}
func testSpecLoaderLoadingJSON() {
describe {
$0.it("merges includes") {
let path = fixturePath + "include_test.json"
let project = try Project(path: path)
try expect(project.name) == "NewName"
try expect(project.settingGroups) == [
"test": Settings(dictionary: ["MY_SETTING1": "NEW VALUE", "MY_SETTING2": "VALUE2", "MY_SETTING3": "VALUE3"]),
"new": Settings(dictionary: ["MY_SETTING": "VALUE"]),
"toReplace": Settings(dictionary: ["MY_SETTING2": "VALUE2"]),
]
try expect(project.targets) == [
Target(name: "IncludedTargetNew", type: .application, platform: .tvOS, sources: ["NewSource"]),
Target(name: "NewTarget", type: .application, platform: .iOS),
]
}
}
}
describe("Spec Loader JSON") {
$0.it("merges includes") {
let path = fixturePath + "include_test.json"
let project = try Project(path: path)
func testProjectSpecParser() {
let validTarget: [String: Any] = ["type": "application", "platform": "iOS"]
let invalid = "invalid"
try expect(project.name) == "NewName"
try expect(project.settingGroups) == [
"test": Settings(dictionary: ["MY_SETTING1": "NEW VALUE", "MY_SETTING2": "VALUE2", "MY_SETTING3": "VALUE3"]),
"new": Settings(dictionary: ["MY_SETTING": "VALUE"]),
"toReplace": Settings(dictionary: ["MY_SETTING2": "VALUE2"]),
]
try expect(project.targets) == [
Target(name: "IncludedTargetNew", type: .application, platform: .tvOS, sources: ["NewSource"]),
Target(name: "NewTarget", type: .application, platform: .iOS),
]
}
}
describe {
describe("Project Spec Parser") {
$0.it("fails with incorrect platform") {
var target = validTarget
target["platform"] = invalid
try expectTargetError(target, .unknownTargetPlatform(invalid))
}
$0.it("fails with incorrect platform") {
var target = validTarget
target["platform"] = invalid
try expectTargetError(target, .unknownTargetPlatform(invalid))
}
$0.it("fails with incorrect product type") {
var target = validTarget
target["type"] = invalid
try expectTargetError(target, .unknownTargetType(invalid))
}
$0.it("fails with incorrect product type") {
var target = validTarget
target["type"] = invalid
try expectTargetError(target, .unknownTargetType(invalid))
}
$0.it("fails with invalid dependency") {
var target = validTarget
target["dependencies"] = [[invalid: "name"]]
try expectTargetError(target, .invalidDependency([invalid: "name"]))
}
$0.it("fails with invalid dependency") {
var target = validTarget
target["dependencies"] = [[invalid: "name"]]
try expectTargetError(target, .invalidDependency([invalid: "name"]))
}
$0.it("parses sources") {
var targetDictionary1 = validTarget
targetDictionary1["sources"] = [
"sourceString",
["path": "sourceObject"],
["path": "sourceWithFlagsArray", "compilerFlags": ["-Werror"]],
["path": "sourceWithFlagsString", "compilerFlags": "-Werror -Wextra"],
["path": "sourceWithExcludes", "excludes": ["Foo.swift"]],
["path": "sourceWithFileType", "type": "file"],
["path": "sourceWithGroupType", "type": "group"],
["path": "sourceWithFolderType", "type": "folder"],
]
var targetDictionary2 = validTarget
targetDictionary2["sources"] = "source3"
$0.it("parses sources") {
var targetDictionary1 = validTarget
targetDictionary1["sources"] = [
"sourceString",
["path": "sourceObject"],
["path": "sourceWithFlagsArray", "compilerFlags": ["-Werror"]],
["path": "sourceWithFlagsString", "compilerFlags": "-Werror -Wextra"],
["path": "sourceWithExcludes", "excludes": ["Foo.swift"]],
["path": "sourceWithFileType", "type": "file"],
["path": "sourceWithGroupType", "type": "group"],
["path": "sourceWithFolderType", "type": "folder"],
]
var targetDictionary2 = validTarget
targetDictionary2["sources"] = "source3"
let target1 = try Target(name: "test", jsonDictionary: targetDictionary1)
let target2 = try Target(name: "test", jsonDictionary: targetDictionary2)
let target1 = try Target(name: "test", jsonDictionary: targetDictionary1)
let target2 = try Target(name: "test", jsonDictionary: targetDictionary2)
let target1SourcesExpect = [
TargetSource(path: "sourceString"),
TargetSource(path: "sourceObject"),
TargetSource(path: "sourceWithFlagsArray", compilerFlags: ["-Werror"]),
TargetSource(path: "sourceWithFlagsString", compilerFlags: ["-Werror", "-Wextra"]),
TargetSource(path: "sourceWithExcludes", excludes: ["Foo.swift"]),
TargetSource(path: "sourceWithFileType", type: .file),
TargetSource(path: "sourceWithGroupType", type: .group),
TargetSource(path: "sourceWithFolderType", type: .folder),
]
let target1SourcesExpect = [
TargetSource(path: "sourceString"),
TargetSource(path: "sourceObject"),
TargetSource(path: "sourceWithFlagsArray", compilerFlags: ["-Werror"]),
TargetSource(path: "sourceWithFlagsString", compilerFlags: ["-Werror", "-Wextra"]),
TargetSource(path: "sourceWithExcludes", excludes: ["Foo.swift"]),
TargetSource(path: "sourceWithFileType", type: .file),
TargetSource(path: "sourceWithGroupType", type: .group),
TargetSource(path: "sourceWithFolderType", type: .folder),
]
try expect(target1.sources) == target1SourcesExpect
try expect(target2.sources) == ["source3"]
}
try expect(target1.sources) == target1SourcesExpect
try expect(target2.sources) == ["source3"]
}
$0.it("parses target dependencies") {
var targetDictionary = validTarget
targetDictionary["dependencies"] = [
["target": "name", "embed": false],
["carthage": "name"],
["framework": "path"],
]
let target = try Target(name: "test", jsonDictionary: targetDictionary)
try expect(target.dependencies.count) == 3
try expect(target.dependencies[0]) == Dependency(type: .target, reference: "name", embed: false)
try expect(target.dependencies[1]) == Dependency(type: .carthage, reference: "name")
try expect(target.dependencies[2]) == Dependency(type: .framework, reference: "path")
}
$0.it("parses target dependencies") {
var targetDictionary = validTarget
targetDictionary["dependencies"] = [
["target": "name", "embed": false],
["carthage": "name"],
["framework": "path"],
]
let target = try Target(name: "test", jsonDictionary: targetDictionary)
try expect(target.dependencies.count) == 3
try expect(target.dependencies[0]) == Dependency(type: .target, reference: "name", embed: false)
try expect(target.dependencies[1]) == Dependency(type: .carthage, reference: "name")
try expect(target.dependencies[2]) == Dependency(type: .framework, reference: "path")
}
$0.it("parses cross platform targets") {
let targetDictionary: [String: Any] = [
"platform": ["iOS", "tvOS"],
"type": "framework",
"sources": ["Framework", "Framework $platform"],
"settings": ["SETTING": "value_$platform"],
]
$0.it("parses cross platform targets") {
let targetDictionary: [String: Any] = [
"platform": ["iOS", "tvOS"],
"type": "framework",
"sources": ["Framework", "Framework $platform"],
"settings": ["SETTING": "value_$platform"],
]
let project = try getProjectSpec(["targets": ["Framework": targetDictionary]])
var target_iOS = Target(name: "Framework_iOS", type: .framework, platform: .iOS)
var target_tvOS = Target(name: "Framework_tvOS", type: .framework, platform: .tvOS)
let project = try getProjectSpec(["targets": ["Framework": targetDictionary]])
var target_iOS = Target(name: "Framework_iOS", type: .framework, platform: .iOS)
var target_tvOS = Target(name: "Framework_tvOS", type: .framework, platform: .tvOS)
target_iOS.sources = ["Framework", "Framework iOS"]
target_tvOS.sources = ["Framework", "Framework tvOS"]
target_iOS.settings = ["PRODUCT_NAME": "Framework", "SETTING": "value_iOS"]
target_tvOS.settings = ["PRODUCT_NAME": "Framework", "SETTING": "value_tvOS"]
target_iOS.sources = ["Framework", "Framework iOS"]
target_tvOS.sources = ["Framework", "Framework tvOS"]
target_iOS.settings = ["PRODUCT_NAME": "Framework", "SETTING": "value_iOS"]
target_tvOS.settings = ["PRODUCT_NAME": "Framework", "SETTING": "value_tvOS"]
try expect(project.targets.count) == 2
try expect(project.targets) == [target_iOS, target_tvOS]
}
try expect(project.targets.count) == 2
try expect(project.targets) == [target_iOS, target_tvOS]
}
$0.it("parses target schemes") {
var targetDictionary = validTarget
targetDictionary["scheme"] = [
"testTargets": ["t1", "t2"],
"configVariants": ["dev", "app-store"],
"commandLineArguments": [
"ENV1": true,
],
"gatherCoverageData": true,
"environmentVariables": [
"TEST_VAR": "TEST_VAL",
],
"preActions": [
[
"script": "dothing",
"name": "Do Thing",
"settingsTarget": "test",
$0.it("parses target schemes") {
var targetDictionary = validTarget
targetDictionary["scheme"] = [
"testTargets": ["t1", "t2"],
"configVariants": ["dev", "app-store"],
"commandLineArguments": [
"ENV1": true,
],
],
"postActions": [
[
"script": "hello",
],
],
]
let target = try Target(name: "test", jsonDictionary: targetDictionary)
let scheme = TargetScheme(
testTargets: ["t1", "t2"],
configVariants: ["dev", "app-store"],
gatherCoverageData: true,
commandLineArguments: ["ENV1": true],
environmentVariables: [XCScheme.EnvironmentVariable(variable: "TEST_VAR", value: "TEST_VAL", enabled: true)],
preActions: [.init(name: "Do Thing", script: "dothing", settingsTarget: "test")],
postActions: [.init(name: "Run Script", script: "hello")]
)
try expect(target.scheme) == scheme
}
$0.it("parses schemes") {
let schemeDictionary: [String: Any] = [
"build": [
"parallelizeBuild": false,
"buildImplicitDependencies": false,
"targets": [
"Target1": "all",
"Target2": "testing",
"Target3": "none",
"Target4": ["testing": true],
"Target5": ["testing": false],
"Target6": ["test", "analyze"],
"gatherCoverageData": true,
"environmentVariables": [
"TEST_VAR": "TEST_VAL",
],
"preActions": [
[
"script": "echo Before Build",
"name": "Before Build",
"settingsTarget": "Target1",
"script": "dothing",
"name": "Do Thing",
"settingsTarget": "test",
],
],
],
]
let scheme = try Scheme(name: "Scheme", jsonDictionary: schemeDictionary)
let expectedTargets: [Scheme.BuildTarget] = [
Scheme.BuildTarget(target: "Target1", buildTypes: BuildType.all),
Scheme.BuildTarget(target: "Target2", buildTypes: [.testing, .analyzing]),
Scheme.BuildTarget(target: "Target3", buildTypes: []),
Scheme.BuildTarget(target: "Target4", buildTypes: [.testing]),
Scheme.BuildTarget(target: "Target5", buildTypes: []),
Scheme.BuildTarget(target: "Target6", buildTypes: [.testing, .analyzing]),
]
try expect(scheme.name) == "Scheme"
try expect(scheme.build.targets) == expectedTargets
try expect(scheme.build.preActions.first?.script) == "echo Before Build"
try expect(scheme.build.preActions.first?.name) == "Before Build"
try expect(scheme.build.preActions.first?.settingsTarget) == "Target1"
try expect(scheme.build.parallelizeBuild) == false
try expect(scheme.build.buildImplicitDependencies) == false
}
$0.it("parses schemes variables") {
let schemeDictionary: [String: Any] = [
"build": [
"targets": ["Target1": "all"],
],
"run": [
"environmentVariables": [
["variable": "BOOL_TRUE", "value": true],
["variable": "BOOL_YES", "value": "YES"],
["variable": "ENVIRONMENT", "value": "VARIABLE"],
["variable": "OTHER_ENV_VAR", "value": "VAL", "isEnabled": false],
"postActions": [
[
"script": "hello",
],
],
],
"test": [
"environmentVariables": [
"BOOL_TRUE": true,
"BOOL_YES": "YES",
"TEST": "VARIABLE",
],
],
"profile": [
"config": "Release",
],
]
let scheme = try Scheme(name: "Scheme", jsonDictionary: schemeDictionary)
let expectedRunVariables = [
XCScheme.EnvironmentVariable(variable: "BOOL_TRUE", value: "YES", enabled: true),
XCScheme.EnvironmentVariable(variable: "BOOL_YES", value: "YES", enabled: true),
XCScheme.EnvironmentVariable(variable: "ENVIRONMENT", value: "VARIABLE", enabled: true),
XCScheme.EnvironmentVariable(variable: "OTHER_ENV_VAR", value: "VAL", enabled: false),
]
let expectedTestVariables = [
XCScheme.EnvironmentVariable(variable: "BOOL_TRUE", value: "YES", enabled: true),
XCScheme.EnvironmentVariable(variable: "BOOL_YES", value: "YES", enabled: true),
XCScheme.EnvironmentVariable(variable: "TEST", value: "VARIABLE", enabled: true),
]
try expect(scheme.run?.environmentVariables) == expectedRunVariables
try expect(scheme.test?.environmentVariables) == expectedTestVariables
try expect(scheme.profile?.config) == "Release"
try expect(scheme.profile?.environmentVariables.isEmpty) == true
}
$0.it("parses settings") {
let project = try Project(path: fixturePath + "settings_test.yml")
let buildSettings: BuildSettings = ["SETTING": "value"]
let configSettings: [String: Settings] = ["config1": Settings(buildSettings: ["SETTING1": "value"])]
let groups = ["preset1"]
let preset1 = Settings(buildSettings: buildSettings, configSettings: [:], groups: [])
let preset2 = Settings(buildSettings: [:], configSettings: configSettings, groups: [])
let preset3 = Settings(buildSettings: buildSettings, configSettings: configSettings, groups: [])
let preset4 = Settings(buildSettings: buildSettings, configSettings: [:], groups: [])
let preset5 = Settings(buildSettings: buildSettings, configSettings: [:], groups: groups)
let preset6 = Settings(buildSettings: buildSettings, configSettings: configSettings, groups: groups)
let preset7 = Settings(buildSettings: buildSettings, configSettings: ["config1": Settings(buildSettings: buildSettings, groups: groups)])
let preset8 = Settings(buildSettings: [:], configSettings: ["config1": Settings(configSettings: configSettings)])
try expect(project.settingGroups.count) == 8
try expect(project.settingGroups["preset1"]) == preset1
try expect(project.settingGroups["preset2"]) == preset2
try expect(project.settingGroups["preset3"]) == preset3
try expect(project.settingGroups["preset4"]) == preset4
try expect(project.settingGroups["preset5"]) == preset5
try expect(project.settingGroups["preset6"]) == preset6
try expect(project.settingGroups["preset7"]) == preset7
try expect(project.settingGroups["preset8"]) == preset8
}
$0.it("parses run scripts") {
var target = validTarget
let scripts: [[String: Any]] = [
["path": "script.sh"],
["script": "shell script\ndo thing", "name": "myscript", "inputFiles": ["file", "file2"], "outputFiles": ["file", "file2"], "shell": "bin/customshell", "runOnlyWhenInstalling": true],
]
target["prebuildScripts"] = scripts
target["postbuildScripts"] = scripts
let expectedScripts = [
BuildScript(script: .path("script.sh")),
BuildScript(script: .script("shell script\ndo thing"), name: "myscript", inputFiles: ["file", "file2"], outputFiles: ["file", "file2"], shell: "bin/customshell", runOnlyWhenInstalling: true),
]
let parsedTarget = try Target(name: "test", jsonDictionary: target)
try expect(parsedTarget.prebuildScripts) == expectedScripts
try expect(parsedTarget.postbuildScripts) == expectedScripts
}
$0.it("parses build rules") {
var target = validTarget
let buildRules: [[String: Any]] = [
[
"name": "My Rule",
"script": "my script",
"filePattern": "*.swift",
"outputFiles": ["file1", "file2"],
"outputFilesCompilerFlags": ["-a","-b"]
],
[
"compilerSpec": "apple.tool",
"fileType": "sourcecode.swift",
]
]
target["buildRules"] = buildRules
let expectedBuildRules = [
BuildRule(fileType: .pattern("*.swift"), action: .script("my script"), name: "My Rule", outputFiles: ["file1", "file2"], outputFilesCompilerFlags: ["-a","-b"]),
BuildRule(fileType: .type("sourcecode.swift"), action: .compilerSpec("apple.tool")),
]
let parsedTarget = try Target(name: "test", jsonDictionary: target)
try expect(parsedTarget.buildRules) == expectedBuildRules
}
let target = try Target(name: "test", jsonDictionary: targetDictionary)
$0.it("parses options") {
let options = SpecOptions(
carthageBuildPath: "../Carthage/Build",
carthageExecutablePath: "../bin/carthage",
createIntermediateGroups: true,
bundleIdPrefix: "com.test",
developmentLanguage: "ja",
deploymentTarget: DeploymentTarget(
iOS: "11.1",
tvOS: "10.0",
watchOS: "3.0",
macOS: "10.12.1"
let scheme = TargetScheme(
testTargets: ["t1", "t2"],
configVariants: ["dev", "app-store"],
gatherCoverageData: true,
commandLineArguments: ["ENV1": true],
environmentVariables: [XCScheme.EnvironmentVariable(variable: "TEST_VAR", value: "TEST_VAL", enabled: true)],
preActions: [.init(name: "Do Thing", script: "dothing", settingsTarget: "test")],
postActions: [.init(name: "Run Script", script: "hello")]
)
)
let expected = Project(basePath: "", name: "test", options: options)
let dictionary: [String: Any] = ["options": [
"carthageBuildPath": "../Carthage/Build",
"carthageExecutablePath": "../bin/carthage",
"bundleIdPrefix": "com.test",
"createIntermediateGroups": true,
"developmentLanguage": "ja",
"deploymentTarget": ["iOS": 11.1, "tvOS": 10.0, "watchOS": "3", "macOS": "10.12.1"],
]]
let parsedSpec = try getProjectSpec(dictionary)
try expect(parsedSpec) == expected
try expect(target.scheme) == scheme
}
$0.it("parses schemes") {
let schemeDictionary: [String: Any] = [
"build": [
"parallelizeBuild": false,
"buildImplicitDependencies": false,
"targets": [
"Target1": "all",
"Target2": "testing",
"Target3": "none",
"Target4": ["testing": true],
"Target5": ["testing": false],
"Target6": ["test", "analyze"],
],
"preActions": [
[
"script": "echo Before Build",
"name": "Before Build",
"settingsTarget": "Target1",
],
],
],
]
let scheme = try Scheme(name: "Scheme", jsonDictionary: schemeDictionary)
let expectedTargets: [Scheme.BuildTarget] = [
Scheme.BuildTarget(target: "Target1", buildTypes: BuildType.all),
Scheme.BuildTarget(target: "Target2", buildTypes: [.testing, .analyzing]),
Scheme.BuildTarget(target: "Target3", buildTypes: []),
Scheme.BuildTarget(target: "Target4", buildTypes: [.testing]),
Scheme.BuildTarget(target: "Target5", buildTypes: []),
Scheme.BuildTarget(target: "Target6", buildTypes: [.testing, .analyzing]),
]
try expect(scheme.name) == "Scheme"
try expect(scheme.build.targets) == expectedTargets
try expect(scheme.build.preActions.first?.script) == "echo Before Build"
try expect(scheme.build.preActions.first?.name) == "Before Build"
try expect(scheme.build.preActions.first?.settingsTarget) == "Target1"
try expect(scheme.build.parallelizeBuild) == false
try expect(scheme.build.buildImplicitDependencies) == false
}
$0.it("parses schemes variables") {
let schemeDictionary: [String: Any] = [
"build": [
"targets": ["Target1": "all"],
],
"run": [
"environmentVariables": [
["variable": "BOOL_TRUE", "value": true],
["variable": "BOOL_YES", "value": "YES"],
["variable": "ENVIRONMENT", "value": "VARIABLE"],
["variable": "OTHER_ENV_VAR", "value": "VAL", "isEnabled": false],
],
],
"test": [
"environmentVariables": [
"BOOL_TRUE": true,
"BOOL_YES": "YES",
"TEST": "VARIABLE",
],
],
"profile": [
"config": "Release",
],
]
let scheme = try Scheme(name: "Scheme", jsonDictionary: schemeDictionary)
let expectedRunVariables = [
XCScheme.EnvironmentVariable(variable: "BOOL_TRUE", value: "YES", enabled: true),
XCScheme.EnvironmentVariable(variable: "BOOL_YES", value: "YES", enabled: true),
XCScheme.EnvironmentVariable(variable: "ENVIRONMENT", value: "VARIABLE", enabled: true),
XCScheme.EnvironmentVariable(variable: "OTHER_ENV_VAR", value: "VAL", enabled: false),
]
let expectedTestVariables = [
XCScheme.EnvironmentVariable(variable: "BOOL_TRUE", value: "YES", enabled: true),
XCScheme.EnvironmentVariable(variable: "BOOL_YES", value: "YES", enabled: true),
XCScheme.EnvironmentVariable(variable: "TEST", value: "VARIABLE", enabled: true),
]
try expect(scheme.run?.environmentVariables) == expectedRunVariables
try expect(scheme.test?.environmentVariables) == expectedTestVariables
try expect(scheme.profile?.config) == "Release"
try expect(scheme.profile?.environmentVariables.isEmpty) == true
}
$0.it("parses settings") {
let project = try Project(path: fixturePath + "settings_test.yml")
let buildSettings: BuildSettings = ["SETTING": "value"]
let configSettings: [String: Settings] = ["config1": Settings(buildSettings: ["SETTING1": "value"])]
let groups = ["preset1"]
let preset1 = Settings(buildSettings: buildSettings, configSettings: [:], groups: [])
let preset2 = Settings(buildSettings: [:], configSettings: configSettings, groups: [])
let preset3 = Settings(buildSettings: buildSettings, configSettings: configSettings, groups: [])
let preset4 = Settings(buildSettings: buildSettings, configSettings: [:], groups: [])
let preset5 = Settings(buildSettings: buildSettings, configSettings: [:], groups: groups)
let preset6 = Settings(buildSettings: buildSettings, configSettings: configSettings, groups: groups)
let preset7 = Settings(buildSettings: buildSettings, configSettings: ["config1": Settings(buildSettings: buildSettings, groups: groups)])
let preset8 = Settings(buildSettings: [:], configSettings: ["config1": Settings(configSettings: configSettings)])
try expect(project.settingGroups.count) == 8
try expect(project.settingGroups["preset1"]) == preset1
try expect(project.settingGroups["preset2"]) == preset2
try expect(project.settingGroups["preset3"]) == preset3
try expect(project.settingGroups["preset4"]) == preset4
try expect(project.settingGroups["preset5"]) == preset5
try expect(project.settingGroups["preset6"]) == preset6
try expect(project.settingGroups["preset7"]) == preset7
try expect(project.settingGroups["preset8"]) == preset8
}
$0.it("parses run scripts") {
var target = validTarget
let scripts: [[String: Any]] = [
["path": "script.sh"],
["script": "shell script\ndo thing", "name": "myscript", "inputFiles": ["file", "file2"], "outputFiles": ["file", "file2"], "shell": "bin/customshell", "runOnlyWhenInstalling": true],
]
target["prebuildScripts"] = scripts
target["postbuildScripts"] = scripts
let expectedScripts = [
BuildScript(script: .path("script.sh")),
BuildScript(script: .script("shell script\ndo thing"), name: "myscript", inputFiles: ["file", "file2"], outputFiles: ["file", "file2"], shell: "bin/customshell", runOnlyWhenInstalling: true),
]
let parsedTarget = try Target(name: "test", jsonDictionary: target)
try expect(parsedTarget.prebuildScripts) == expectedScripts
try expect(parsedTarget.postbuildScripts) == expectedScripts
}
$0.it("parses build rules") {
var target = validTarget
let buildRules: [[String: Any]] = [
[
"name": "My Rule",
"script": "my script",
"filePattern": "*.swift",
"outputFiles": ["file1", "file2"],
"outputFilesCompilerFlags": ["-a", "-b"],
],
[
"compilerSpec": "apple.tool",
"fileType": "sourcecode.swift",
],
]
target["buildRules"] = buildRules
let expectedBuildRules = [
BuildRule(fileType: .pattern("*.swift"), action: .script("my script"), name: "My Rule", outputFiles: ["file1", "file2"], outputFilesCompilerFlags: ["-a", "-b"]),
BuildRule(fileType: .type("sourcecode.swift"), action: .compilerSpec("apple.tool")),
]
let parsedTarget = try Target(name: "test", jsonDictionary: target)
try expect(parsedTarget.buildRules) == expectedBuildRules
}
$0.it("parses options") {
let options = SpecOptions(
carthageBuildPath: "../Carthage/Build",
carthageExecutablePath: "../bin/carthage",
createIntermediateGroups: true,
bundleIdPrefix: "com.test",
developmentLanguage: "ja",
deploymentTarget: DeploymentTarget(
iOS: "11.1",
tvOS: "10.0",
watchOS: "3.0",
macOS: "10.12.1"
)
)
let expected = Project(basePath: "", name: "test", options: options)
let dictionary: [String: Any] = ["options": [
"carthageBuildPath": "../Carthage/Build",
"carthageExecutablePath": "../bin/carthage",
"bundleIdPrefix": "com.test",
"createIntermediateGroups": true,
"developmentLanguage": "ja",
"deploymentTarget": ["iOS": 11.1, "tvOS": 10.0, "watchOS": "3", "macOS": "10.12.1"],
]]
let parsedSpec = try getProjectSpec(dictionary)
try expect(parsedSpec) == expected
}
}
}
}
@discardableResult
fileprivate func getProjectSpec(_ project: [String: Any], file: String = #file, line: Int = #line) throws -> Project {
var projectDictionary: [String: Any] = ["name": "test"]
for (key, value) in project {
projectDictionary[key] = value
}
do {
return try Project(basePath: "", jsonDictionary: projectDictionary)
} catch {
throw failure("\(error)", file: file, line: line)
}
}
fileprivate func expectSpecError(_ project: [String: Any], _ expectedError: SpecParsingError, file: String = #file, line: Int = #line) throws {
try expectError(expectedError, file: file, line: line) {
try getProjectSpec(project)
}
}
fileprivate func expectTargetError(_ target: [String: Any], _ expectedError: SpecParsingError, file: String = #file, line: Int = #line) throws {
try expectError(expectedError, file: file, line: line) {
_ = try Target(name: "test", jsonDictionary: target)
}
}
+16 -5
View File
@@ -2,17 +2,28 @@ import Foundation
import ProjectSpec
import Spectre
import xcproj
import PathKit
func expectError<T: Error>(_ expectedError: T, _ closure: () throws -> Void) throws where T: CustomStringConvertible {
let fixturePath = Path(#file).parent().parent() + "Fixtures"
func doThrowing<T>(file: String = #file, line: Int = #line, _ closure: () throws -> T) throws -> T {
do {
return try closure()
} catch {
throw failure(String(describing: error), file: file, line: line)
}
}
func expectError<T: Error>(_ expectedError: T, function: String = #function, file: String = #file, line: Int = #line, _ closure: () throws -> Void) throws where T: CustomStringConvertible {
do {
try closure()
} catch let error as T {
try expect(error.description) == expectedError.description
try expect(error.description, file: file, line: line, function: function) == expectedError.description
return
} catch {
throw failure("Supposed to fail with \"\(expectedError)\"")
throw failure("Supposed to fail with \"\(expectedError)\"", function: function, file: file, line: line)
}
throw failure("Supposed to fail with \"\(expectedError)\"")
throw failure("Supposed to fail with \"\(expectedError)\"", function: function, file: file, line: line)
}
struct ExpectationFailure: FailureType {
@@ -32,7 +43,7 @@ struct ExpectationFailure: FailureType {
open class ArrayExpectation<T>: ExpectationType {
public typealias ValueType = Array<T>
open let expression: () throws -> ValueType?
public let expression: () throws -> ValueType?
let file: String
let line: Int
-12
View File
@@ -1,12 +0,0 @@
import Foundation
import XCTest
class XCodeGenKitTests: XCTestCase {
func testXcodeGenKit() {
projectGeneratorTests()
projectLoadingTests()
fixtureTests()
projectSpecTests()
}
}