class Slather::Project

Attributes

arch[RW]
binary_basename[RW]
binary_file[RW]
build_directory[RW]
ci_service[RW]
configuration[RW]
coverage_access_token[RW]
coverage_service[RW]
decimals[RW]
ignore_list[RW]
input_format[RW]
llvm_version[RW]
output_directory[RW]
scheme[RW]
show_html[RW]
source_directory[RW]
source_files[RW]
verbose_mode[RW]
workspace[RW]
xcodeproj[RW]

Public Class Methods

open(xcodeproj) click to toggle source
Calls superclass method
# File lib/slather/project.rb, line 52
def self.open(xcodeproj)
  proj = super
  proj.xcodeproj = xcodeproj
  proj
end
yml() click to toggle source
# File lib/slather/project.rb, line 318
def self.yml
  @yml ||= File.exist?(yml_filename) ? YAML.load_file(yml_filename) : {}
end
yml_filename() click to toggle source
# File lib/slather/project.rb, line 314
def self.yml_filename
  '.slather.yml'
end

Public Instance Methods

ci_service=(service) click to toggle source
# File lib/slather/project.rb, line 415
def ci_service=(service)
  @ci_service = service && service.to_sym
end
configure() click to toggle source
# File lib/slather/project.rb, line 322
def configure
  begin
    configure_scheme
    configure_configuration
    configure_workspace
    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_arch
    configure_binary_file
    configure_decimals

    self.llvm_version = `xcrun llvm-cov --version`.match(/LLVM version ([\d\.]+)/).captures[0]
  rescue => e
    puts e.message
    puts failure_help_string
    puts "\n"
    raise
  end

  if self.verbose_mode
    puts "\nProcessing coverage file: #{profdata_file}"
    if self.binary_file
      puts "Against binary files:"
      self.binary_file.each do |binary_file|
        puts "\t#{binary_file}"
      end
    else
      puts "No binary files found."
    end
    puts "\n"
  end
end
configure_arch() click to toggle source
# File lib/slather/project.rb, line 460
def configure_arch
  self.arch ||= self.class.yml["arch"] if self.class.yml["arch"]
end
configure_binary_file() click to toggle source
# File lib/slather/project.rb, line 454
def configure_binary_file
  if self.input_format == "profdata"
    self.binary_file = load_option_array("binary_file") || find_binary_files
  end
end
configure_build_directory() click to toggle source
# File lib/slather/project.rb, line 361
def configure_build_directory
  self.build_directory ||= self.class.yml["build_directory"] || derived_data_path
end
configure_ci_service() click to toggle source
# File lib/slather/project.rb, line 377
def configure_ci_service
  self.ci_service ||= (self.class.yml["ci_service"] || :travis_ci)
end
configure_configuration() click to toggle source
# File lib/slather/project.rb, line 401
def configure_configuration
  self.configuration ||= self.class.yml["configuration"] if self.class.yml["configuration"]
end
configure_coverage_access_token() click to toggle source
# File lib/slather/project.rb, line 423
def configure_coverage_access_token
  self.coverage_access_token ||= (ENV["COVERAGE_ACCESS_TOKEN"] || self.class.yml["coverage_access_token"] || "")
end
configure_coverage_service() click to toggle source
# File lib/slather/project.rb, line 419
def configure_coverage_service
  self.coverage_service ||= (self.class.yml["coverage_service"] || :terminal)
end
configure_decimals() click to toggle source
# File lib/slather/project.rb, line 405
def configure_decimals
  return if self.decimals
  self.decimals ||= self.class.yml["decimals"] if self.class.yml["decimals"]
  self.decimals = self.decimals ? Integer(self.decimals) : 2
end
configure_ignore_list() click to toggle source
# File lib/slather/project.rb, line 373
def configure_ignore_list
  self.ignore_list ||= [(self.class.yml["ignore"] || [])].flatten
end
configure_input_format() click to toggle source
# File lib/slather/project.rb, line 381
def configure_input_format
  self.input_format ||= (self.class.yml["input_format"] || "auto")
end
configure_output_directory() click to toggle source
# File lib/slather/project.rb, line 369
def configure_output_directory
  self.output_directory ||= self.class.yml["output_directory"] if self.class.yml["output_directory"]
end
configure_scheme() click to toggle source
# File lib/slather/project.rb, line 397
def configure_scheme
  self.scheme ||= self.class.yml["scheme"] if self.class.yml["scheme"]
end
configure_source_directory() click to toggle source
# File lib/slather/project.rb, line 365
def configure_source_directory
  self.source_directory ||= self.class.yml["source_directory"] if self.class.yml["source_directory"]
