lib/slather/project.rb in slather-1.8.3 vs lib/slather/project.rb in slather-2.0.0

- old
+ new

@@ -1,47 +1,83 @@ require 'fileutils' require 'xcodeproj' require 'json' require 'yaml' +require 'shellwords' module Xcodeproj + class Project - def slather_setup_for_coverage + def slather_setup_for_coverage(format = :auto) + unless [:gcov, :clang, :auto].include?(format) + raise StandardError, "Only supported formats for setup are gcov, clang or auto" + end + if format == :auto + format = Slather.xcode_version[0] < 7 ? :gcov : :clang + end + build_configurations.each do |build_configuration| - build_configuration.build_settings["GCC_INSTRUMENT_PROGRAM_FLOW_ARCS"] = "YES" - build_configuration.build_settings["GCC_GENERATE_TEST_COVERAGE_FILES"] = "YES" + if format == :clang + build_configuration.build_settings["CLANG_ENABLE_CODE_COVERAGE"] = "YES" + else + build_configuration.build_settings["GCC_INSTRUMENT_PROGRAM_FLOW_ARCS"] = "YES" + build_configuration.build_settings["GCC_GENERATE_TEST_COVERAGE_FILES"] = "YES" + end end + + # Patch xcschemes too + if format == :clang + if Gem::Requirement.new('~> 0.27') =~ Gem::Version.new(Xcodeproj::VERSION) + # @todo This will require to bump the xcodeproj dependency to ~> 0.27 + # (which would require to bump cocoapods too) + schemes_path = Xcodeproj::XCScheme.shared_data_dir(self.path) + Xcodeproj::Project.schemes(self.path).each do |scheme_name| + xcscheme_path = "#{schemes_path + scheme_name}.xcscheme" + xcscheme = Xcodeproj::XCScheme.new(xcscheme_path) + xcscheme.test_action.xml_element.attributes['codeCoverageEnabled'] = 'YES' + xcscheme.save_as(self.path, scheme_name) + end + else + # @todo In the meantime, simply inform the user to do it manually + puts %Q(Ensure you enabled "Gather coverage data" in each of your scheme's Test action) + end + end end end end module Slather class Project < Xcodeproj::Project - attr_accessor :build_directory, :ignore_list, :ci_service, :coverage_service, :coverage_access_token, :source_directory, :output_directory, :xcodeproj, :show_html + attr_accessor :build_directory, :ignore_list, :ci_service, :coverage_service, :coverage_access_token, :source_directory, + :output_directory, :xcodeproj, :show_html, :verbose_mode, :input_format, :scheme, :binary_file, :binary_basename alias_method :setup_for_coverage, :slather_setup_for_coverage def self.open(xcodeproj) proj = super - proj.configure_from_yml proj.xcodeproj = xcodeproj proj end - def derived_data_dir + def derived_data_path File.expand_path('~') + "/Library/Developer/Xcode/DerivedData/" end - private :derived_data_dir + private :derived_data_path - def build_directory - @build_directory || derived_data_dir + def coverage_files + if self.input_format == "profdata" + profdata_coverage_files + else + gcov_coverage_files + end end + private :coverage_files - def coverage_files + def gcov_coverage_files coverage_files = Dir["#{build_directory}/**/*.gcno"].map do |file| coverage_file = coverage_file_class.new(self, file) # If there's no source file for this gcno, it probably belongs to another project. coverage_file.source_file_pathname && !coverage_file.ignored? ? coverage_file : nil end.compact @@ -50,12 +86,84 @@ raise StandardError, "No coverage files found. Are you sure your project is setup for generating coverage files? Try `slather setup your/project.xcodeproj`" else dedupe(coverage_files) end end - private :coverage_files + private :gcov_coverage_files + def profdata_coverage_files + files = profdata_llvm_cov_output.split("\n\n") + + files.map do |source| + coverage_file = coverage_file_class.new(self, source) + !coverage_file.ignored? ? coverage_file : nil + end.compact + end + private :profdata_coverage_files + + def profdata_coverage_dir + raise StandardError, "The specified build directory (#{self.build_directory}) does not exist" unless File.exists?(self.build_directory) + dir = nil + if self.scheme + dir = Dir["#{build_directory}/**/CodeCoverage/#{self.scheme}"].first + else + dir = Dir["#{build_directory}/**/#{self.products.first.name}"].first + end + + raise StandardError, "No coverage directory found. Are you sure your project is setup for generating coverage files? Try `slather setup your/project.xcodeproj`" unless dir != nil + dir + end + + def profdata_file + profdata_coverage_dir = self.profdata_coverage_dir + if profdata_coverage_dir == nil + raise StandardError, "No coverage directory found. Please make sure the \"Code Coverage\" checkbox is enabled in your scheme's Test action or the build_directory property is set." + end + + file = Dir["#{profdata_coverage_dir}/**/Coverage.profdata"].first + unless file != nil + return nil + end + return File.expand_path(file) + end + private :profdata_file + + def find_binary_file_for_app(app_bundle_file) + app_bundle_file_name_noext = Pathname.new(app_bundle_file).basename.to_s.gsub(".app", "") + Dir["#{app_bundle_file}/**/#{app_bundle_file_name_noext}"].first + end + + def find_binary_file_for_dynamic_lib(framework_bundle_file) + framework_bundle_file_name_noext = Pathname.new(framework_bundle_file).basename.to_s.gsub(".framework", "") + "#{framework_bundle_file}/#{framework_bundle_file_name_noext}" + end + + def find_binary_file_for_static_lib(xctest_bundle_file) + xctest_bundle_file_name_noext = Pathname.new(xctest_bundle_file).basename.to_s.gsub(".xctest", "") + Dir["#{xctest_bundle_file}/**/#{xctest_bundle_file_name_noext}"].first + end + + def unsafe_profdata_llvm_cov_output + profdata_file_arg = profdata_file + if profdata_file_arg == nil + raise StandardError, "No Coverage.profdata files found. Please make sure the \"Code Coverage\" checkbox is enabled in your scheme's Test action or the build_directory property is set." + end + + if self.binary_file == nil + raise StandardError, "No binary file found." + end + + llvm_cov_args = %W(show -instr-profile #{profdata_file_arg} #{self.binary_file}) + `xcrun llvm-cov #{llvm_cov_args.shelljoin}` + end + private :unsafe_profdata_llvm_cov_output + + def profdata_llvm_cov_output + unsafe_profdata_llvm_cov_output.encode!('UTF-8', 'binary', :invalid => :replace, undef: :replace) + end + private :profdata_llvm_cov_output + def dedupe(coverage_files) coverage_files.group_by(&:source_file_pathname).values.map { |cf_array| cf_array.max_by(&:percentage_lines_tested) } end private :dedupe @@ -65,49 +173,77 @@ def self.yml @yml ||= File.exist?(yml_filename) ? YAML.load_file(yml_filename) : {} end - def configure_from_yml - configure_build_directory_from_yml - configure_ignore_list_from_yml - configure_ci_service_from_yml - configure_coverage_access_token_from_yml - configure_coverage_service_from_yml - configure_source_directory_from_yml - configure_output_directory_from_yml + def configure + configure_build_directory + configure_ignore_list + configure_ci_service + configure_coverage_access_token + configure_coverage_service + configure_source_directory + configure_output_directory + configure_input_format + configure_scheme + configure_binary_file + + if self.verbose_mode + puts "\nProcessing coverage file: #{profdata_file}" + puts "Against binary file: #{self.binary_file}\n\n" + end end - def configure_build_directory_from_yml - self.build_directory = self.class.yml["build_directory"] if self.class.yml["build_directory"] && !@build_directory + def configure_build_directory + self.build_directory ||= self.class.yml["build_directory"] || derived_data_path end - def configure_source_directory_from_yml + def configure_source_directory self.source_directory ||= self.class.yml["source_directory"] if self.class.yml["source_directory"] end - def configure_output_directory_from_yml + def configure_output_directory self.output_directory ||= self.class.yml["output_directory"] if self.class.yml["output_directory"] end - def configure_ignore_list_from_yml + def configure_ignore_list self.ignore_list ||= [(self.class.yml["ignore"] || [])].flatten end - def configure_ci_service_from_yml + def configure_ci_service self.ci_service ||= (self.class.yml["ci_service"] || :travis_ci) end + def configure_input_format + self.input_format ||= self.class.yml["input_format"] || input_format + end + + def input_format=(format) + format ||= "auto" + unless %w(gcov profdata auto).include?(format) + raise StandardError, "Only supported input formats are gcov, profdata or auto" + end + if format == "auto" + @input_format = Slather.xcode_version[0] < 7 ? "gcov" : "profdata" + else + @input_format = format + end + end + + def configure_scheme + self.scheme ||= self.class.yml["scheme"] if self.class.yml["scheme"] + end + def ci_service=(service) @ci_service = service && service.to_sym end - def configure_coverage_service_from_yml + def configure_coverage_service self.coverage_service ||= (self.class.yml["coverage_service"] || :terminal) end - def configure_coverage_access_token_from_yml + def configure_coverage_access_token self.coverage_access_token ||= (ENV["COVERAGE_ACCESS_TOKEN"] || self.class.yml["coverage_access_token"] || "") end def coverage_service=(service) service = service && service.to_sym @@ -126,7 +262,35 @@ else raise ArgumentError, "`#{coverage_service}` is not a valid coverage service. Try `terminal`, `coveralls`, `gutter_json`, `cobertura_xml` or `html`" end @coverage_service = service end + + def configure_binary_file + if self.input_format == "profdata" + self.binary_file ||= self.class.yml["binary_file"] || File.expand_path(find_binary_file) + end + end + + def find_binary_file + xctest_bundle = Dir["#{profdata_coverage_dir}/**/*.xctest"].reject { |bundle| + bundle.include? "-Runner.app/PlugIns/" + }.first + raise StandardError, "No product binary found in #{profdata_coverage_dir}. Are you sure your project is setup for generating coverage files? Try `slather setup your/project.xcodeproj`" unless xctest_bundle != nil + + # Find the matching binary file + search_for = self.binary_basename || self.class.yml["binary_basename"] || '*' + xctest_bundle_file_directory = Pathname.new(xctest_bundle).dirname + app_bundle = Dir["#{xctest_bundle_file_directory}/#{search_for}.app"].first + dynamic_lib_bundle = Dir["#{xctest_bundle_file_directory}/#{search_for}.framework"].first + + if app_bundle != nil + find_binary_file_for_app(app_bundle) + elsif dynamic_lib_bundle != nil + find_binary_file_for_dynamic_lib(dynamic_lib_bundle) + else + find_binary_file_for_static_lib(xctest_bundle) + end + end + end end