diff --git a/CHANGELOG.md b/CHANGELOG.md index 857d539..ed9071d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Add support for Swift Packages and Xcode 16 synchronized folders + - Add new optional variable nammed "project_type" which accepts 2 values: "xcode" and "filesystem". When missing ccios use "xcode" by default. + - When "project_type" is set to "filesystem" ccios will not try to update a pbxproj and will only generate files. + - When "project_type" is set to "filesystem" multi target definition is no longer supported for generated files. + - When generating files for an filesystem project, the target name in the header is either: the target defined in the template variables, the target defined in `.ccios.yml`, or it will try to guess the name when using SPM by searching the target name inside the standard naming scheme of: "Sources//". + ## [5.1.0] ### Changed diff --git a/Gemfile.lock b/Gemfile.lock index f066a55..259dd3f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,7 +4,7 @@ PATH ccios (5.1.0) activesupport (> 4) mustache (~> 1.0) - xcodeproj (~> 1.4) + xcodeproj (~> 1.27) GEM remote: https://rubygems.org/ diff --git a/README.md b/README.md index 7eee6d5..8689a9b 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ templates_collection: ccios/templates # Global overrides of variables [Optional] variables: + project_type: xcode project: Project.xcodeproj target: SomeDefaultTarget @@ -221,6 +222,9 @@ parameters: # List of templates variables that is used to generate files in an xcode project. [Optional] # Those variables can be overridden in config file, see section "Variable hierarchy" for more informations. variables: + # Type of project "filesystem" or "xcode", will be considered as "xcode" if not specified. [Optional] + # You want to use "filesystem" if you want to generate files for an SPM project, or if your Xcode project uses the new synchronized group from Xcode 16. + project_type: filesystem # The name of the xcode project. "*.xcodeproj" will use the first it finds. [required] project: "*.xcodeproj" # The base path used to generate an element. This variable must be defined once here, or on each elements below. @@ -297,3 +301,16 @@ Element will use variables in this order (first in this list is used): (For file - Config Template variables - Default templates variables - Config Global variables + +# How to develop + +1. install the gem locally using +```bash +make install +``` +2. Run use the locally installed ccios gem on your project +```bash +ccios presenter MyNewPresenterStack +``` + +Note: ensure that you use the same ruby version when building, installing and running this local version of ccios. diff --git a/ccios.gemspec b/ccios.gemspec index 6dfa0dc..e9c4a7d 100644 --- a/ccios.gemspec +++ b/ccios.gemspec @@ -12,7 +12,7 @@ Gem::Specification.new do |s| s.homepage = 'http://rubygems.org/gems/hola' s.license = 'MIT' s.add_dependency 'activesupport', '> 4' - s.add_dependency 'xcodeproj', '~> 1.4' + s.add_dependency 'xcodeproj', '~> 1.27' s.add_dependency "mustache", "~> 1.0" s.add_development_dependency 'rake', '~> 13.2' diff --git a/lib/ccios/file_creator.rb b/lib/ccios/file_creator.rb index 8eedc47..5c94ec1 100644 --- a/lib/ccios/file_creator.rb +++ b/lib/ccios/file_creator.rb @@ -3,18 +3,6 @@ require 'logger' require 'xcodeproj' -class Xcodeproj::Project::Object::PBXGroup - - def pf_new_group(associate_path_to_group:, name:, path:) - # When using "Group with folder" we only provide a path - # When using "Group without folder" we only provide a name - new_group( - associate_path_to_group ? nil : name, - associate_path_to_group ? path : nil - ) - end -end - class Xcodeproj::Project def project_name_from_path @@ -37,10 +25,18 @@ def templater_options(targets, project) full_username: git_username, date: DateTime.now.strftime("%d/%m/%Y"), } - if targets.count == 1 - defaults["project_name"] = targets[0].display_name + if project.nil? + if targets.count == 1 + defaults["project_name"] = targets[0] + else + raise "A file outside an xcode project cannot require multiple targets" + end else - defaults["project_name"] = project.project_name_from_path + if targets.count == 1 + defaults["project_name"] = targets[0].display_name + else + defaults["project_name"] = project.project_name_from_path + end end defaults end @@ -69,10 +65,7 @@ def create_file_using_template_path(template_path, generated_filename, group, ta file.puts(file_content) file.close - file_ref = group.new_reference(file_path) - targets.each do |target| - target.add_file_references([file_ref]) - end + group.register_file_to_targets(file_path, targets) end def print_file_content_using_template(filename, template_path, context) @@ -83,8 +76,12 @@ def print_file_content_using_template(filename, template_path, context) logger.info file_content end - def create_empty_directory(group) + def create_empty_directory_for_group(group) dirname = group.real_path + create_empty_directory(dirname) + end + + def create_empty_directory(dirname) FileUtils.mkdir_p dirname unless File.directory?(dirname) git_keep_path = File.join(dirname, ".gitkeep") diff --git a/lib/ccios/file_template_definition.rb b/lib/ccios/file_template_definition.rb index f0717ee..a80068c 100644 --- a/lib/ccios/file_template_definition.rb +++ b/lib/ccios/file_template_definition.rb @@ -1,6 +1,6 @@ require_relative 'code_templater' require_relative 'file_creator' -require_relative 'pbxproj_parser' +require_relative 'xcode_group_representation' class FileTemplateDefinition def initialize(file_template_definition_hash) @@ -33,20 +33,34 @@ def validate(parser, project, context, template_definition, config) base_path = merged_variables["base_path"] raise "Missing base_path variable" if base_path.nil? - base_group = project[base_path] - raise "Base path \"#{base_path}\" is missing" if base_group.nil? - target_name = merged_variables["target"] - if target_name.is_a?(String) || target_name.nil? - target = parser.target_for(project, target_name) - raise "Unable to find target \"#{target_name}\"" if target.nil? - elsif target_name.is_a?(Array) - target_name.each do |target_name| + if project.nil? + if target_name.is_a?(String) + target = target_name + elsif target_name.nil? + guessed_name = guess_target_name_from_path(base_path) + raise "Unable to guess the target from the base path \"#{base_path}\", please specify the target in your config file" if guessed_name.nil? + target = guessed_name + elsif target_name.is_a?(Array) + raise "A template generating files in an filesystem project type cannot specify multiple targets" + else + raise "Invalid target in template #{@name}" + end + else + base_group = XcodeGroupRepresentation.findGroup(base_path, project) + raise "Base path \"#{base_path}\" is missing" if base_group.nil? + + if target_name.is_a?(String) || target_name.nil? target = parser.target_for(project, target_name) raise "Unable to find target \"#{target_name}\"" if target.nil? + elsif target_name.is_a?(Array) + target_name.each do |target_name| + target = parser.target_for(project, target_name) + raise "Unable to find target \"#{target_name}\"" if target.nil? + end + else + raise "Invalid target in template #{@name}" end - else - raise "Invalid target in template #{@name}" end end @@ -55,32 +69,30 @@ def generate(parser, project, context, template_definition, config) merged_variables = config.variables_for_template_element(template_definition, @name, @variables) base_path = merged_variables["base_path"] - base_group = project[base_path] + base_group = XcodeGroupRepresentation.findGroup(base_path, project) file_path = CodeTemplater.new.render_string(@path, context) intermediates_groups = file_path.split("/")[0...-1] generated_filename = file_path.split("/")[-1] - group = base_group - associate_path_to_group = !base_group.path.nil? - - intermediates_groups.each do |group_name| - new_group_path = File.join(group.real_path, group_name) - existing_group = group.groups.find { |g| g.display_name == group_name } - group = existing_group || group.pf_new_group( - associate_path_to_group: associate_path_to_group, - name: group_name, - path: new_group_path - ) - end + group = base_group.create_groups_if_needed_for_intermediate_groups(intermediates_groups) target_name = merged_variables["target"] targets = [] - if target_name.is_a?(String) || target_name.nil? - targets = [parser.target_for(project, target_name)] - elsif target_name.is_a?(Array) - targets = target_name.map { |name| parser.target_for(project, name) } + if project.nil? + if target_name.is_a?(String) + targets = [target_name] + elsif target_name.nil? + guessed_name = guess_target_name_from_path(base_path) + targets = [guessed_name] + end + else + if target_name.is_a?(String) || target_name.nil? + targets = [parser.target_for(project, target_name)] + elsif target_name.is_a?(Array) + targets = target_name.map { |name| parser.target_for(project, name) } + end end FileCreator.new.create_file_using_template_path( @@ -92,4 +104,15 @@ def generate(parser, project, context, template_definition, config) context ) end -end \ No newline at end of file + + private def guess_target_name_from_path(path) + # SPM standard format + parts = path.split(File::SEPARATOR) + sources_index = parts.index("Sources") + if sources_index && sources_index + 1 < parts.length + return parts[sources_index + 1] + else + return nil + end + end +end diff --git a/lib/ccios/group_template_definition.rb b/lib/ccios/group_template_definition.rb index a49c567..5f7449d 100644 --- a/lib/ccios/group_template_definition.rb +++ b/lib/ccios/group_template_definition.rb @@ -1,6 +1,5 @@ require_relative 'code_templater' require_relative 'file_creator' -require_relative 'pbxproj_parser' class GroupTemplateDefinition def initialize(group_template_definition_hash) @@ -22,7 +21,7 @@ def validate(parser, project, context, template_definition, config) base_path = merged_variables["base_path"] raise "Missing base_path variable" if base_path.nil? - base_group = project[base_path] + base_group = XcodeGroupRepresentation.findGroup(base_path, project) raise "Base path \"#{base_path}\" is missing" if base_group.nil? end @@ -30,25 +29,13 @@ def generate(parser, project, context, template_definition, config) merged_variables = config.variables_for_template_element(template_definition, @name, @variables) base_path = merged_variables["base_path"] - base_group = project[base_path] + base_group = XcodeGroupRepresentation.findGroup(base_path, project) group_path = CodeTemplater.new.render_string(@path, context) - group_path = group_path.split("/") - - group = base_group - associate_path_to_group = !base_group.path.nil? - - group_path.each do |group_name| - new_group_path = File.join(group.real_path, group_name) - existing_group = group.groups.find { |g| g.display_name == group_name } - group = existing_group || group.pf_new_group( - associate_path_to_group: associate_path_to_group, - name: group_name, - path: new_group_path - ) - end + group_path_components = group_path.split("/") + group = base_group.create_groups_if_needed_for_intermediate_groups(group_path_components) file_creator = FileCreator.new - file_creator.create_empty_directory(group) + file_creator.create_empty_directory_for_group(group) end -end \ No newline at end of file +end diff --git a/lib/ccios/pbxproj_parser.rb b/lib/ccios/pbxproj_parser.rb index 887fc5f..897878c 100644 --- a/lib/ccios/pbxproj_parser.rb +++ b/lib/ccios/pbxproj_parser.rb @@ -33,4 +33,4 @@ def target_for(project, target_name) project.targets.find { |t| t.name == target_name } end end -end \ No newline at end of file +end diff --git a/lib/ccios/template_definition.rb b/lib/ccios/template_definition.rb index a0fc83f..abf84a3 100644 --- a/lib/ccios/template_definition.rb +++ b/lib/ccios/template_definition.rb @@ -59,10 +59,17 @@ def validate(parser, options, config) raise "Error: invalid template name" unless @name.is_a? String merged_variables = config.variables_for_template(self) - project_path = merged_variables["project"] - - project = parser.project_for(project_path) - raise "Error: Unable to find project \"#{project_path}\"" if project.nil? + project_type = merged_variables["project_type"] || "xcode" + case project_type + when "xcode" + project_path = merged_variables["project"] + project = parser.project_for(project_path) + raise "Error: Unable to find project \"#{project_path}\"" if project.nil? + when "filesystem" + project = nil + else + raise "Invalid project_type given \"#{project_type}\", only \"xcode\" and \"fiilesystem\" are supported" + end @template_file_source.each do |file_template_name, path| raise "Missing template source file for \"#{file_template_name}\"" unless File.exist?(self.template_source_file(file_template_name)) @@ -93,9 +100,16 @@ def generate(parser, options, config) options = agrument_transformed_options(options) merged_variables = config.variables_for_template(self) - project_path = merged_variables["project"] - project = parser.project_for project_path + project_type = merged_variables["project_type"] || "xcode" + case project_type + when "xcode" + project_path = merged_variables["project"] + project = parser.project_for project_path + when "filesystem" + project = nil + + end @generated_elements.each do |element| element.generate(parser, project, options, self, config) diff --git a/lib/ccios/xcode_group_representation.rb b/lib/ccios/xcode_group_representation.rb new file mode 100644 index 0000000..651b1a4 --- /dev/null +++ b/lib/ccios/xcode_group_representation.rb @@ -0,0 +1,107 @@ +require_relative 'pbxproj_parser' + +# This object handles Xcode groups, folder reference and synchronized folder reference +class XcodeGroupRepresentation + def self.findGroup(path, project) + intermediates_groups = path.split("/") + + if project.nil? + return self.new nil, nil, intermediates_groups + end + + deepest_group = project.main_group + additional_path = [] + + intermediates_groups.each do |group_name| + if deepest_group.is_a?(Xcodeproj::Project::Object::PBXFileSystemSynchronizedRootGroup) + additional_path.append(group_name) + elsif deepest_group.is_a?(Xcodeproj::Project::Object::PBXGroup) + deepest_group = deepest_group.find_subpath(group_name) + return nil if deepest_group.nil? + else + raise "Unsupported element found with name \"#{group_name}\": #{deepest_group}" + end + end + self.new project, deepest_group, additional_path + end + + def initialize(project, xcode_group, additional_path = []) + if project.nil? + throw "Unexpected xcode_group when project is nil, we should be in an filesystem context" unless xcode_group.nil? + else + throw "Unsupported group type" unless xcode_group.is_a?(Xcodeproj::Project::Object::PBXFileSystemSynchronizedRootGroup) || xcode_group.is_a?(Xcodeproj::Project::Object::PBXGroup) + if !additional_path.empty? && !xcode_group.is_a?(Xcodeproj::Project::Object::PBXFileSystemSynchronizedRootGroup) + throw "additional_path can only be specified for a synchronized file system group" + end + end + # This represent the xcode project if present, if nil we should be generating files in an Swift package + @project = project + # This represents the deepest group or folder reference in the project + @xcode_deepest_group = xcode_group + # This represents the additional filesystem path after `xcode_deepest_group` as an Array of strings. This should be non empty only when deepest group is a synchronized group + @additional_path = additional_path + end + + def real_path + if @xcode_deepest_group.nil? + @additional_path.join("/") + else + Xcodeproj::Project::Object::GroupableHelper.real_path(@xcode_deepest_group) + @additional_path.join("/") + end + end + + + def register_file_to_targets(file_path, targets) + if @xcode_deepest_group.nil? + return + end + if @xcode_deepest_group.is_a?(Xcodeproj::Project::Object::PBXGroup) + file_ref = @xcode_deepest_group.new_reference(file_path) + targets.each do |target| + target.add_file_references([file_ref]) + end + else + # no file to register, unless exceptions needs to be made + # TODO: Handle synchronized groups (no new reference, but use exceptions) + targets.each do |taget| + puts "Unsupported target mismatch between \"#{file_path}\" and synchronized group #{ @xcode_deepest_group.display_name }" unless taget.file_system_synchronized_groups.index(@xcode_deepest_group) != nil + end + end + end + + # @return [XcodeGroupRepresentation] the created group or self if the intermediates_groups is empty + def create_groups_if_needed_for_intermediate_groups(intermediates_groups) + return self if intermediates_groups.empty? + + new_deepest_group = @xcode_deepest_group + new_additional_path = @additional_path + + intermediates_groups.each do |group_name| + if new_deepest_group.nil? || new_deepest_group.is_a?(Xcodeproj::Project::Object::PBXFileSystemSynchronizedRootGroup) + new_additional_path.append(group_name) + elsif new_deepest_group.is_a?(Xcodeproj::Project::Object::PBXGroup) + existing_child = new_deepest_group.find_subpath(group_name) + new_group_path = File.join(new_deepest_group.real_path, group_name) + new_deepest_group = existing_child || new_deepest_group.pf_new_group( + associate_path_to_group: !new_deepest_group.path.nil?, + name: group_name, + path: new_group_path + ) + else + raise "Unsupported element found for \"#{group_name}\": #{new_deepest_group}" + end + end + XcodeGroupRepresentation.new @project, new_deepest_group, new_additional_path + end +end + +# Private utility method + +class Xcodeproj::Project::Object::PBXGroup + def pf_new_group(associate_path_to_group:, name:, path:) + new_group( + associate_path_to_group ? nil : name, + associate_path_to_group ? path : nil + ) + end +end diff --git a/test/project/MyProject.xcodeproj/project.pbxproj b/test/project/MyProject.xcodeproj/project.pbxproj index 7d31a9b..337f218 100644 --- a/test/project/MyProject.xcodeproj/project.pbxproj +++ b/test/project/MyProject.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -26,6 +26,10 @@ 49922DA123852057009A9FF6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + FE77422B2D68C62200F94376 /* GroupWithSynchronizedFolder */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = GroupWithSynchronizedFolder; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 49922D8D23852054009A9FF6 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -134,6 +138,7 @@ 49922D9223852054009A9FF6 /* MyProject */ = { isa = PBXGroup; children = ( + FE77422B2D68C62200F94376 /* GroupWithSynchronizedFolder */, 496EC49E245AA83600CE2F8F /* GroupWithoutFolder */, 496EC499245AA7D700CE2F8F /* GroupWithFolder */, 49922D9323852054009A9FF6 /* AppDelegate.swift */, @@ -162,6 +167,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + FE77422B2D68C62200F94376 /* GroupWithSynchronizedFolder */, + ); name = MyProject; productName = MyProject; productReference = 49922D9023852054009A9FF6 /* MyProject.app */; diff --git a/test/project/MyProject/GroupWithSynchronizedFolder/Coordinator/.gitkeep b/test/project/MyProject/GroupWithSynchronizedFolder/Coordinator/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/project/MyProject/GroupWithSynchronizedFolder/Interactor/.gitkeep b/test/project/MyProject/GroupWithSynchronizedFolder/Interactor/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/project/MyProject/GroupWithSynchronizedFolder/Presenter/.gitkeep b/test/project/MyProject/GroupWithSynchronizedFolder/Presenter/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/project/MyProject/GroupWithSynchronizedFolder/Repository/.gitkeep b/test/project/MyProject/GroupWithSynchronizedFolder/Repository/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/swift_package/.gitignore b/test/swift_package/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/test/swift_package/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/test/swift_package/Package.swift b/test/swift_package/Package.swift new file mode 100644 index 0000000..145684f --- /dev/null +++ b/test/swift_package/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version: 6.1 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "swift_package", + platforms: [ + .iOS(.v18) + ], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "swift_package", + targets: ["App"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "App", + dependencies: ["Core", "Data"] + ), + .target(name: "Core"), + .target( + name: "Data", + dependencies: ["Core"] + ) + ] +) diff --git a/test/swift_package/Sources/App/App.swift b/test/swift_package/Sources/App/App.swift new file mode 100644 index 0000000..08b22b8 --- /dev/null +++ b/test/swift_package/Sources/App/App.swift @@ -0,0 +1,2 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book diff --git a/test/swift_package/Sources/App/Coordinator/.gitkeep b/test/swift_package/Sources/App/Coordinator/.gitkeep new file mode 100644 index 0000000..08b22b8 --- /dev/null +++ b/test/swift_package/Sources/App/Coordinator/.gitkeep @@ -0,0 +1,2 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book diff --git a/test/swift_package/Sources/App/Screen/.gitkeep b/test/swift_package/Sources/App/Screen/.gitkeep new file mode 100644 index 0000000..08b22b8 --- /dev/null +++ b/test/swift_package/Sources/App/Screen/.gitkeep @@ -0,0 +1,2 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book diff --git a/test/swift_package/Sources/Core/Core.swift b/test/swift_package/Sources/Core/Core.swift new file mode 100644 index 0000000..08b22b8 --- /dev/null +++ b/test/swift_package/Sources/Core/Core.swift @@ -0,0 +1,2 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book diff --git a/test/swift_package/Sources/Core/Interactor/.gitkeep b/test/swift_package/Sources/Core/Interactor/.gitkeep new file mode 100644 index 0000000..08b22b8 --- /dev/null +++ b/test/swift_package/Sources/Core/Interactor/.gitkeep @@ -0,0 +1,2 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book diff --git a/test/swift_package/Sources/Core/Repository/.gitkeep b/test/swift_package/Sources/Core/Repository/.gitkeep new file mode 100644 index 0000000..08b22b8 --- /dev/null +++ b/test/swift_package/Sources/Core/Repository/.gitkeep @@ -0,0 +1,2 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book diff --git a/test/swift_package/Sources/Data/Data.swift b/test/swift_package/Sources/Data/Data.swift new file mode 100644 index 0000000..08b22b8 --- /dev/null +++ b/test/swift_package/Sources/Data/Data.swift @@ -0,0 +1,2 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book diff --git a/test/test_swift_package.rb b/test/test_swift_package.rb new file mode 100644 index 0000000..bc4ecf6 --- /dev/null +++ b/test/test_swift_package.rb @@ -0,0 +1,132 @@ +require 'minitest/autorun' +require_relative '../lib/ccios/templates_loader' +require_relative '../lib/ccios/file_creator' +require 'tempfile' +require 'tmpdir' + +class SwiftPackageTest < Minitest::Test + + def setup + FileCreator.logger.level = Logger::ERROR + @test_dir = set_up_project_in_temporary_directory + end + + def teardown + FileUtils.remove_entry(@test_dir) + end + + def test_coordinator_without_folders + generate_and_assert( + source_path: "Sources", + template_name: "coordinator", + name: "Cotest", + expected_files: ["App/Coordinator/CotestCoordinator.swift"] + ) + end + + def test_interactor_without_folders + generate_and_assert( + source_path: "Sources", + template_name: "interactor", + name: "Intest", + expected_files: [ + "Core/Interactor/IntestInteractor/IntestInteractor.swift", + "Core/Interactor/IntestInteractor/IntestInteractorImplementation.swift" + ] + ) + end + + def test_presenter_without_folders + generate_and_assert( + source_path: "Sources", + template_name: "presenter", + name: "Pretest", + expected_files: [ + "App/Screen/Pretest/UI/View", + "App/Screen/Pretest/UI/ViewController/PretestViewController.swift", + "App/Screen/Pretest/UI/PretestViewContract.swift", + "App/Screen/Pretest/Presenter/PretestPresenter.swift", + "App/Screen/Pretest/Presenter/PretestPresenterImplementation.swift", + "App/Screen/Pretest/Model", + ] + ) + end + + def test_repository_data_folders + generate_and_assert( + source_path: "Sources", + template_name: "repository", + name: "Retest", + expected_files: ["Data/Repository/Retest/RetestRepositoryImplementation.swift"] + ) + end + + def test_repository_core_folders + generate_and_assert( + source_path: "Sources", + template_name: "repository", + name: "Retest", + expected_files: ["Core/Repository/Retest/RetestRepository.swift"] + ) + end + + private + + def setup_parser + yml_path = File.join(@test_dir, ".ccios.yml") + File.open(yml_path, "w") do |io| + io.puts ccios_yml_swift_package_content + end + + config = Config.parse yml_path + yield(config) + end + + def generate_and_assert(source_path:, template_name:, name:, expected_files:) + setup_parser do |config| + template = TemplatesLoader.new.get_templates(config)[template_name] + raise "Template not found #{template_name}" if template.nil? + + Dir.chdir(@test_dir) do + template.generate(nil, {"name" => name}, config) + + expected_files.each do |file_name| + expected_path = File.join(@test_dir, source_path, file_name) + assert File.exist?(expected_path), "File #{expected_path} does not exist" + end + end + end + end + + def set_up_project_in_temporary_directory + dir = Dir.mktmpdir("ccios-tests", "/tmp") + project_path = File.join(Dir.pwd, "test", "project") + FileUtils.copy_entry project_path, dir + dir + end + + def ccios_yml_swift_package_content + <<-eos +variables: + project_type: filesystem + +templates_config: + repository: + variables: {} + elements_variables: + repository: + base_path: Sources/Core/Repository + repository_implementation: + base_path: Sources/Data/Repository + presenter: + variables: + base_path: Sources/App/Screen + coordinator: + variables: + base_path: Sources/App/Coordinator + interactor: + variables: + base_path: Sources/Core/Interactor +eos + end +end diff --git a/test/test_xcodeproj_with_synchronized_folder.rb b/test/test_xcodeproj_with_synchronized_folder.rb new file mode 100644 index 0000000..066d423 --- /dev/null +++ b/test/test_xcodeproj_with_synchronized_folder.rb @@ -0,0 +1,166 @@ +require 'minitest/autorun' +require_relative '../lib/ccios/templates_loader' +require_relative '../lib/ccios/file_creator' +require 'tempfile' +require 'tmpdir' +require 'xcodeproj' + +class XcodeProjWithSynchronizedFolderTest < Minitest::Test + + def setup + FileCreator.logger.level = Logger::ERROR + @test_dir = set_up_project_in_temporary_directory + end + + def teardown + FileUtils.remove_entry(@test_dir) + end + + [false, true].each do |use_suffix| + method_suffix = use_suffix ? "_with_suffix" : "" + name = use_suffix ? "CotestCoordinator" : "Cotest" + define_method "test_coordinator_folders#{method_suffix}" do + generate_and_assert( + source_path: "MyProject/GroupWithSynchronizedFolder/Coordinator", + template_name: "coordinator", + name: name, + expected_files: ["CotestCoordinator.swift"] + ) + end + end + + [false, true].each do |use_suffix| + method_suffix = use_suffix ? "_with_suffix" : "" + name = use_suffix ? "IntestInteractor" : "Intest" + define_method "test_interactor_folders#{method_suffix}" do + generate_and_assert( + source_path: "MyProject/GroupWithSynchronizedFolder/Interactor", + template_name: "interactor", + name: name, + expected_files: [ + "IntestInteractor/IntestInteractor.swift", + "IntestInteractor/IntestInteractorImplementation.swift" + ] + ) + end + end + + [false, true].each do |use_suffix| + method_suffix = use_suffix ? "_with_suffix" : "" + name = use_suffix ? "PretestPresenter" : "Pretest" + define_method "test_presenter_folders#{method_suffix}" do + generate_and_assert( + source_path: "MyProject/GroupWithSynchronizedFolder/Presenter", + template_name: "presenter", + name: name, + expected_files: [ + "Pretest/UI/View", + "Pretest/UI/ViewController/PretestViewController.swift", + "Pretest/UI/PretestViewContract.swift", + "Pretest/Presenter/PretestPresenter.swift", + "Pretest/Presenter/PretestPresenterImplementation.swift", + "Pretest/Model" + ] + ) + end + end + + [false, true].each do |use_suffix| + method_suffix = use_suffix ? "_with_suffix" : "" + name = use_suffix ? "RetestRepository" : "Retest" + define_method "test_repository_data_folders#{method_suffix}" do + generate_and_assert( + source_path: "MyProject/GroupWithSynchronizedFolder/Repository", + template_name: "repository", + name: name, + expected_files: ["Retest/RetestRepositoryImplementation.swift"] + ) + end + end + + [false, true].each do |use_suffix| + method_suffix = use_suffix ? "_with_suffix" : "" + name = use_suffix ? "RetestRepository" : "Retest" + define_method "test_repository_core_folders#{method_suffix}" do + generate_and_assert( + source_path: "MyProject/GroupWithSynchronizedFolder/Interactor", + template_name: "repository", + name: name, + expected_files: ["Retest/RetestRepository.swift"] + ) + end + end + + private + + def setup_parser + yml_path = File.join(@test_dir, ".ccios.yml") + File.open(yml_path, "w") do |io| + io.puts ccios_yml_with_folders_content + end + + config = Config.parse yml_path + parser = PBXProjParser.new(@test_dir, config) + yield(config, parser) + end + + def generate_and_assert(source_path:, template_name:, name:, expected_files:) + setup_parser do |config, parser| + template = TemplatesLoader.new.get_templates(config)[template_name] + raise "Template not found #{template_name}" if template.nil? + + Dir.chdir(@test_dir) do + template.generate(parser, {"name" => name}, config) + + expected_files.each do |file_name| + expected_path = File.join(@test_dir, source_path, file_name) + assert File.exist?(expected_path), "File #{expected_path} does not exist" + end + end + end + end + + def assert_group_and_subgroups_have_no_name(group) + subgroups = group.children.select { |o| o.isa == "PBXGroup" } + subgroups.each { |subgroup| + assert_nil subgroup.name + refute_nil subgroup.path + # recursively test children + assert_group_and_subgroups_have_no_name subgroup + } + end + + def set_up_project_in_temporary_directory + dir = Dir.mktmpdir("ccios-tests", "/tmp") + project_path = File.join(Dir.pwd, "test", "project") + FileUtils.copy_entry project_path, dir + dir + end + + def ccios_yml_with_folders_content + <<-eos +variables: + project_type: filesystem + project: MyProject.xcodeproj + +templates_config: + repository: + variables: {} + elements_variables: + repository: + base_path: MyProject/GroupWithSynchronizedFolder/Interactor + repository_implementation: + base_path: MyProject/GroupWithSynchronizedFolder/Repository + presenter: + variables: + base_path: MyProject/GroupWithSynchronizedFolder/Presenter + coordinator: + variables: + base_path: MyProject/GroupWithSynchronizedFolder/Coordinator + interactor: + variables: + base_path: MyProject/GroupWithSynchronizedFolder/Interactor +eos + end + +end