refactoring

This commit is contained in:
Yonas Kolb 2018-11-03 20:27:59 +11:00
parent ea2d38ecdd
commit e7ef30a241
14 changed files with 286 additions and 245 deletions

View File

@ -38,6 +38,10 @@ let package = Package(
.testTarget(name: "XcodeGenKitTests", dependencies: [
"XcodeGenKit",
"Spectre",
]),
.testTarget(name: "PerformanceTests", dependencies: [
"XcodeGenKit",
"Spectre",
])
]
)

View File

@ -78,7 +78,7 @@ public struct Project: BuildSettingsContainer {
return configs.first { $0.name == configName }
}
public var projectPath: Path {
public var defaultProjectPath: Path {
return basePath + "\(name).xcodeproj"
}
}

View File

@ -22,7 +22,7 @@ func generate(spec: String, project: String, isQuiet: Bool, justVersion: Bool) {
}
let projectSpecPath = Path(spec).absolute()
let projectPath = project == "" ? projectSpecPath.parent() : Path(project).absolute()
var projectPath = project == "" ? projectSpecPath.parent() : Path(project).absolute()
if !projectSpecPath.exists {
fatalError("No project spec found at \(projectSpecPath.absolute())")
@ -39,19 +39,20 @@ func generate(spec: String, project: String, isQuiet: Bool, justVersion: Bool) {
}
do {
logger.info("⚙️ Generating project...")
try project.validateMinimumXcodeGenVersion(version)
try project.validate()
logger.info("⚙️ Generating project...")
let projectGenerator = ProjectGenerator(project: project)
let xcodeProject = try projectGenerator.generateXcodeProject()
logger.info("⚙️ Writing project...")
let projectWriter = ProjectWriter(project: project)
try projectWriter.writeXcodeProject(xcodeProject)
try projectWriter.writePlists()
logger.info("⚙️ Writing project...")
let fileWriter = FileWriter(project: project)
projectPath = projectPath + "\(project.name).xcodeproj"
try fileWriter.writeXcodeProject(xcodeProject, to: projectPath)
try fileWriter.writePlists()
logger.success("💾 Saved project to \(project.projectPath.string)")
logger.success("💾 Saved project to \(projectPath)")
} catch let error as SpecValidationError {
fatalError(error.description)
} catch {

View File

@ -3,7 +3,7 @@ import ProjectSpec
import xcodeproj
import PathKit
public class ProjectWriter {
public class FileWriter {
let project: Project
@ -11,8 +11,8 @@ public class ProjectWriter {
self.project = project
}
public func writeXcodeProject(_ xcodeProject: XcodeProj) throws {
let projectPath = project.projectPath
public func writeXcodeProject(_ xcodeProject: XcodeProj, to projectPath: Path? = nil) throws {
let projectPath = project.defaultProjectPath
let tempPath = Path.temporary + "XcodeGen_\(Int(NSTimeIntervalSince1970))"
try? tempPath.delete()
if projectPath.exists {

View File

@ -27,9 +27,7 @@ public class PBXProjGenerator {
public init(project: Project) {
self.project = project
pbxProj = PBXProj(rootObject: nil, objectVersion: 46)
sourceGenerator = SourceGenerator(project: project) { [unowned self] object in
_ = self.addObject(object)
}
sourceGenerator = SourceGenerator(project: project, pbxProj: pbxProj)
}
func addObject<T: PBXObject>(_ object: T, context: String? = nil) -> T {

View File

@ -13,10 +13,7 @@ public class ProjectGenerator {
self.project = project
}
public func generateXcodeProject(validate: Bool = true) throws -> XcodeProj {
if validate {
try project.validate()
}
public func generateXcodeProject() throws -> XcodeProj {
// generate PBXProj
let pbxProjGenerator = PBXProjGenerator(project: project)

View File

@ -18,7 +18,8 @@ class SourceGenerator {
private var variantGroupsByPath: [Path: PBXVariantGroup] = [:]
private let project: Project
var addObjectClosure: (PBXObject) -> Void
let pbxProj: PBXProj
var targetSourceExcludePaths: Set<Path> = []
var defaultExcludedFiles = [
".DS_Store",
@ -26,13 +27,14 @@ class SourceGenerator {
private(set) var knownRegions: Set<String> = []
init(project: Project, addObjectClosure: @escaping (PBXObject) -> Void) {
init(project: Project, pbxProj: PBXProj) {
self.project = project
self.addObjectClosure = addObjectClosure
self.pbxProj = pbxProj
}
func addObject<T: PBXObject>(_ object: T) -> T {
addObjectClosure(object)
func addObject<T: PBXObject>(_ object: T, context: String? = nil) -> T {
pbxProj.add(object: object)
object.context = context
return object
}

View File

@ -23,11 +23,13 @@ class GeneratedPerformanceTests: XCTestCase {
let xcodeProject = try generator.generateXcodeProject()
self.measure {
xcodeProject.pbxproj.invalidateUUIDs()
try! xcodeProject.write(path: project.projectPath)
try! xcodeProject.write(path: project.defaultProjectPath)
}
}
}
let fixturePath = Path(#file).parent().parent() + "Fixtures"
class FixturePerformanceTests: XCTestCase {
let specPath = fixturePath + "TestProject/project.yml"
@ -52,7 +54,7 @@ class FixturePerformanceTests: XCTestCase {
let xcodeProject = try generator.generateXcodeProject()
self.measure {
xcodeProject.pbxproj.invalidateUUIDs()
try! xcodeProject.write(path: project.projectPath)
try! xcodeProject.write(path: project.defaultProjectPath)
}
}
}

View File

@ -10,6 +10,7 @@ extension Project {
func generateXcodeProject(file: String = #file, line: Int = #line) throws -> XcodeProj {
return try doThrowing(file: file, line: line) {
try validate()
let generator = ProjectGenerator(project: self)
return try generator.generateXcodeProject()
}

View File

@ -9,62 +9,18 @@ class ProjectFixtureTests: XCTestCase {
func testProjectFixture() {
describe {
var xcodeProject: XcodeProj?
$0.it("generates") {
xcodeProject = try generateXcodeProject(specPath: fixturePath + "TestProject/project.yml")
}
$0.it("generates variant group") {
guard let xcodeProject = xcodeProject else { return }
func getFileReferences(_ path: String) -> [PBXFileReference] {
return xcodeProject.pbxproj.fileReferences.filter { $0.path == path }
}
func getVariableGroups(_ name: String?) -> [PBXVariantGroup] {
return xcodeProject.pbxproj.variantGroups.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 }.count) == 1
}
do {
let refs = getFileReferences(localizedResource)
try expect(refs.count) == 1
try expect(variableGroup.children.filter { $0 == refs.first }.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"
try generateXcodeProject(specPath: fixturePath + "TestProject/project.yml")
}
}
}
}
fileprivate func generateXcodeProject(specPath: Path, file: String = #file, line: Int = #line) throws -> XcodeProj {
fileprivate func generateXcodeProject(specPath: Path, file: String = #file, line: Int = #line) throws {
let project = try Project(path: specPath)
let generator = ProjectGenerator(project: project)
let writer = FileWriter(project: project)
let xcodeProject = try generator.generateXcodeProject()
let writer = ProjectWriter(project: project)
try writer.writePlists()
try writer.writeXcodeProject(xcodeProject)
return xcodeProject
try writer.writePlists()
}

View File

@ -854,7 +854,7 @@ class ProjectGeneratorTests: XCTestCase {
let tempPath = Path.temporary + "info"
let project = Project(basePath: tempPath, name: "", targets: [Target(name: "", type: .application, platform: .iOS, info: plist)])
let pbxProject = try project.generatePbxProj()
let writer = ProjectWriter(project: project)
let writer = FileWriter(project: project)
try writer.writePlists()
guard let targetConfig = pbxProject.nativeTargets.first?.buildConfigurationList?.buildConfigurations.first else {
@ -881,174 +881,4 @@ class ProjectGeneratorTests: XCTestCase {
}
}
}
func testSchemes() {
describe {
let buildTarget = Scheme.BuildTarget(target: app.name)
$0.it("generates scheme") {
let preAction = Scheme.ExecutionAction(name: "Script", script: "echo Starting", settingsTarget: app.name)
let scheme = Scheme(
name: "MyScheme",
build: Scheme.Build(targets: [buildTarget], preActions: [preAction])
)
let project = Project(
basePath: "",
name: "test",
targets: [app, framework],
schemes: [scheme]
)
let xcodeProject = try project.generateXcodeProject()
guard let target = xcodeProject.pbxproj.nativeTargets
.first(where: { $0.name == app.name }) else {
throw failure("Target not found")
}
guard let xcscheme = xcodeProject.sharedData?.schemes.first else {
throw failure("Scheme not found")
}
try expect(scheme.name) == "MyScheme"
try expect(xcscheme.buildAction?.buildImplicitDependencies) == true
try expect(xcscheme.buildAction?.parallelizeBuild) == true
try expect(xcscheme.buildAction?.preActions.first?.title) == "Script"
try expect(xcscheme.buildAction?.preActions.first?.scriptText) == "echo Starting"
try expect(xcscheme.buildAction?.preActions.first?.environmentBuildable?.buildableName) == "MyApp.app"
try expect(xcscheme.buildAction?.preActions.first?.environmentBuildable?.blueprintName) == "MyApp"
guard let buildActionEntry = xcscheme.buildAction?.buildActionEntries.first else {
throw failure("Build Action entry not found")
}
try expect(buildActionEntry.buildFor) == BuildType.all
let buildableReferences: [XCScheme.BuildableReference] = [
buildActionEntry.buildableReference,
xcscheme.launchAction?.buildableProductRunnable?.buildableReference,
xcscheme.profileAction?.buildableProductRunnable?.buildableReference,
xcscheme.testAction?.macroExpansion,
].compactMap { $0 }
for buildableReference in buildableReferences {
// FIXME: try expect(buildableReference.blueprintIdentifier) == target.reference
try expect(buildableReference.blueprintName) == target.name
try expect(buildableReference.buildableName) == "\(target.name).\(target.productType!.fileExtension!)"
}
try expect(xcscheme.launchAction?.buildConfiguration) == "Debug"
try expect(xcscheme.testAction?.buildConfiguration) == "Debug"
try expect(xcscheme.profileAction?.buildConfiguration) == "Release"
try expect(xcscheme.analyzeAction?.buildConfiguration) == "Debug"
try expect(xcscheme.archiveAction?.buildConfiguration) == "Release"
}
$0.it("sets environment variables for a scheme") {
let runVariables: [XCScheme.EnvironmentVariable] = [
XCScheme.EnvironmentVariable(variable: "RUN_ENV", value: "ENABLED", enabled: true),
XCScheme.EnvironmentVariable(variable: "OTHER_RUN_ENV", value: "DISABLED", enabled: false),
]
let scheme = Scheme(
name: "EnvironmentVariablesScheme",
build: Scheme.Build(targets: [buildTarget]),
run: Scheme.Run(config: "Debug", environmentVariables: runVariables),
test: Scheme.Test(config: "Debug"),
profile: Scheme.Profile(config: "Debug")
)
let project = Project(
basePath: "",
name: "test",
targets: [app, framework],
schemes: [scheme]
)
let xcodeProject = try project.generateXcodeProject()
guard let xcscheme = xcodeProject.sharedData?.schemes.first else {
throw failure("Scheme not found")
}
try expect(
xcodeProject.pbxproj.nativeTargets
.contains(where: { $0.name == app.name })
).beTrue()
try expect(xcscheme.launchAction?.environmentVariables) == runVariables
try expect(xcscheme.testAction?.environmentVariables).to.beNil()
try expect(xcscheme.profileAction?.environmentVariables).to.beNil()
}
$0.it("generates target schemes from config variant") {
let configVariants = ["Test", "Production"]
var target = app
target.scheme = TargetScheme(configVariants: configVariants)
let configs: [Config] = [
Config(name: "Test Debug", type: .debug),
Config(name: "Production Debug", type: .debug),
Config(name: "Test Release", type: .release),
Config(name: "Production Release", type: .release),
]
let project = Project(basePath: "", name: "test", configs: configs, targets: [target, framework])
let xcodeProject = try project.generateXcodeProject()
try expect(xcodeProject.sharedData?.schemes.count) == 2
guard let xcscheme = xcodeProject.sharedData?.schemes
.first(where: { $0.name == "\(target.name) Test" }) else {
throw failure("Scheme not found")
}
guard let buildActionEntry = xcscheme.buildAction?.buildActionEntries.first else {
throw failure("Build Action entry not found")
}
try expect(buildActionEntry.buildableReference.blueprintIdentifier.count > 0) == true
try expect(xcscheme.launchAction?.buildConfiguration) == "Test Debug"
try expect(xcscheme.testAction?.buildConfiguration) == "Test Debug"
try expect(xcscheme.profileAction?.buildConfiguration) == "Test Release"
try expect(xcscheme.analyzeAction?.buildConfiguration) == "Test Debug"
try expect(xcscheme.archiveAction?.buildConfiguration) == "Test Release"
}
$0.it("generates environment variables for target schemes") {
let variables: [XCScheme.EnvironmentVariable] = [XCScheme.EnvironmentVariable(variable: "env", value: "var", enabled: false)]
var target = app
target.scheme = TargetScheme(environmentVariables: variables)
let project = Project(basePath: "", name: "test", targets: [target, framework])
let xcodeProject = try project.generateXcodeProject()
try expect(xcodeProject.sharedData?.schemes.count) == 1
guard let xcscheme = xcodeProject.sharedData?.schemes.first else {
throw failure("Scheme not found")
}
try expect(xcscheme.launchAction?.environmentVariables) == variables
try expect(xcscheme.testAction?.environmentVariables) == variables
try expect(xcscheme.profileAction?.environmentVariables) == variables
}
$0.it("generates pre and post actions for target schemes") {
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 project.generateXcodeProject()
try expect(xcodeProject.sharedData?.schemes.count) == 1
guard let xcscheme = xcodeProject.sharedData?.schemes.first else {
throw failure("Scheme not found")
}
try expect(xcscheme.launchAction?.preActions.count) == 1
try expect(xcscheme.launchAction?.preActions.first?.title) == "Run"
try expect(xcscheme.launchAction?.preActions.first?.scriptText) == "do"
try expect(xcscheme.testAction?.postActions.count) == 1
try expect(xcscheme.testAction?.postActions.first?.title) == "Run2"
try expect(xcscheme.testAction?.postActions.first?.scriptText) == "post"
try expect(xcscheme.testAction?.postActions.first?.environmentBuildable?.blueprintName) == "MyApp"
}
}
}
}

View File

@ -0,0 +1,208 @@
import PathKit
import ProjectSpec
import Spectre
import XcodeGenKit
import xcodeproj
import XCTest
import Yams
fileprivate let app = Target(
name: "MyApp",
type: .application,
platform: .iOS,
dependencies: [Dependency(type: .target, reference: "MyFramework")]
)
fileprivate let framework = Target(
name: "MyFramework",
type: .framework,
platform: .iOS
)
fileprivate let optionalFramework = Target(
name: "MyOptionalFramework",
type: .framework,
platform: .iOS
)
fileprivate let uiTest = Target(
name: "MyAppUITests",
type: .uiTestBundle,
platform: .iOS,
dependencies: [Dependency(type: .target, reference: "MyApp")]
)
class SchemeGeneratorTests: XCTestCase {
func testSchemes() {
describe {
let buildTarget = Scheme.BuildTarget(target: app.name)
$0.it("generates scheme") {
let preAction = Scheme.ExecutionAction(name: "Script", script: "echo Starting", settingsTarget: app.name)
let scheme = Scheme(
name: "MyScheme",
build: Scheme.Build(targets: [buildTarget], preActions: [preAction])
)
let project = Project(
basePath: "",
name: "test",
targets: [app, framework],
schemes: [scheme]
)
let xcodeProject = try project.generateXcodeProject()
guard let target = xcodeProject.pbxproj.nativeTargets
.first(where: { $0.name == app.name }) else {
throw failure("Target not found")
}
guard let xcscheme = xcodeProject.sharedData?.schemes.first else {
throw failure("Scheme not found")
}
try expect(scheme.name) == "MyScheme"
try expect(xcscheme.buildAction?.buildImplicitDependencies) == true
try expect(xcscheme.buildAction?.parallelizeBuild) == true
try expect(xcscheme.buildAction?.preActions.first?.title) == "Script"
try expect(xcscheme.buildAction?.preActions.first?.scriptText) == "echo Starting"
try expect(xcscheme.buildAction?.preActions.first?.environmentBuildable?.buildableName) == "MyApp.app"
try expect(xcscheme.buildAction?.preActions.first?.environmentBuildable?.blueprintName) == "MyApp"
guard let buildActionEntry = xcscheme.buildAction?.buildActionEntries.first else {
throw failure("Build Action entry not found")
}
try expect(buildActionEntry.buildFor) == BuildType.all
let buildableReferences: [XCScheme.BuildableReference] = [
buildActionEntry.buildableReference,
xcscheme.launchAction?.buildableProductRunnable?.buildableReference,
xcscheme.profileAction?.buildableProductRunnable?.buildableReference,
xcscheme.testAction?.macroExpansion,
].compactMap { $0 }
for buildableReference in buildableReferences {
// FIXME: try expect(buildableReference.blueprintIdentifier) == target.reference
try expect(buildableReference.blueprintName) == target.name
try expect(buildableReference.buildableName) == "\(target.name).\(target.productType!.fileExtension!)"
}
try expect(xcscheme.launchAction?.buildConfiguration) == "Debug"
try expect(xcscheme.testAction?.buildConfiguration) == "Debug"
try expect(xcscheme.profileAction?.buildConfiguration) == "Release"
try expect(xcscheme.analyzeAction?.buildConfiguration) == "Debug"
try expect(xcscheme.archiveAction?.buildConfiguration) == "Release"
}
$0.it("sets environment variables for a scheme") {
let runVariables: [XCScheme.EnvironmentVariable] = [
XCScheme.EnvironmentVariable(variable: "RUN_ENV", value: "ENABLED", enabled: true),
XCScheme.EnvironmentVariable(variable: "OTHER_RUN_ENV", value: "DISABLED", enabled: false),
]
let scheme = Scheme(
name: "EnvironmentVariablesScheme",
build: Scheme.Build(targets: [buildTarget]),
run: Scheme.Run(config: "Debug", environmentVariables: runVariables),
test: Scheme.Test(config: "Debug"),
profile: Scheme.Profile(config: "Debug")
)
let project = Project(
basePath: "",
name: "test",
targets: [app, framework],
schemes: [scheme]
)
let xcodeProject = try project.generateXcodeProject()
guard let xcscheme = xcodeProject.sharedData?.schemes.first else {
throw failure("Scheme not found")
}
try expect(
xcodeProject.pbxproj.nativeTargets
.contains(where: { $0.name == app.name })
).beTrue()
try expect(xcscheme.launchAction?.environmentVariables) == runVariables
try expect(xcscheme.testAction?.environmentVariables).to.beNil()
try expect(xcscheme.profileAction?.environmentVariables).to.beNil()
}
$0.it("generates target schemes from config variant") {
let configVariants = ["Test", "Production"]
var target = app
target.scheme = TargetScheme(configVariants: configVariants)
let configs: [Config] = [
Config(name: "Test Debug", type: .debug),
Config(name: "Production Debug", type: .debug),
Config(name: "Test Release", type: .release),
Config(name: "Production Release", type: .release),
]
let project = Project(basePath: "", name: "test", configs: configs, targets: [target, framework])
let xcodeProject = try project.generateXcodeProject()
try expect(xcodeProject.sharedData?.schemes.count) == 2
guard let xcscheme = xcodeProject.sharedData?.schemes
.first(where: { $0.name == "\(target.name) Test" }) else {
throw failure("Scheme not found")
}
guard let buildActionEntry = xcscheme.buildAction?.buildActionEntries.first else {
throw failure("Build Action entry not found")
}
try expect(buildActionEntry.buildableReference.blueprintIdentifier.count > 0) == true
try expect(xcscheme.launchAction?.buildConfiguration) == "Test Debug"
try expect(xcscheme.testAction?.buildConfiguration) == "Test Debug"
try expect(xcscheme.profileAction?.buildConfiguration) == "Test Release"
try expect(xcscheme.analyzeAction?.buildConfiguration) == "Test Debug"
try expect(xcscheme.archiveAction?.buildConfiguration) == "Test Release"
}
$0.it("generates environment variables for target schemes") {
let variables: [XCScheme.EnvironmentVariable] = [XCScheme.EnvironmentVariable(variable: "env", value: "var", enabled: false)]
var target = app
target.scheme = TargetScheme(environmentVariables: variables)
let project = Project(basePath: "", name: "test", targets: [target, framework])
let xcodeProject = try project.generateXcodeProject()
try expect(xcodeProject.sharedData?.schemes.count) == 1
guard let xcscheme = xcodeProject.sharedData?.schemes.first else {
throw failure("Scheme not found")
}
try expect(xcscheme.launchAction?.environmentVariables) == variables
try expect(xcscheme.testAction?.environmentVariables) == variables
try expect(xcscheme.profileAction?.environmentVariables) == variables
}
$0.it("generates pre and post actions for target schemes") {
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 project.generateXcodeProject()
try expect(xcodeProject.sharedData?.schemes.count) == 1
guard let xcscheme = xcodeProject.sharedData?.schemes.first else {
throw failure("Scheme not found")
}
try expect(xcscheme.launchAction?.preActions.count) == 1
try expect(xcscheme.launchAction?.preActions.first?.title) == "Run"
try expect(xcscheme.launchAction?.preActions.first?.scriptText) == "do"
try expect(xcscheme.testAction?.postActions.count) == 1
try expect(xcscheme.testAction?.postActions.first?.title) == "Run2"
try expect(xcscheme.testAction?.postActions.first?.scriptText) == "post"
try expect(xcscheme.testAction?.postActions.first?.environmentBuildable?.blueprintName) == "MyApp"
}
}
}
}

View File

@ -126,6 +126,48 @@ class SourceGeneratorTests: XCTestCase {
try expect(fileReference.path) == "model2.xcdatamodel"
}
$0.it("generates variant groups") {
let directories = """
Sources:
Base.lproj:
- LocalizedStoryboard.storyboard
en.lproj:
- LocalizedStoryboard.strings
"""
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()
func getFileReferences(_ path: String) -> [PBXFileReference] {
return pbxProj.fileReferences.filter { $0.path == path }
}
func getVariableGroups(_ name: String?) -> [PBXVariantGroup] {
return pbxProj.variantGroups.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 }.count) == 1
}
do {
let refs = getFileReferences(localizedResource)
try expect(refs.count) == 1
try expect(variableGroup.children.filter { $0 == refs.first }.count) == 1
}
}
$0.it("handles duplicate names") {
let directories = """
Sources: