From 862b19aa1a5905ddd05dc4706fbcc7addf2a3635 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Mon, 28 Jan 2019 13:39:46 +1100 Subject: [PATCH 1/4] refactor Spec --- Sources/ProjectSpec/Project.swift | 15 ++- .../{Spec.swift => SpecFile.swift} | 119 ++++++++++-------- Sources/ProjectSpec/SpecLoader.swift | 16 --- Sources/XcodeGenKit/SpecLoader.swift | 7 +- Tests/XcodeGenKitTests/SpecLoadingTests.swift | 3 +- 5 files changed, 84 insertions(+), 76 deletions(-) rename Sources/ProjectSpec/{Spec.swift => SpecFile.swift} (53%) delete mode 100644 Sources/ProjectSpec/SpecLoader.swift diff --git a/Sources/ProjectSpec/Project.swift b/Sources/ProjectSpec/Project.swift index ca23a913..d00da2f4 100644 --- a/Sources/ProjectSpec/Project.swift +++ b/Sources/ProjectSpec/Project.swift @@ -132,16 +132,19 @@ extension Project: Equatable { extension Project { - public init(basePath: Path, jsonDictionary: JSONDictionary) throws { - let spec = Spec(relativePath: Path(), jsonDictionary: jsonDictionary) - try self.init(spec: spec, basePath: basePath) + public init(path: Path) throws { + let spec = try SpecFile(path: path) + try self.init(spec: spec) } - public init(spec: Spec, basePath: Path) throws { + public init(spec: SpecFile) throws { + try self.init(basePath: spec.basePath, jsonDictionary: spec.resolvedDictionary()) + } + + public init(basePath: Path = "", jsonDictionary: JSONDictionary) throws { self.basePath = basePath - let spec = spec.resolvingPaths() - let jsonDictionary = try Project.resolveProject(jsonDictionary: spec.resolvedDictionary()) + let jsonDictionary = try Project.resolveProject(jsonDictionary: jsonDictionary) name = try jsonDictionary.json(atKeyPath: "name") settings = jsonDictionary.json(atKeyPath: "settings") ?? .empty diff --git a/Sources/ProjectSpec/Spec.swift b/Sources/ProjectSpec/SpecFile.swift similarity index 53% rename from Sources/ProjectSpec/Spec.swift rename to Sources/ProjectSpec/SpecFile.swift index dd53ad02..adae6092 100644 --- a/Sources/ProjectSpec/Spec.swift +++ b/Sources/ProjectSpec/SpecFile.swift @@ -2,89 +2,110 @@ import Foundation import JSONUtilities import PathKit -public struct Spec { +public struct SpecFile { + public let basePath: Path public let relativePath: Path public let jsonDictionary: JSONDictionary - public let subSpecs: [Spec] + public let subSpecs: [SpecFile] - public init(relativePath: Path, jsonDictionary: JSONDictionary, subSpecs: [Spec] = []) { + fileprivate struct Include { + let path: Path + let relativePaths: Bool + + static let defaultRelativePaths = true + + init?(any: Any) { + if let string = any as? String { + path = Path(string) + relativePaths = Include.defaultRelativePaths + } else if let dictionary = any as? JSONDictionary, + let path = dictionary["path"] as? String { + self.path = Path(path) + self.relativePaths = dictionary["relativePaths"] as? Bool ?? Include.defaultRelativePaths + } else { + return nil + } + } + + static func parse(json: Any?) -> [Include] { + if let array = json as? [Any] { + return array.compactMap(Include.init) + } else if let object = json, let include = Include(any: object) { + return [include] + } else { + return [] + } + } + } + + public init(path: Path) throws { + try self.init(filename: path.lastComponent, basePath: path.parent()) + } + + public init(jsonDictionary: JSONDictionary, basePath: Path = "", relativePath: Path = "", subSpecs: [SpecFile] = []) { + self.basePath = basePath self.relativePath = relativePath self.jsonDictionary = jsonDictionary self.subSpecs = subSpecs } - public init(filename: String, basePath: Path, relativePath: Path = Path()) throws { - let path = basePath + relativePath + filename + fileprivate init(include: Include, basePath: Path, relativePath: Path) throws { + let basePath = include.relativePaths ? (basePath + relativePath) : (basePath + relativePath + include.path.parent()) + let relativePath = include.relativePaths ? include.path.parent() : Path() + try self.init(filename: include.path.lastComponent, basePath: basePath, relativePath: relativePath) + } + + fileprivate init(filename: String, basePath: Path, relativePath: Path = "") throws { + let path = basePath + relativePath + filename + let jsonDictionary = try SpecFile.loadDictionary(path: path) + + let includes = Include.parse(json: jsonDictionary["include"]) + let subSpecs: [SpecFile] = try includes.map { include in + try SpecFile(include: include, basePath: basePath, relativePath: relativePath) + } + + self.init(jsonDictionary: jsonDictionary, basePath: basePath, relativePath: relativePath, subSpecs: subSpecs) + } + + static func loadDictionary(path: Path) throws -> JSONDictionary { // Depending on the extension we will either load the file as YAML or JSON - var json: [String: Any] if path.extension?.lowercased() == "json" { let data: Data = try path.read() let jsonData = try JSONSerialization.jsonObject(with: data, options: .allowFragments) guard let jsonDictionary = jsonData as? [String: Any] else { fatalError("Invalid JSON at path \(path)") } - json = jsonDictionary + return jsonDictionary } else { - json = try loadYamlDictionary(path: path) + return try loadYamlDictionary(path: path) } - - let processIncludeOption = { (option: Any) -> (String, Bool)? in - if let option = option as? String { - return (option, true) - } else if let option = option as? JSONDictionary, let path = option["path"] as? String { - return (path, (option["relativePaths"] as? Bool) ?? true) - } - return nil - } - - let includeSources: [(String, Bool)] - if let sources = json["include"] as? [Any] { - includeSources = sources.compactMap { processIncludeOption($0) } - } else if let source = json["include"] { - includeSources = [processIncludeOption(source)].compactMap { $0 } - } else { - includeSources = [] - } - - let includes = try includeSources.map { include -> Spec in - let path = Path(include.0) - let basePath = include.1 ? basePath + relativePath : basePath + relativePath + path.parent() - let relativePath = include.1 ? path.parent() : Path() - - return try Spec(filename: path.lastComponent, basePath: basePath, relativePath: relativePath) - } - - self.relativePath = relativePath - self.jsonDictionary = json - self.subSpecs = includes } public func resolvedDictionary() -> JSONDictionary { + let resolvedSpec = resolvingPaths() + return resolvedSpec.mergedDictionary() + } + + func mergedDictionary() -> JSONDictionary { return jsonDictionary.merged(onto: subSpecs - .map { $0.resolvedDictionary() } + .map { $0.mergedDictionary() } .reduce([:]) { $1.merged(onto: $0) } ) } -} -extension Spec { - - func resolvingPaths(relativeTo basePath: Path = Path()) -> Spec { + func resolvingPaths(relativeTo basePath: Path = Path()) -> SpecFile { let relativePath = (basePath + self.relativePath).normalize() guard relativePath != Path() else { return self } let jsonDictionary = Project.pathProperties.resolvingPaths(in: self.jsonDictionary, relativeTo: relativePath) - - return Spec( - relativePath: self.relativePath, + return SpecFile( jsonDictionary: jsonDictionary, - subSpecs: self.subSpecs.map { template in - return template.resolvingPaths(relativeTo: relativePath) - } + relativePath: self.relativePath, + subSpecs: self.subSpecs.map { $0.resolvingPaths(relativeTo: relativePath) } ) } } diff --git a/Sources/ProjectSpec/SpecLoader.swift b/Sources/ProjectSpec/SpecLoader.swift deleted file mode 100644 index db5ca726..00000000 --- a/Sources/ProjectSpec/SpecLoader.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation -import JSONUtilities -import PathKit - -extension Project { - - public init(path: Path) throws { - let basePath = path.parent() - let template = try Spec(filename: path.lastComponent, basePath: basePath) - try self.init(spec: template, basePath: basePath) - } - - public static func loadDictionary(path: Path) throws -> JSONDictionary { - return try Spec(filename: path.lastComponent, basePath: path.parent()).jsonDictionary - } -} diff --git a/Sources/XcodeGenKit/SpecLoader.swift b/Sources/XcodeGenKit/SpecLoader.swift index 86edd32d..35514f97 100644 --- a/Sources/XcodeGenKit/SpecLoader.swift +++ b/Sources/XcodeGenKit/SpecLoader.swift @@ -16,11 +16,12 @@ public class SpecLoader { } public func loadProject(path: Path) throws -> Project { - let template = try Spec(filename: path.lastComponent, basePath: path.parent()) - let project = try Project(spec: template, basePath: path.parent()) + let spec = try SpecFile(path: path) + let resolvedDictionary = spec.resolvedDictionary() + let project = try Project(basePath: spec.basePath, jsonDictionary: resolvedDictionary) self.project = project - projectDictionary = template.jsonDictionary + projectDictionary = resolvedDictionary return project } diff --git a/Tests/XcodeGenKitTests/SpecLoadingTests.swift b/Tests/XcodeGenKitTests/SpecLoadingTests.swift index f0ddd865..633ec1d3 100644 --- a/Tests/XcodeGenKitTests/SpecLoadingTests.swift +++ b/Tests/XcodeGenKitTests/SpecLoadingTests.swift @@ -657,8 +657,7 @@ fileprivate func getProjectSpec(_ project: [String: Any], file: String = #file, projectDictionary[key] = value } do { - let template = Spec(relativePath: "", jsonDictionary: projectDictionary) - return try Project(spec: template, basePath: "") + return try Project(jsonDictionary: projectDictionary) } catch { throw failure("\(error)", file: file, line: line) } From a6ca395033980bf580af7463a281b3ad2af24931 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Mon, 28 Jan 2019 13:39:46 +1100 Subject: [PATCH 2/4] remove basePath init requirement --- Sources/ProjectSpec/Project.swift | 2 +- .../ProjectGeneratorTests.swift | 26 +++++++------------ Tests/XcodeGenKitTests/ProjectSpecTests.swift | 2 +- .../SchemeGeneratorTests.swift | 8 +++--- Tests/XcodeGenKitTests/SpecLoadingTests.swift | 2 +- 5 files changed, 16 insertions(+), 24 deletions(-) diff --git a/Sources/ProjectSpec/Project.swift b/Sources/ProjectSpec/Project.swift index d00da2f4..85885d76 100644 --- a/Sources/ProjectSpec/Project.swift +++ b/Sources/ProjectSpec/Project.swift @@ -33,7 +33,7 @@ public struct Project: BuildSettingsContainer { private var aggregateTargetsMap: [String: AggregateTarget] public init( - basePath: Path, + basePath: Path = "", name: String, configs: [Config] = Config.defaultConfigs, targets: [Target] = [], diff --git a/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift b/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift index 89b523d1..5cb11bb6 100644 --- a/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift @@ -45,7 +45,7 @@ class ProjectGeneratorTests: XCTestCase { $0.it("generates bundle id") { let options = SpecOptions(bundleIdPrefix: "com.test") - let project = Project(basePath: "", name: "test", targets: [framework], options: options) + let project = Project(name: "test", targets: [framework], options: options) let pbxProj = try project.generatePbxProj() guard let target = pbxProj.nativeTargets.first, let buildConfigList = target.buildConfigurationList, @@ -57,7 +57,7 @@ class ProjectGeneratorTests: XCTestCase { $0.it("clears setting presets") { let options = SpecOptions(settingPresets: .none) - let project = Project(basePath: "", name: "test", targets: [framework], options: options) + let project = Project(name: "test", targets: [framework], options: options) let pbxProj = try project.generatePbxProj() let allSettings = pbxProj.buildConfigurations.reduce([:]) { $0.merged($1.buildSettings) }.keys.sorted() try expect(allSettings) == ["SDKROOT", "SETTING_2"] @@ -65,7 +65,7 @@ class ProjectGeneratorTests: XCTestCase { $0.it("generates development language") { let options = SpecOptions(developmentLanguage: "de") - let project = Project(basePath: "", name: "test", options: options) + let project = Project(name: "test", options: options) let pbxProj = try project.generatePbxProj() guard let pbxProject = pbxProj.projects.first else { throw failure("Could't find PBXProject") @@ -93,7 +93,7 @@ class ProjectGeneratorTests: XCTestCase { $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 project = Project(name: "test", configs: [Config(name: "Aconfig"), Config(name: "Bconfig")], targets: [framework], options: options) let pbxProject = try project.generatePbxProj() guard let projectConfigList = pbxProject.projects.first?.buildConfigurationList, @@ -111,7 +111,7 @@ class ProjectGeneratorTests: XCTestCase { describe { $0.it("generates config defaults") { - let project = Project(basePath: "", name: "test") + let project = Project(name: "test") let pbxProj = try project.generatePbxProj() let configs = pbxProj.buildConfigurations try expect(configs.count) == 2 @@ -121,7 +121,6 @@ class ProjectGeneratorTests: XCTestCase { $0.it("generates configs") { let project = Project( - basePath: "", name: "test", configs: [Config(name: "config1"), Config(name: "config2")] ) @@ -134,7 +133,6 @@ class ProjectGeneratorTests: XCTestCase { $0.it("clears config settings when missing type") { let project = Project( - basePath: "", name: "test", configs: [Config(name: "config")] ) @@ -176,7 +174,6 @@ class ProjectGeneratorTests: XCTestCase { $0.it("applies partial config settings") { let project = Project( - basePath: "", name: "test", configs: [ Config(name: "Staging Debug", type: .debug), @@ -192,7 +189,6 @@ class ProjectGeneratorTests: XCTestCase { $0.it("sets project SDKROOT if there is only a single platform") { var project = Project( - basePath: "", name: "test", targets: [ Target(name: "1", type: .application, platform: .iOS), @@ -216,7 +212,7 @@ class ProjectGeneratorTests: XCTestCase { let otherTarget2 = Target(name: "Other2", type: .framework, platform: .iOS, dependencies: [Dependency(type: .target, reference: "Other")], transitivelyLinkDependencies: true) let aggregateTarget = AggregateTarget(name: "AggregateTarget", targets: ["MyApp", "MyFramework"]) let aggregateTarget2 = AggregateTarget(name: "AggregateTarget2", targets: ["AggregateTarget"]) - let project = Project(basePath: "", name: "test", targets: [app, framework, otherTarget, otherTarget2], aggregateTargets: [aggregateTarget, aggregateTarget2]) + let project = Project(name: "test", targets: [app, framework, otherTarget, otherTarget2], aggregateTargets: [aggregateTarget, aggregateTarget2]) $0.it("generates aggregate targets") { let pbxProject = try project.generatePbxProj() @@ -246,7 +242,7 @@ class ProjectGeneratorTests: XCTestCase { func testTargets() { describe { - let project = Project(basePath: "", name: "test", targets: targets) + let project = Project(name: "test", targets: targets) $0.it("generates targets") { let pbxProject = try project.generatePbxProj() @@ -265,7 +261,7 @@ class ProjectGeneratorTests: XCTestCase { var testTargetWithAttributes = uiTest testTargetWithAttributes.settings.buildSettings["CODE_SIGN_STYLE"] = "Manual" - let project = Project(basePath: "", name: "test", targets: [appTargetWithAttributes, framework, optionalFramework, testTargetWithAttributes]) + let project = Project(name: "test", targets: [appTargetWithAttributes, framework, optionalFramework, testTargetWithAttributes]) let pbxProject = try project.generatePbxProj() guard let targetAttributes = pbxProject.projects.first?.targetAttributes else { @@ -288,7 +284,7 @@ class ProjectGeneratorTests: XCTestCase { $0.it("generates platform version") { 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 project = Project(name: "", targets: [target], options: .init(deploymentTarget: DeploymentTarget(iOS: "10.0", watchOS: "3.0"))) let pbxProject = try project.generatePbxProj() @@ -568,7 +564,6 @@ class ProjectGeneratorTests: XCTestCase { let targets = [app, iosFrameworkZ, staticLibrary, resourceBundle, iosFrameworkA, iosFrameworkB, appTest, appTestWithoutTransitive, stickerPack] let project = Project( - basePath: "", name: "test", targets: targets, options: SpecOptions(transitivelyLinkDependencies: true) @@ -801,7 +796,6 @@ class ProjectGeneratorTests: XCTestCase { dependencies: [Dependency(type: .target, reference: "target1")] ) let project = Project( - basePath: "", name: "test", targets: [target1, target2] ) @@ -860,7 +854,7 @@ class ProjectGeneratorTests: XCTestCase { ] ) - let project = Project(basePath: "", name: "test", targets: [app, framework, optionalFramework, uiTest]) + let project = Project(name: "test", targets: [app, framework, optionalFramework, uiTest]) let pbxProject = try project.generatePbxProj() guard let nativeTarget = pbxProject.nativeTargets.first(where: { $0.name == app.name }) else { diff --git a/Tests/XcodeGenKitTests/ProjectSpecTests.swift b/Tests/XcodeGenKitTests/ProjectSpecTests.swift index 74e14896..6eace70c 100644 --- a/Tests/XcodeGenKitTests/ProjectSpecTests.swift +++ b/Tests/XcodeGenKitTests/ProjectSpecTests.swift @@ -69,7 +69,7 @@ class ProjectSpecTests: XCTestCase { func testValidation() { describe { - let baseProject = Project(basePath: "", name: "", configs: [Config(name: "invalid")]) + let baseProject = Project(name: "", configs: [Config(name: "invalid")]) let invalidSettings = Settings( configSettings: ["invalidConfig": [:]], groups: ["invalidSettingGroup"] diff --git a/Tests/XcodeGenKitTests/SchemeGeneratorTests.swift b/Tests/XcodeGenKitTests/SchemeGeneratorTests.swift index b617a5e0..ebfe9df8 100644 --- a/Tests/XcodeGenKitTests/SchemeGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/SchemeGeneratorTests.swift @@ -45,7 +45,6 @@ class SchemeGeneratorTests: XCTestCase { build: Scheme.Build(targets: [buildTarget], preActions: [preAction]) ) let project = Project( - basePath: "", name: "test", targets: [app, framework], schemes: [scheme] @@ -104,7 +103,6 @@ class SchemeGeneratorTests: XCTestCase { profile: Scheme.Profile(config: "Debug") ) let project = Project( - basePath: "", name: "test", targets: [app, framework], schemes: [scheme] @@ -135,7 +133,7 @@ class SchemeGeneratorTests: XCTestCase { Config(name: "Production Release", type: .release), ] - let project = Project(basePath: "", name: "test", configs: configs, targets: [target, framework]) + let project = Project(name: "test", configs: configs, targets: [target, framework]) let xcodeProject = try project.generateXcodeProject() try expect(xcodeProject.sharedData?.schemes.count) == 2 @@ -162,7 +160,7 @@ class SchemeGeneratorTests: XCTestCase { var target = app target.scheme = TargetScheme(environmentVariables: variables) - let project = Project(basePath: "", name: "test", targets: [target, framework]) + let project = Project(name: "test", targets: [target, framework]) let xcodeProject = try project.generateXcodeProject() try expect(xcodeProject.sharedData?.schemes.count) == 1 @@ -183,7 +181,7 @@ class SchemeGeneratorTests: XCTestCase { postActions: [.init(name: "Run2", script: "post", settingsTarget: "MyApp")] ) - let project = Project(basePath: "", name: "test", targets: [target, framework]) + let project = Project(name: "test", targets: [target, framework]) let xcodeProject = try project.generateXcodeProject() try expect(xcodeProject.sharedData?.schemes.count) == 1 diff --git a/Tests/XcodeGenKitTests/SpecLoadingTests.swift b/Tests/XcodeGenKitTests/SpecLoadingTests.swift index 633ec1d3..5ca5d6d3 100644 --- a/Tests/XcodeGenKitTests/SpecLoadingTests.swift +++ b/Tests/XcodeGenKitTests/SpecLoadingTests.swift @@ -619,7 +619,7 @@ class SpecLoadingTests: XCTestCase { macOS: "10.12.1" ) ) - let expected = Project(basePath: "", name: "test", options: options) + let expected = Project(name: "test", options: options) let dictionary: [String: Any] = ["options": [ "carthageBuildPath": "../Carthage/Build", "carthageExecutablePath": "../bin/carthage", From 43d8927fdf0d2388df560ec79a27250b4f5184d8 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Mon, 28 Jan 2019 13:45:26 +1100 Subject: [PATCH 3/4] remove some transformed path from options --- Sources/ProjectSpec/SpecOptions.swift | 2 -- Tests/XcodeGenKitTests/SpecLoadingTests.swift | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/ProjectSpec/SpecOptions.swift b/Sources/ProjectSpec/SpecOptions.swift index 9bfb5a2d..4fe36f22 100644 --- a/Sources/ProjectSpec/SpecOptions.swift +++ b/Sources/ProjectSpec/SpecOptions.swift @@ -126,8 +126,6 @@ extension SpecOptions: PathContainer { static var pathProperties: [PathProperty] { return [ .string("carthageBuildPath"), - .string("carthageExecutablePath"), - .string("defaultConfig"), ] } } diff --git a/Tests/XcodeGenKitTests/SpecLoadingTests.swift b/Tests/XcodeGenKitTests/SpecLoadingTests.swift index 5ca5d6d3..dc060e78 100644 --- a/Tests/XcodeGenKitTests/SpecLoadingTests.swift +++ b/Tests/XcodeGenKitTests/SpecLoadingTests.swift @@ -38,7 +38,7 @@ class SpecLoadingTests: XCTestCase { try expect(project.options) == SpecOptions( carthageBuildPath: "paths_test/recursive_test/carthage_build", - carthageExecutablePath: "paths_test/recursive_test/carthage_executable" + carthageExecutablePath: "carthage_executable" ) try expect(project.aggregateTargets) == [ From ce60fadb540784f6ef84a4e76c96f5ba0788e204 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Mon, 28 Jan 2019 14:13:54 +1100 Subject: [PATCH 4/4] use SpecLoader in tests --- Tests/XcodeGenKitTests/SpecLoadingTests.swift | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Tests/XcodeGenKitTests/SpecLoadingTests.swift b/Tests/XcodeGenKitTests/SpecLoadingTests.swift index dc060e78..f68960e6 100644 --- a/Tests/XcodeGenKitTests/SpecLoadingTests.swift +++ b/Tests/XcodeGenKitTests/SpecLoadingTests.swift @@ -12,7 +12,7 @@ class SpecLoadingTests: XCTestCase { describe { $0.it("merges includes") { let path = fixturePath + "include_test.yml" - let project = try Project(path: path) + let project = try loadSpec(path: path) try expect(project.name) == "NewName" try expect(project.settingGroups) == [ @@ -28,7 +28,7 @@ class SpecLoadingTests: XCTestCase { $0.it("expands directories") { let path = fixturePath + "paths_test.yml" - let project = try Project(path: path) + let project = try loadSpec(path: path) try expect(project.configFiles) == [ "IncludedConfig": "paths_test/config", @@ -107,7 +107,7 @@ class SpecLoadingTests: XCTestCase { $0.it("respects directory expansion preference") { let path = fixturePath + "legacy_paths_test.yml" - let project = try Project(path: path) + let project = try loadSpec(path: path) try expect(project.configFiles) == [ "IncludedConfig": "config", @@ -186,7 +186,7 @@ class SpecLoadingTests: XCTestCase { describe { $0.it("merges includes") { let path = fixturePath + "include_test.json" - let project = try Project(path: path) + let project = try loadSpec(path: path) try expect(project.name) == "NewName" try expect(project.settingGroups) == [ @@ -663,6 +663,15 @@ fileprivate func getProjectSpec(_ project: [String: Any], file: String = #file, } } +fileprivate func loadSpec(path: Path, file: String = #file, line: Int = #line) throws -> Project { + do { + let specLoader = SpecLoader(version: "1.1.0") + return try specLoader.loadProject(path: path) + } 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)