diff --git a/App.xcodeproj/project.pbxproj b/App.xcodeproj/project.pbxproj index 379a41a81..c6ed445f0 100644 --- a/App.xcodeproj/project.pbxproj +++ b/App.xcodeproj/project.pbxproj @@ -92,6 +92,9 @@ B9F1EDEF208D1DB200477835 /* BuildPhase+TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9F1EDEE208D1DB200477835 /* BuildPhase+TestData.swift */; }; B9F1EDF1208D1F0F00477835 /* Settings+TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9F1EDF0208D1F0F00477835 /* Settings+TestData.swift */; }; B9F1EDF3208D200C00477835 /* Scheme+TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9F1EDF2208D200C00477835 /* Scheme+TestData.swift */; }; + B9F1EDF6208D228600477835 /* ProjectValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9F1EDF5208D228600477835 /* ProjectValidator.swift */; }; + B9F1EDF8208D22D900477835 /* TargetValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9F1EDF7208D22D900477835 /* TargetValidator.swift */; }; + B9F1EDFB208D24F700477835 /* ProjectValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9F1EDFA208D24F700477835 /* ProjectValidatorTests.swift */; }; B9FB2DBF2086506E00BC2FB3 /* Basic.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9FB2DBB20864FF800BC2FB3 /* Basic.framework */; }; B9FB2DC62086515F00BC2FB3 /* Basic.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B9FB2DBB20864FF800BC2FB3 /* Basic.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B9FB2DC72086516500BC2FB3 /* POSIX.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B9FB2DC02086507200BC2FB3 /* POSIX.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -298,6 +301,9 @@ B9F1EDEE208D1DB200477835 /* BuildPhase+TestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BuildPhase+TestData.swift"; sourceTree = ""; }; B9F1EDF0208D1F0F00477835 /* Settings+TestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Settings+TestData.swift"; sourceTree = ""; }; B9F1EDF2208D200C00477835 /* Scheme+TestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Scheme+TestData.swift"; sourceTree = ""; }; + B9F1EDF5208D228600477835 /* ProjectValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectValidator.swift; sourceTree = ""; }; + B9F1EDF7208D22D900477835 /* TargetValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetValidator.swift; sourceTree = ""; }; + B9F1EDFA208D24F700477835 /* ProjectValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectValidatorTests.swift; sourceTree = ""; }; B9FB2DBB20864FF800BC2FB3 /* Basic.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Basic.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B9FB2DBD2086502100BC2FB3 /* Utility.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Utility.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B9FB2DC02086507200BC2FB3 /* POSIX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = POSIX.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -411,6 +417,7 @@ B915ED3B2063B04C004B6630 /* Tests */ = { isa = PBXGroup; children = ( + B9F1EDF9208D24EA00477835 /* Validator */, B9589605208B4E3500F00ACF /* Generator */, B92BF9FC2075607C00EE4EBD /* Extensions */, B92BF9ED2075599900EE4EBD /* Models */, @@ -425,6 +432,7 @@ B915ED3E2063B04C004B6630 /* Sources */ = { isa = PBXGroup; children = ( + B9F1EDF4208D215600477835 /* Validator */, B95895F1208A2A9E00F00ACF /* Generator */, B9B6299C20864E2300EE9E07 /* App */, B9B629BD20864E7D00EE9E07 /* Commands */, @@ -643,6 +651,23 @@ path = Models; sourceTree = ""; }; + B9F1EDF4208D215600477835 /* Validator */ = { + isa = PBXGroup; + children = ( + B9F1EDF5208D228600477835 /* ProjectValidator.swift */, + B9F1EDF7208D22D900477835 /* TargetValidator.swift */, + ); + path = Validator; + sourceTree = ""; + }; + B9F1EDF9208D24EA00477835 /* Validator */ = { + isa = PBXGroup; + children = ( + B9F1EDFA208D24F700477835 /* ProjectValidatorTests.swift */, + ); + path = Validator; + sourceTree = ""; + }; B9FB2DCD2086538E00BC2FB3 /* Loader */ = { isa = PBXGroup; children = ( @@ -961,7 +986,9 @@ B9FB2DE62086544900BC2FB3 /* Scheme.swift in Sources */, B95895FB208A2FFB00F00ACF /* WorkspaceGenerator.swift in Sources */, B9589600208A37B700F00ACF /* GraphJSONInitiatable.swift in Sources */, + B9F1EDF8208D22D900477835 /* TargetValidator.swift in Sources */, B9B6299F20864E2300EE9E07 /* Constants.swift in Sources */, + B9F1EDF6208D228600477835 /* ProjectValidator.swift in Sources */, B9FB2DCA2086527B00BC2FB3 /* BuildFiles.swift in Sources */, B9FB2DE1208653F500BC2FB3 /* GraphController.swift in Sources */, B9B629AA20864E3A00EE9E07 /* TechLogger.swift in Sources */, @@ -986,6 +1013,7 @@ B9F1EDED208D1CFE00477835 /* Target+TestData.swift in Sources */, B9F1EDD3208CD2E200477835 /* ShellCompletion+Equatable.swift in Sources */, B9E2DC9E20872D400061DF86 /* AppTests.swift in Sources */, + B9F1EDFB208D24F700477835 /* ProjectValidatorTests.swift in Sources */, B9E2DC9720872B5D0061DF86 /* MockManifestLoader.swift in Sources */, B9E2DCC82089B78D0061DF86 /* DumpCommandTests.swift in Sources */, B9E2DCA32087651D0061DF86 /* Project+TestData.swift in Sources */, diff --git a/App/xcbuddykit/Sources/Validator/ProjectValidator.swift b/App/xcbuddykit/Sources/Validator/ProjectValidator.swift new file mode 100644 index 000000000..194e436ec --- /dev/null +++ b/App/xcbuddykit/Sources/Validator/ProjectValidator.swift @@ -0,0 +1,42 @@ +import Basic +import Foundation + +enum ProjectValidationError: Error, CustomStringConvertible, Equatable { + case duplicatedTargets([String], AbsolutePath) + var description: String { + switch self { + case let .duplicatedTargets(targets, projectPath): + return "Targets \(targets.joined(separator: ", ")) from project at \(projectPath.asString) have duplicates." + } + } + + static func == (lhs: ProjectValidationError, rhs: ProjectValidationError) -> Bool { + switch (lhs, rhs) { + case let (.duplicatedTargets(lhsTargets, lhsPath), .duplicatedTargets(rhsTargets, rhsPath)): + return lhsTargets == rhsTargets && lhsPath == rhsPath + } + } +} + +/// Validates the format of the project. +class ProjectValidator { + let targetValidator: TargetValidator = TargetValidator() + + func validate(_ project: Project) throws { + try validateTargets(project: project) + } + + fileprivate func validateTargets(project: Project) throws { + try project.targets.forEach(targetValidator.validate) + try validateNotDuplicatedTargets(project: project) + } + + fileprivate func validateNotDuplicatedTargets(project: Project) throws { + let duplicatedTargets = project.targets.map({ $0.name }) + .reduce(into: [String: Int]()) { $0[$1] = ($0[$1] ?? 0) + 1 } + .filter({ $0.value > 1 }) + .keys + if duplicatedTargets.count == 0 { return } + throw ProjectValidationError.duplicatedTargets(Array(duplicatedTargets), project.path) + } +} diff --git a/App/xcbuddykit/Sources/Validator/TargetValidator.swift b/App/xcbuddykit/Sources/Validator/TargetValidator.swift new file mode 100644 index 000000000..ad2e2efb6 --- /dev/null +++ b/App/xcbuddykit/Sources/Validator/TargetValidator.swift @@ -0,0 +1,6 @@ +import Foundation + +class TargetValidator { + func validate(_: Target) throws { + } +} diff --git a/App/xcbuddykit/Tests/Models/Project+TestData.swift b/App/xcbuddykit/Tests/Models/Project+TestData.swift index 85ac8f51a..e5136fe90 100644 --- a/App/xcbuddykit/Tests/Models/Project+TestData.swift +++ b/App/xcbuddykit/Tests/Models/Project+TestData.swift @@ -3,12 +3,12 @@ import Foundation @testable import xcbuddykit extension Project { - static func testData(path: AbsolutePath = AbsolutePath("/test/"), - name: String = "Project", - config: Config? = nil, - schemes: [Scheme] = [Scheme.test()], - settings: Settings? = Settings.test(), - targets: [Target] = [Target.test()]) -> Project { + static func test(path: AbsolutePath = AbsolutePath("/test/"), + name: String = "Project", + config: Config? = nil, + schemes: [Scheme] = [Scheme.test()], + settings: Settings? = Settings.test(), + targets: [Target] = [Target.test()]) -> Project { return Project(path: path, name: name, config: config, diff --git a/App/xcbuddykit/Tests/Models/Target+TestData.swift b/App/xcbuddykit/Tests/Models/Target+TestData.swift index 019e67f7f..1498f941c 100644 --- a/App/xcbuddykit/Tests/Models/Target+TestData.swift +++ b/App/xcbuddykit/Tests/Models/Target+TestData.swift @@ -6,8 +6,8 @@ extension Target { static func test(name: String = "Target", platform: Platform = .ios, product: Product = .module, - infoPlist: AbsolutePath = AbsolutePath("Info.plist"), - entitlements: AbsolutePath? = AbsolutePath("Test.entitlements"), + infoPlist: AbsolutePath = AbsolutePath("/Info.plist"), + entitlements: AbsolutePath? = AbsolutePath("/Test.entitlements"), settings: Settings? = Settings.test(), buildPhases: [BuildPhase] = [ SourcesBuildPhase.test(), diff --git a/App/xcbuddykit/Tests/Validator/ProjectValidatorTests.swift b/App/xcbuddykit/Tests/Validator/ProjectValidatorTests.swift new file mode 100644 index 000000000..3ca02e7db --- /dev/null +++ b/App/xcbuddykit/Tests/Validator/ProjectValidatorTests.swift @@ -0,0 +1,28 @@ +import Basic +import Foundation +@testable import xcbuddykit +import XCTest + +final class ProjectValidationErrorTests: XCTestCase { + func test_description_whenDuplicatedTargets() { + let error = ProjectValidationError.duplicatedTargets(["A", "B"], AbsolutePath("/test")) + XCTAssertEqual(error.description, "Targets A, B from project at /test have duplicates.") + } +} + +final class ProjectValidatorTests: XCTestCase { + var subject: ProjectValidator! + + override func setUp() { + super.setUp() + subject = ProjectValidator() + } + + func test_validate_throws_when_there_are_duplicated_targets() throws { + let target = Target.test(name: "A") + let project = Project.test(targets: [target, target]) + XCTAssertThrowsError(try subject.validate(project)) { error in + XCTAssertEqual(error as? ProjectValidationError, ProjectValidationError.duplicatedTargets(["A"], project.path)) + } + } +}