end
configure_workspace() click to toggle source
# File lib/slather/project.rb, line 411
def configure_workspace
  self.workspace ||= self.class.yml["workspace"] if self.class.yml["workspace"]
end
coverage_files() click to toggle source
# File lib/slather/project.rb, line 96
def coverage_files
  if self.input_format == "profdata"
    profdata_coverage_files
  else
    gcov_coverage_files
  end
end
coverage_service=(service) click to toggle source
# File lib/slather/project.rb, line 427
def coverage_service=(service)
  service = service && service.to_sym
  case service
  when :coveralls
    extend(Slather::CoverageService::Coveralls)
  when :hardcover
    extend(Slather::CoverageService::Hardcover)
  when :terminal
    extend(Slather::CoverageService::SimpleOutput)
  when :gutter_json
    extend(Slather::CoverageService::GutterJsonOutput)
  when :cobertura_xml
    extend(Slather::CoverageService::CoberturaXmlOutput)
  when :llvm_cov
    extend(Slather::CoverageService::LlvmCovOutput)
  when :html
    extend(Slather::CoverageService::HtmlOutput)
  when :json
    extend(Slather::CoverageService::JsonOutput)
  when :sonarqube_xml
    extend(Slather::CoverageService::SonarqubeXmlOutput)
  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
decimal_f(decimal_arg) click to toggle source
# File lib/slather/project.rb, line 464
def decimal_f decimal_arg
  configure_decimals unless decimals
  decimal = "%.#{decimals}f" % decimal_arg
  return decimal if decimals == 2 # special case 2 for backwards compatibility
  decimal.to_f.to_s
end
failure_help_string() click to toggle source
# File lib/slather/project.rb, line 58
def failure_help_string
  "\n\tAre you sure your project is generating coverage? Make sure you enable code coverage in the Test section of your Xcode scheme.\n\tDid you specify your Xcode scheme? (--scheme or 'scheme' in .slather.yml)\n\tIf you're using a workspace, did you specify it? (--workspace or 'workspace' in .slather.yml)\n\tIf you use a different Xcode configuration, did you specify it? (--configuration or 'configuration' in .slather.yml)"
end
find_binary_file_in_bundle(bundle_file) click to toggle source
# File lib/slather/project.rb, line 471
def find_binary_file_in_bundle(bundle_file)
  if File.directory? bundle_file
    bundle_file_noext = File.basename(bundle_file, File.extname(bundle_file))
    Dir["#{bundle_file}/**/#{bundle_file_noext}"].first
  else
    bundle_file
  end
end
find_binary_files() click to toggle source
# File lib/slather/project.rb, line 480
def find_binary_files
  binary_basename = load_option_array("binary_basename")
  found_binaries = []

  # Get scheme info out of the xcodeproj
  if self.scheme
    schemes_path = Xcodeproj::XCScheme.shared_data_dir(self.path)
    xcscheme_path = "#{schemes_path + self.scheme}.xcscheme"

    # Try to look inside 'xcuserdata' if the scheme is not found in 'xcshareddata'
    if !File.file?(xcscheme_path)
      schemes_path = Xcodeproj::XCScheme.user_data_dir(self.path)
      xcscheme_path = "#{schemes_path + self.scheme}.xcscheme"
    end

    if self.workspace and !File.file?(xcscheme_path)
      # No scheme was found in the xcodeproj, check the workspace
      schemes_path = Xcodeproj::XCScheme.shared_data_dir(self.workspace)
      xcscheme_path = "#{schemes_path + self.scheme}.xcscheme"

      if !File.file?(xcscheme_path)
        schemes_path = Xcodeproj::XCScheme.user_data_dir(self.workspace)
        xcscheme_path = "#{schemes_path + self.scheme}.xcscheme"
      end
    end

    raise StandardError, "No scheme named '#{self.scheme}' found in #{self.path}" unless File.exists? xcscheme_path

    xcscheme = Xcodeproj::XCScheme.new(xcscheme_path)

    if self.configuration
      configuration = self.configuration
    else
      configuration = xcscheme.test_action.build_configuration
    end

    search_list = binary_basename || find_buildable_names(xcscheme)
    search_dir = profdata_coverage_dir

    if Slather.xcode_version[0] >= 9
      # Go from the directory containing Coverage.profdata back to the directory containing Products (back out of ProfileData/UUID-dir)
      search_dir = File.join(search_dir, '../..')
    end

    search_list.each do |search_for|
      found_product = Dir["#{search_dir}/Products/#{configuration}*/#{search_for}*"].sort { |x, y|
        # Sort the matches without the file extension to ensure better matches when there are multiple candidates
        # For example, if the binary_basename is Test then we want Test.app to be matched before Test Helper.app
        File.basename(x, File.extname(x)) <=> File.basename(y, File.extname(y))
      }.find { |path|
        next if path.end_with? ".dSYM"
        next if path.end_with? ".swiftmodule"

        if File.directory? path
          path = find_binary_file_in_bundle(path)
          next if path.nil?
        end

        matches_arch(path)
      }

      if found_product and File.directory? found_product
        found_binary = find_binary_file_in_bundle(found_product)
      else
        found_binary = found_product
      end

      if found_binary
        found_binaries.push(found_binary)
      end
    end
  else
    xctest_bundle = Dir["#{profdata_coverage_dir}/**/*.xctest"].reject { |bundle|
        # Ignore xctest bundles that are in the UI runner app
        bundle.include? "-Runner.app/PlugIns/"
    }.first

    # Find the matching binary file
    search_list = binary_basename || ['*']

    search_list.each do |search_for|
      xctest_bundle_file_directory = Pathname.new(xctest_bundle).dirname
      app_bundle = Dir["#{xctest_bundle_file_directory}/#{search_for}.app"].first
      matched_xctest_bundle = Dir["#{xctest_bundle_file_directory}/#{search_for}.xctest"].first
      dynamic_lib_bundle = Dir["#{xctest_bundle_file_directory}/#{search_for}.{framework,dylib}"].first

      if app_bundle != nil
        found_binary = find_binary_file_in_bundle(app_bundle)
      elsif matched_xctest_bundle != nil
        found_binary = find_binary_file_in_bundle(matched_xctest_bundle)
      elsif dynamic_lib_bundle != nil
        found_binary = find_binary_file_in_bundle(dynamic_lib_bundle)
      else
        found_binary = find_binary_file_in_bundle(xctest_bundle)
      end

      if found_binary
        found_binaries.push(found_binary)
      end
    end
  end

  raise StandardError, "No product binary found in #{profdata_coverage_dir}." unless found_binaries.count > 0

  found_binaries.map { |binary| File.expand_path(binary) }
end
find_buildable_names(xcscheme) click to toggle source
# File lib/slather/project.rb, line 587
def find_buildable_names(xcscheme)
  found_buildable_names = []

  # enumerate code coverage targets
  begin
    code_coverage_targets = xcscheme.test_action.xml_element.elements['CodeCoverageTargets']
    targets = code_coverage_targets.map do |node|
      Xcodeproj::XCScheme::BuildableReference.new(node) if node.is_a?(REXML::Element)
    end.compact
    buildable_names = targets.each do |target|
      found_buildable_names.push(target.buildable_name)
    end
  rescue
    # just in case if there are no entries in the test action
  end

  # enumerate build action entries
  begin
    xcscheme.build_action.entries.each do |entry|
      buildable_name = entry.buildable_references[0].buildable_name

      if !buildable_name.end_with? ".a"
        # Can't run code coverage on static libraries
        found_buildable_names.push(buildable_name)
      end
    end
  rescue
    # xcodeproj will raise an exception if there are no entries in the build action
  end

  # enumerate test action entries
  begin
    xcscheme.test_action.testables.each do |entry|
      buildable_name = entry.buildable_references[0].buildable_name
      found_buildable_names.push(buildable_name)
    end
  rescue
    # just in case if there are no entries in the test action
  end

  # some items are both buildable and testable, so return only unique ones
  found_buildable_names.uniq
end
find_source_files() click to toggle source
# File lib/slather/project.rb, line 641
def find_source_files
  source_files = load_option_array("source_files")
  return if source_files.nil?

  current_dir = Pathname("./").realpath
  paths = source_files.flat_map { |pattern| Dir.glob(pattern) }.uniq

  paths.map do |path|
    source_file_absolute_path = Pathname(path).realpath
    source_file_relative_path = source_file_absolute_path.relative_path_from(current_dir)
    self.ignore_list.any? { |ignore| File.fnmatch(ignore, source_file_relative_path) } ? nil : source_file_absolute_path
  end.compact
end
first_product_name() click to toggle source
# File lib/slather/project.rb, line 202
def first_product_name
  first_product = self.products.first
  # If name is not available it computes it using
  # the path by dropping the 'extension' of the path.
  first_product.name || remove_extension(first_product.path)
end
input_format=(format) click to toggle source
# File lib/slather/project.rb, line 385
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
load_option_array(option) click to toggle source
# File lib/slather/project.rb, line 655
def load_option_array(option)
  value = self.send(option.to_sym)
  # Only load if a value is not already set
  if !value
    value_yml = self.class.yml[option]
    # Need to check the type in the config file because it can be a string or array
    if value_yml and value_yml.is_a? Array
      value = value_yml
    elsif value_yml
      value = [value_yml]
    end
  end
  value
end
matches_arch(binary_path) click to toggle source
# File lib/slather/project.rb, line 631
def matches_arch(binary_path)
  if self.arch
    lipo_output = `lipo -info "#{binary_path}"`
    archs_in_binary = lipo_output.split(':').last.split(' ')
    archs_in_binary.include? self.arch
  else
    true
  end
end
profdata_coverage_dir() click to toggle source
# File lib/slather/project.rb, line 209
def profdata_coverage_dir
  @profdata_coverage_dir ||= begin
    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[File.join(build_directory,"/**/CodeCoverage/#{self.scheme}")].first
    else
      dir = Dir[File.join(build_directory,"/**/#{first_product_name}")].first
    end

    if dir == nil
      # Xcode 7.3 moved the location of Coverage.profdata
      dir = Dir[File.join(build_directory,"/**/CodeCoverage")].first
    end

    if dir == nil && Slather.xcode_version[0] >= 9
      # Xcode 9 moved the location of Coverage.profdata
      coverage_files = Dir[File.join(build_directory, "/**/ProfileData/*/Coverage.profdata")]

      if coverage_files.count == 0
        # Look up one directory
        # The ProfileData directory is next to Intermediates.noindex (in previous versions of Xcode the coverage was inside Intermediates)
        coverage_files = Dir[File.join(build_directory, "../**/ProfileData/*/Coverage.profdata")]
      end

      if coverage_files != nil && coverage_files.count != 0
        dir = Pathname.new(coverage_files.first).parent()
      end
    end

    raise StandardError, "No coverage directory found." unless dir != nil
    dir
  end
end
remove_extension(path) click to toggle source
# File lib/slather/project.rb, line 198
def remove_extension(path)
  path.split(".")[0..-2].join(".")
end

Private Instance Methods

create_coverage_files(binary_path, path_objects) click to toggle source
# File lib/slather/project.rb, line 170
def create_coverage_files(binary_path, path_objects)
  line_numbers_first = Gem::Version.new(self.llvm_version) >= Gem::Version.new('8.1.0')
  # get just file names from the path objects
  pathnames = path_objects.map { |path_obj| path_obj["filename"] }.compact
  # Map of path name => segment array
  paths_to_segments = path_objects.reduce(Hash.new) do |hash, path_obj|
    hash[path_obj["filename"]] = path_obj["segments"]
    hash
  end
  files = create_profdata(binary_path, pathnames)
  files.map do |source|
    coverage_file = coverage_file_class.new(self, source, line_numbers_first)
    # If a single source file is used, the resulting output does not contain the file name.
    coverage_file.source_file_pathname = pathnames.first if pathnames.count == 1
    # if there is segment data for the given path, add it to the coverage_file
    if paths_to_segments.key?(coverage_file.source_file_pathname)
      coverage_file.segments = paths_to_segments[coverage_file.source_file_pathname]
    end
    !coverage_file.ignored? ? coverage_file : nil
  end.compact
end
create_coverage_files_for_binary(binary_path, pathnames_per_binary) click to toggle source
# File lib/slather/project.rb, line 149
def create_coverage_files_for_binary(binary_path, pathnames_per_binary)
  coverage_files = []

  begin
    coverage_files.concat(create_coverage_files(binary_path, pathnames_per_binary))
  rescue Errno::E2BIG => e
    # pathnames_per_binary is too big for the OS to handle so it's split in two halfs which are processed independently
    if pathnames_per_binary.count > 1
      left, right = pathnames_per_binary.each_slice( (pathnames_per_binary.size/2.0).round ).to_a
      coverage_files.concat(create_coverage_files_for_binary(binary_path, left))
      coverage_files.concat(create_coverage_files_for_binary(binary_path, right))
    else
      # pathnames_per_binary contains one element which is too big for the OS to handle.
      raise e, "#{e}. A path in your project is close to the E2BIG limit. https://github.com/SlatherOrg/slather/pull/414", e.backtrace
    end
  end

  coverage_files
end
create_profdata(binary_path, pathnames) click to toggle source
# File lib/slather/project.rb, line 193
def create_profdata(binary_path, pathnames)
  profdata_llvm_cov_output(binary_path, pathnames).split("\n\n")
end
dedupe(coverage_files) click to toggle source
# File lib/slather/project.rb, line 309
def dedupe(coverage_files)
  coverage_files.group_by(&:source_file_pathname).values.map { |cf_array| cf_array.max_by(&:percentage_lines_tested) }
