blob: 9cb034e2cbed502e9fb55ae5643187fc7634bfc2 [file] [log] [blame]
// Copyright 2016 The Tulsi Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import XCTest
@testable import TulsiGenerator
typealias StringToObjectDict = [String: NSObject]
class PBXProjSerializerTests: XCTestCase {
var gidGenerator: MockGIDGenerator! = nil
var project: PBXProject! = nil
var serializer: OpenStepSerializer! = nil
override func setUp() {
super.setUp()
gidGenerator = MockGIDGenerator()
project = PBXProject(name: "TestProject")
serializer = OpenStepSerializer(rootObject: project, gidGenerator: gidGenerator)
}
// MARK: - Tests
func testOpenStepSerializesEmptyDictionaries() {
let config = project.buildConfigurationList.getOrCreateBuildConfiguration("Empty")
config.buildSettings = Dictionary<String, String>()
config.globalID = gidGenerator.generateReservedID()
guard let openStepData = serializer.serialize() else {
XCTFail("Failed to generate OpenStep format")
return
}
let root: StringToObjectDict
do {
root = try PropertyListSerialization.propertyList(from: openStepData,
options: [], format: nil) as! StringToObjectDict
} catch let error as NSError {
let serializedData = String(data: openStepData, encoding: String.Encoding.utf8)!
XCTFail("Failed to parse OpenStep serialized data " + error.localizedDescription + "\n" + serializedData)
return
}
let objects = root["objects"] as! StringToObjectDict
let buildConfigDict: StringToObjectDict! = getObjectByID(config.globalID,
withPBXClass: "XCBuildConfiguration",
fromObjects: objects)
XCTAssertNotNil(buildConfigDict["buildSettings"])
}
func testOpenStepSerializationIsStable() {
let project1 = PBXProject(name: "TestProject")
let gidGenerator1 = MockGIDGenerator()
populateProject(project1, withGIDGenerator: gidGenerator1)
let serializer1 = OpenStepSerializer(rootObject: project1, gidGenerator: gidGenerator1)
guard let openStepData1 = serializer1.serialize() else {
XCTFail("Failed to generate OpenStep format")
return
}
let project2 = PBXProject(name: "TestProject")
let gidGenerator2 = MockGIDGenerator()
populateProject(project2, withGIDGenerator: gidGenerator2)
let serializer2 = OpenStepSerializer(rootObject: project2, gidGenerator: gidGenerator2)
guard let openStepData2 = serializer2.serialize() else {
XCTFail("Failed to generate OpenStep format")
return
}
let serializedData1 = String(data: openStepData1, encoding: String.Encoding.utf8)!
let serializedData2 = String(data: openStepData2, encoding: String.Encoding.utf8)!
XCTAssertEqual(serializedData1, serializedData2)
}
// MARK: - Helper methods
// Captures the testable values in defining a PBXFileReference.
struct FileDefinition {
let sourceTree: SourceTree
let path: String
let uti: String?
let gid: String
let isInputFile: Bool
init(sourceTree: SourceTree, path: String, uti: String?, gid: String, isInputFile: Bool = true) {
self.sourceTree = sourceTree
self.path = path
self.uti = uti
self.gid = gid
self.isInputFile = isInputFile
}
init(sourceTree: SourceTree, path: String, gid: String, isInputFile: Bool = true) {
let uti = FileExtensionToUTI[(path as NSString).pathExtension]
self.init(sourceTree: sourceTree,
path: path,
uti: uti,
gid: gid,
isInputFile: isInputFile)
}
}
// Captures the testable values in defining a PBXGroup.
class GroupDefinition {
let name: String
let sourceTree: SourceTree
let path: String?
let gid: String
let files: [FileDefinition]
let groups: [GroupDefinition]
let expectedPBXClass: String
init(name: String,
sourceTree: SourceTree,
path: String?,
gid: String,
files: [FileDefinition],
groups: [GroupDefinition],
expectedPBXClass: String = "PBXGroup") {
self.name = name
self.sourceTree = sourceTree
self.path = path
self.gid = gid
self.files = files
self.groups = groups
self.expectedPBXClass = expectedPBXClass
}
func groupByAddingGroup(_ group: GroupDefinition) -> GroupDefinition {
var newGroups = groups
newGroups.append(group)
return GroupDefinition(name: name,
sourceTree: sourceTree,
path: path,
gid: gid,
files: files,
groups: newGroups,
expectedPBXClass: expectedPBXClass)
}
}
class VersionGroupDefinition: GroupDefinition {
let currentVersion: FileDefinition
let versionGroupType: String
init(name: String,
sourceTree: SourceTree,
path: String?,
gid: String,
files: [FileDefinition],
groups: [GroupDefinition],
currentVersion: FileDefinition,
versionGroupType: String? = nil) {
self.currentVersion = currentVersion
if let versionGroupType = versionGroupType {
self.versionGroupType = versionGroupType
} else {
self.versionGroupType = FileExtensionToUTI[(name as NSString).pathExtension] ?? ""
}
super.init(name: name,
sourceTree: sourceTree,
path: path,
gid: gid,
files: files,
groups: groups,
expectedPBXClass: "XCVersionGroup")
}
}
// Captures the testable values used when defining a simple PBXProject.
struct SimpleProjectDefinition {
struct NativeTargetDefinition {
let name: String
let settings: Dictionary<String, String>
let config: String
let targetType: PBXTarget.ProductType
}
struct LegacyTargetDefinition {
let name: String
let buildToolPath: String
let buildArguments: String
let buildWorkingDirectory: String
}
let projectLevelBuildConfigName: String
let projectLevelBuildConfigSettings: Dictionary<String, String>
let nativeTarget: NativeTargetDefinition
let legacyTarget: LegacyTargetDefinition
let mainGroupGID: String
let mainGroupDefinition: GroupDefinition
}
private func generateSimpleProject() -> SimpleProjectDefinition {
return populateProject(project, withGIDGenerator: gidGenerator)
}
@discardableResult
private func populateProject(_ targetProject: PBXProject, withGIDGenerator generator: MockGIDGenerator) -> SimpleProjectDefinition {
let projectLevelBuildConfigName = "ProjectConfig"
let projectLevelBuildConfigSettings = ["TEST_SETTING": "test_setting",
"QuotedSetting": "Quoted string value"]
let nativeTarget = SimpleProjectDefinition.NativeTargetDefinition(name: "NativeApplicationTarget",
settings: ["PRODUCT_NAME": "ProductName", "QuotedValue": "A quoted value"],
config: "Config1",
targetType: PBXTarget.ProductType.Application
)
let legacyTarget = SimpleProjectDefinition.LegacyTargetDefinition(name: "LegacyTarget",
buildToolPath: "buildToolPath",
buildArguments: "buildArguments",
buildWorkingDirectory: "buildWorkingDirectory")
// Note: This test relies on the fact that the current serializer implementation preserves the
// GIDs of any objects it attempts to serialize.
let mainGroupGID = generator.generateReservedID()
let mainGroupDefinition: GroupDefinition
do {
let mainGroupFiles = [
FileDefinition(sourceTree: .Group, path: "GroupFile.swift", gid: generator.generateReservedID()),
FileDefinition(sourceTree: .Absolute, path: "/fake/path/AbsoluteFile.swift", gid: generator.generateReservedID()),
]
let activeDatamodelVersion = FileDefinition(sourceTree: .Group,
path: "v2.xcdatamodel",
uti: DirExtensionToUTI["xcdatamodel"],
gid: generator.generateReservedID(),
isInputFile: true)
let mainGroupGroups = [
GroupDefinition(name: "Products",
sourceTree: .Group,
path: nil,
gid: generator.generateReservedID(),
files: [],
groups: []
),
GroupDefinition(name: "ChildGroup",
sourceTree: .Group,
path: "child_group_path",
gid: generator.generateReservedID(),
files: [
FileDefinition(sourceTree: .Group, path: "ChildRelativeFile.swift", gid: generator.generateReservedID()),
FileDefinition(sourceTree: .Group, path: "t.a", gid: generator.generateReservedID()),
FileDefinition(sourceTree: .Group, path: "t.dylib", gid: generator.generateReservedID()),
FileDefinition(sourceTree: .Group, path: "t.framework", gid: generator.generateReservedID()),
FileDefinition(sourceTree: .Group, path: "t.jpg", gid: generator.generateReservedID()),
FileDefinition(sourceTree: .Group, path: "t.m", gid: generator.generateReservedID()),
FileDefinition(sourceTree: .Group, path: "t.mm", gid: generator.generateReservedID()),
FileDefinition(sourceTree: .Group, path: "t.pch", gid: generator.generateReservedID()),
FileDefinition(sourceTree: .Group, path: "t.plist", gid: generator.generateReservedID()),
FileDefinition(sourceTree: .Group, path: "t.png", gid: generator.generateReservedID()),
FileDefinition(sourceTree: .Group, path: "t.rtf", gid: generator.generateReservedID()),
FileDefinition(sourceTree: .Group, path: "t.storyboard", gid: generator.generateReservedID()),
FileDefinition(sourceTree: .Group, path: "t.xcassets", uti: DirExtensionToUTI["xcassets"], gid: generator.generateReservedID()),
FileDefinition(sourceTree: .Group, path: "t.xcstickers", uti: DirExtensionToUTI["xcstickers"], gid: generator.generateReservedID()),
FileDefinition(sourceTree: .Group, path: "t.xib", gid: generator.generateReservedID()),
FileDefinition(sourceTree: .Group, path: "Test", uti: "text", gid: generator.generateReservedID()),
FileDefinition(sourceTree: .Group, path: "Output.app", gid: generator.generateReservedID(), isInputFile: false),
],
groups: []
),
VersionGroupDefinition(name: "DataModel.xcdatamodeld",
sourceTree: .Group,
path: "DataModel.xcdatamodeld",
gid: generator.generateReservedID(),
files: [
FileDefinition(sourceTree: .Group,
path: "v1.xcdatamodel",
uti: DirExtensionToUTI["xcdatamodel"],
gid: generator.generateReservedID(),
isInputFile: true),
activeDatamodelVersion,
],
groups: [],
currentVersion: activeDatamodelVersion,
versionGroupType: DirExtensionToUTI["xcdatamodeld"]
)
]
mainGroupDefinition = GroupDefinition(name: "mainGroup",
sourceTree: .SourceRoot,
path: nil,
gid: mainGroupGID,
files: mainGroupFiles,
groups: mainGroupGroups)
}
let definition = SimpleProjectDefinition(projectLevelBuildConfigName: projectLevelBuildConfigName,
projectLevelBuildConfigSettings: projectLevelBuildConfigSettings,
nativeTarget: nativeTarget,
legacyTarget: legacyTarget,
mainGroupGID: mainGroupGID,
mainGroupDefinition: mainGroupDefinition
)
do {
let config = targetProject.buildConfigurationList.getOrCreateBuildConfiguration(projectLevelBuildConfigName)
config.buildSettings = projectLevelBuildConfigSettings
}
let nativePBXTarget = targetProject.createNativeTarget(nativeTarget.name,
deploymentTarget: nil,
targetType: nativeTarget.targetType)
let config = nativePBXTarget.buildConfigurationList.getOrCreateBuildConfiguration(nativeTarget.config)
config.buildSettings = nativeTarget.settings
let legacyPBXTarget = targetProject.createLegacyTarget(legacyTarget.name,
deploymentTarget: nil,
buildToolPath: legacyTarget.buildToolPath,
buildArguments: legacyTarget.buildArguments,
buildWorkingDirectory: legacyTarget.buildWorkingDirectory)
targetProject.linkTestTarget(legacyPBXTarget, toHostTarget: nativePBXTarget)
do {
func populateGroup(_ group: PBXGroup, groupDefinition: GroupDefinition) {
group.globalID = groupDefinition.gid
for file in groupDefinition.files {
let fileRef = group.getOrCreateFileReferenceBySourceTree(file.sourceTree, path: file.path)
fileRef.globalID = file.gid
fileRef.isInputFile = file.isInputFile
}
for childDef in groupDefinition.groups {
let childGroup: PBXGroup
if let versionedChildDef = childDef as? VersionGroupDefinition {
let versionGroup = group.getOrCreateChildVersionGroupByName(versionedChildDef.name,
path: versionedChildDef.path)
versionGroup.versionGroupType = versionedChildDef.versionGroupType
let currentVersionDef = versionedChildDef.currentVersion
let currentFileRef = versionGroup.getOrCreateFileReferenceBySourceTree(currentVersionDef.sourceTree,
path: currentVersionDef.path)
currentFileRef.globalID = currentVersionDef.gid
currentFileRef.isInputFile = currentVersionDef.isInputFile
versionGroup.currentVersion = currentFileRef
childGroup = versionGroup
} else {
childGroup = group.getOrCreateChildGroupByName(childDef.name, path: childDef.path)
}
populateGroup(childGroup, groupDefinition: childDef)
}
}
let mainGroup = targetProject.mainGroup
populateGroup(mainGroup, groupDefinition: mainGroupDefinition)
}
return definition
}
private func assertDict(_ dict: StringToObjectDict, isPBXObjectClass pbxClass: String, line: UInt = #line) {
guard let isa = dict["isa"] as? String else {
XCTFail("dictionary is not a PBXObject (missing 'isa' member)", line: line)
return
}
XCTAssertEqual(isa, pbxClass, "Serialized dict is not of the expected PBXObject type", line: line)
}
private func getObjectByID(_ gid: String,
withPBXClass pbxClass: String,
fromObjects objects: StringToObjectDict,
line: UInt = #line) -> StringToObjectDict? {
guard let dict = objects[gid] as? StringToObjectDict else {
XCTFail("Missing \(pbxClass) with globalID '\(gid)'", line: line)
return nil
}
assertDict(dict, isPBXObjectClass: pbxClass, line: line)
return dict
}
/// Generates predictable GlobalID's for use in tests.
// Note, this implementation is only suitable for projects with less than 4 billion objects.
class MockGIDGenerator: GIDGeneratorProtocol {
var nextID = 0
func generate(_ item: PBXObjectProtocol) -> String {
// This test implementation doesn't utilize the object in generating an ID.
let gid = gidForCounter(nextID)
nextID += 1
return gid
}
// MARK: - Methods for testing.
func generateReservedID() -> String {
let reservedID = gidForCounter(nextID, prefix: 0xBAADF00D)
nextID += 1
return reservedID
}
private func gidForCounter(_ counter : Int, prefix: Int = 0) -> String {
return String(format: "%08X%08X%08X", prefix, 0, counter & 0xFFFFFFFF)
}
}
}