end
derived_data_path() click to toggle source
# File lib/slather/project.rb, line 62
def derived_data_path
  # Get the derived data path from xcodebuild
  # Use OBJROOT when possible, as it provides regardless of whether or not the Derived Data location is customized
  if self.workspace
    projectOrWorkspaceArgument = "-workspace \"#{self.workspace}\""
  else
    projectOrWorkspaceArgument = "-project \"#{self.path}\""
  end

  if self.scheme
    schemeArgument = "-scheme \"#{self.scheme}\""
    buildAction = "test"
  else
    schemeArgument = nil
    buildAction = nil
  end

  # redirect stderr to avoid xcodebuild errors being printed.
  build_settings = `xcodebuild #{projectOrWorkspaceArgument} #{schemeArgument} -showBuildSettings #{buildAction} CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO 2>&1`

  if build_settings
    derived_data_path = build_settings.match(/ OBJROOT = (.+)/)
    # when match fails derived_data_path is nil
    derived_data_path = derived_data_path[1] if derived_data_path
  end

  if derived_data_path == nil
    derived_data_path = File.expand_path('~') + "/Library/Developer/Xcode/DerivedData/"
  end

  derived_data_path
end
gcov_coverage_files() click to toggle source
# File lib/slather/project.rb, line 104
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

  if coverage_files.empty?
    raise StandardError, "No coverage files found."
  else
    dedupe(coverage_files)
  end
end
llvm_cov_export_output(binary_path) click to toggle source
# File lib/slather/project.rb, line 276
def llvm_cov_export_output(binary_path)
  output = unsafe_llvm_cov_export_output(binary_path)
  output.valid_encoding? ? output : output.encode!('UTF-8', 'binary', :invalid => :replace, undef: :replace)
end
pathnames_per_binary(binary_path) click to toggle source
# File lib/slather/project.rb, line 133
def pathnames_per_binary(binary_path)
  coverage_json_string = llvm_cov_export_output(binary_path)
  coverage_json = JSON.parse(coverage_json_string)
  coverage_json["data"].reduce([]) do |result, chunk|
    result.concat(chunk["files"].map do |file|
      filename = file["filename"]
      path = Pathname(filename)
      # Don't crash if the file doesn't exist on disk.
      # This may happen for autogenerated files that have been deleted.
      filename = path.exist? ? path.realpath : filename
      {"filename" => filename, "segments" => file["segments"]}
    end)
  end
end
profdata_coverage_files() click to toggle source
# File lib/slather/project.rb, line 119
def profdata_coverage_files
  coverage_files = []

  if self.binary_file
    self.binary_file.each do |binary_path|
      pathnames_per_binary = pathnames_per_binary(binary_path)
      coverage_files.concat(create_coverage_files_for_binary(binary_path, pathnames_per_binary))
    end
  end

  coverage_files
end
profdata_file() click to toggle source
# File lib/slather/project.rb, line 244
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
profdata_llvm_cov_output(binary_path, source_files) click to toggle source
# File lib/slather/project.rb, line 303
def profdata_llvm_cov_output(binary_path, source_files)
  output = unsafe_profdata_llvm_cov_output(binary_path, source_files)
  output.valid_encoding? ? output : output.encode!('UTF-8', 'binary', :invalid => :replace, undef: :replace)
end
unsafe_llvm_cov_export_output(binary_path) click to toggle source
# File lib/slather/project.rb, line 258
def unsafe_llvm_cov_export_output(binary_path)
  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 binary_path == nil
    raise StandardError, "No binary file found."
  end

  llvm_cov_args = %W(export -instr-profile #{profdata_file_arg} #{binary_path})
  if self.arch
    llvm_cov_args << "--arch" << self.arch
  end
  `xcrun llvm-cov #{llvm_cov_args.shelljoin}`
end
unsafe_profdata_llvm_cov_output(binary_path, source_files) click to toggle source
# File lib/slather/project.rb, line 282
def unsafe_profdata_llvm_cov_output(binary_path, source_files)
  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 binary_path == nil
    raise StandardError, "No binary file found."
  end

  llvm_cov_args = %W(show -instr-profile #{profdata_file_arg} #{binary_path})
  if self.arch
    llvm_cov_args << "--arch" << self.arch
  end

  # POSIX systems have an ARG_MAX for the maximum total length of the command line, so the command may fail with an error message of "Argument list too long".
  # Using the xargs command we can break the list of source_files into sublists small enough to be acceptable.
  `printf '%s\\0' #{source_files.shelljoin} | xargs -0 xcrun llvm-cov #{llvm_cov_args.shelljoin}`
end