module EarlGrey

Copyright 2016 Google Inc.

Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Constants

CARTHAGE_BUILD_IOS
CARTHAGE_FRAMEWORK_PATH
CARTHAGE_HEADERS_IOS
EARLGREY_FRAMEWORK
ENVIRONMENT_KEY
ENVIRONMENT_VALUE
EnvironmentVariable
FRAMEWORK_SEARCH_PATHS
HEADER_SEARCH_PATHS
POD_FRAMEWORK_PATH
SWIFT_FILETYPE
UNITTEST_PRODUCTTYPE
VERSION
XCSCHEME_EXT
XCScheme

Attributes

carthage[R]
installer[R]
project_name[R]
scheme_file[R]
swift[R]
swift_version[R]
test_target[R]
test_target_name[R]
user_project[R]

Public Class Methods

add_carthage_copy_phase(target) click to toggle source

Add Carthage copy phase

@param [PBXNativeTarget] target

# File lib/earlgrey/configure_earlgrey.rb, line 327
def add_carthage_copy_phase(target)
  shell_script_name = 'Carthage copy-frameworks Run Script'
  target_names = target.shell_script_build_phases.map(&:name)
  unless target_names.include?(shell_script_name)
    shell_script = target.new_shell_script_build_phase shell_script_name
    shell_script.shell_path = '/bin/bash'
    shell_script.shell_script = '/usr/local/bin/carthage copy-frameworks'
    shell_script.input_paths = [CARTHAGE_FRAMEWORK_PATH]
  end
end
add_carthage_search_paths(target) click to toggle source

Updates test target's build configuration framework and header search paths for carthage. Generates a copy files build phase to embed the EarlGrey framework into the app under test.

@param [PBXNativeTarget] target @return [PBXNativeTarget] target

# File lib/earlgrey/configure_earlgrey.rb, line 310
def add_carthage_search_paths(target)
  target.build_configurations.each do |config|
    settings = config.build_settings
    settings[FRAMEWORK_SEARCH_PATHS] = Array(settings[FRAMEWORK_SEARCH_PATHS])
    unless settings[FRAMEWORK_SEARCH_PATHS].include?(CARTHAGE_BUILD_IOS)
      settings[FRAMEWORK_SEARCH_PATHS] << CARTHAGE_BUILD_IOS
    end

    settings[HEADER_SEARCH_PATHS] = Array(settings[HEADER_SEARCH_PATHS])
    settings[HEADER_SEARCH_PATHS] << CARTHAGE_HEADERS_IOS unless settings[HEADER_SEARCH_PATHS].include?(CARTHAGE_HEADERS_IOS)
  end
  target
end
add_earlgrey_copy_files_script(target, framework_ref) click to toggle source

Generates a copy files build phase to embed the EarlGrey framework into the app under test.

@param [PBXNativeTarget] target

the native target to add a copy script that copies the earlgrey
framework into its host app

@param [PBXFileReference] framework_ref

the framework reference pointing to the EarlGrey.framework
# File lib/earlgrey/configure_earlgrey.rb, line 288
def add_earlgrey_copy_files_script(target, framework_ref)
  earlgrey_copy_files_phase_name = 'EarlGrey Copy Files'
  return true if target.copy_files_build_phases.any? do |copy_files_phase|
    copy_files_phase.name == earlgrey_copy_files_phase_name
  end

  return false unless target.product_type.eql? UNITTEST_PRODUCTTYPE
  new_copy_files_phase = target.new_copy_files_build_phase(earlgrey_copy_files_phase_name)
  new_copy_files_phase.dst_path = '$(TEST_HOST)/../'
  new_copy_files_phase.dst_subfolder_spec = '0'

  build_file = new_copy_files_phase.add_file_reference framework_ref, true
  build_file.settings = { 'ATTRIBUTES' => ['CodeSignOnCopy'] }
  build_file
end
add_earlgrey_framework(target, framework_ref) click to toggle source

Add EarlGrey.framework into the build phase “Link Binary With Libraries”

@param [PBXNativeTarget] target @param [PBXFileReference] framework_ref

the framework reference pointing to the EarlGrey.framework
# File lib/earlgrey/configure_earlgrey.rb, line 343
def add_earlgrey_framework(target, framework_ref)
  linked_frameworks = target.frameworks_build_phase.files.map(&:display_name)
  target.frameworks_build_phase.add_file_reference framework_ref, true unless linked_frameworks.include? EARLGREY_FRAMEWORK
end
add_earlgrey_product(project, carthage) click to toggle source

Adds EarlGrey.framework to products group. Returns file ref.

@param [Xcodeproj::Project] project

the xcodeproject that the app is in, and EarlGrey.framework will
be added to.

@param [Boolean] carthage

if the project is carthage
# File lib/earlgrey/configure_earlgrey.rb, line 262
def add_earlgrey_product(project, carthage)
  framework_path = if carthage
                     CARTHAGE_FRAMEWORK_PATH
                   else
                     POD_FRAMEWORK_PATH
                   end

  framework_ref = project.frameworks_group.files.find do |f|
    # TODO: should have some md5 check on the actual binary
    f.path == framework_path
  end
  unless framework_ref
    framework_ref = project.frameworks_group.new_file(framework_path)
    framework_ref.source_tree = 'SOURCE_ROOT'
  end
  framework_ref
end
add_environment_variables_to_test_scheme(name, scheme) click to toggle source

Load the EarlGrey framework when the app binary is loaded by the dynamic loader, before the main() method is called.

@param [String] name @param [Xcodeproj::XCScheme] scheme

# File lib/earlgrey/configure_earlgrey.rb, line 218
    def add_environment_variables_to_test_scheme(name, scheme)
      name = File.basename(name, '.xcscheme')
      test_action = scheme.test_action
      test_variables = test_action.environment_variables

      # If any environment variables or arguments were being used in the test
      # action by being copied from the launch (run) action then copy them over
      # to the test action along with the EarlGrey environment variable.
      if test_action.should_use_launch_scheme_args_env?
        scheme.launch_action.environment_variables.all_variables.each do |var|
          test_variables.assign_variable var
        end
      end

      env_variable = test_variables[ENVIRONMENT_KEY] ||
                     EnvironmentVariable.new(key: ENVIRONMENT_KEY, value: '')
      if env_variable.value.include? ENVIRONMENT_VALUE
        puts_magenta <<-S
          DYLD_INSERT_LIBRARIES is already set up for #{name}, ignored.
        S
        return scheme
      end
      puts_magenta <<-S
        Adding EarlGrey Framework Location as an Environment Variable
        in the App Project's Test Target's Scheme Test Action #{name}.
      S

      test_action.should_use_launch_scheme_args_env = false
      env_variable.value += env_variable.value.empty? ? '' : ':'
      env_variable.value += ENVIRONMENT_VALUE
      env_variable.enabled = true
      test_variables.assign_variable env_variable
      test_action.environment_variables = test_variables

      scheme.save!
    end
configure_for_earlgrey(project_name, test_target_name, scheme_file, opts = {}) click to toggle source

Main entry point. Configures An Xcode project for use with EarlGrey.

@param [String] project_name

the xcodeproj file name

@param [String] test_target_name

the test target name contained in xcodeproj

@param [String] scheme_file

the scheme file name. defaults to project name when nil.

@return [nil]

# File lib/earlgrey/configure_earlgrey.rb, line 146
    def configure_for_earlgrey(project_name, test_target_name,
                               scheme_file, opts = {})
      set_defaults(project_name, test_target_name, scheme_file, opts)

      # Add DYLD_INSERT_LIBRARIES to the schemes
      # rubocop:disable Performance/HashEachMethods
      modify_scheme_for_actions(user_project, [test_target]).each do |_, scheme|
        scheme.save!
      end
      # rubocop:enable Performance/HashEachMethods

      # Add a Copy Files Build Phase for EarlGrey.framework to embed it into
      # the app under test.
      framework_ref = add_earlgrey_product user_project, carthage
      add_earlgrey_framework test_target, framework_ref
      add_earlgrey_copy_files_script test_target, framework_ref

      # Add header/framework search paths for carthage
      add_carthage_search_paths test_target if carthage

      # Adds EarlGrey.swift
      copy_swift_files(user_project, test_target, swift_version) if swift

      user_project.save
      puts_magenta <<-S
        EarlGrey setup complete.
        You can use the Test Target: #{test_target_name} for EarlGrey testing.
      S
    end
copy_swift_files(project, target, swift_version = nil) click to toggle source

Copies EarlGrey.swift and adds it to the project. No op if the target doesn't contain swift.

@param [Xcodeproj::Project] project @param [PBXNativeTarget] target

# File lib/earlgrey/configure_earlgrey.rb, line 364
def copy_swift_files(project, target, swift_version = nil)
  return unless has_swift?(target) || !swift_version.to_s.empty?
  project_test_targets = project.main_group.children
  test_target_group = project_test_targets.find { |g| g.display_name == target.name }

  raise "Test target group not found! #{test_target_group}" unless test_target_group

  swift_version ||= '4.0'
  src_root = File.join(__dir__, 'files')
  dst_root = test_target_group.real_path
  raise "Missing target folder #{dst_root}" unless File.exist? dst_root

  src_swift_name = 'EarlGrey.swift'
  src_swift = File.join(src_root, "Swift-#{swift_version}", src_swift_name)

  unless File.exist? src_swift
    puts_magenta "EarlGrey.swift for version #{swift_version} not found. " \
                 'Falling back to version 4.0.'
    swift_fallback = 'Swift-4.0'
    src_swift = File.join(src_root, swift_fallback, src_swift_name)
    raise "Unable to locate #{swift_fallback} file at path #{src_swift}." unless File.exist?(src_swift)
  end
  dst_swift = File.join(dst_root, src_swift_name)

  FileUtils.copy src_swift, dst_swift

  # Add files to testing target group otherwise Xcode can't read them.
  new_files = [src_swift_name]
  existing_files = test_target_group.children.map(&:display_name)

  new_files.each do |file|
    next if existing_files.include? file
    test_target_group.new_reference(file)
  end

  # Add EarlGrey.swift to sources build phase
  existing_sources = target.source_build_phase.files.map(&:display_name)
  unless existing_sources.include? src_swift_name
    target_files = test_target_group.files
    earlgrey_swift_file_ref = target_files.find { |f| f.display_name == src_swift_name }
    raise 'EarlGrey.swift not found in testing target' unless earlgrey_swift_file_ref
    target.source_build_phase.add_file_reference earlgrey_swift_file_ref, true
  end
end
dir_path() click to toggle source

Returns the project's directory. If CocoaPods hasn't had it passed in, then the current directory is chosen. @return [String] directory path for the Xcode project

# File lib/earlgrey/configure_earlgrey.rb, line 73
def dir_path
  installer ? installer.config.installation_root : Dir.pwd
end
error(message) click to toggle source

Raise error message after removing excessive spaces. @param [String] message the message to raise @return [nil]

# File lib/earlgrey/configure_earlgrey.rb, line 87
def error(message)
  raise strip(message)
end
has_swift?(target) click to toggle source

Check if the target contains a swift source file @param [PBXNativeTarget] target @return [Boolean] rubocop:disable Style/PredicateName

# File lib/earlgrey/configure_earlgrey.rb, line 352
def has_swift?(target)
  target.source_build_phase.files_references.any? do |ref|
    SWIFT_FILETYPE == (ref.last_known_file_type || ref.explicit_file_type)
  end
end
modify_scheme_for_actions(project, targets) click to toggle source

Add DYLD_INSERT_LIBRARIES to the launching environments for the test schemes to ensure that EarlGrey is correctly loaded before main() is called.

@param [Xcodeproj::Project] project @param [Array<Xcodeproj::PBXNativeTarget>] targets @return [Array<String, Xcodeproj::XCScheme>]

# File lib/earlgrey/configure_earlgrey.rb, line 204
def modify_scheme_for_actions(project, targets)
  schemes = schemes_for_native_targets(project, targets).uniq do |name, _|
    name
  end
  schemes.each do |name, scheme|
    add_environment_variables_to_test_scheme(name, scheme)
  end
end
path_for(project_name, ext) click to toggle source

Returns path to Xcode file, prepending current working dir if necessary. @param [String] project_name name of the .xcodeproj file @param [String] ext xcode file extension @return [String] path to Xcode file

# File lib/earlgrey/configure_earlgrey.rb, line 63
def path_for(project_name, ext)
  ext_match = File.extname(project_name) == ext
  return project_name if File.exist?(project_name) && ext_match
  path = File.join(dir_path, File.basename(project_name, '.*') + ext)
  path ? path : nil
end
puts_magenta(string) click to toggle source

Prints string as magenta after stripping excess spacing @param [String] string the string to print @return [nil]

# File lib/earlgrey/configure_earlgrey.rb, line 94
def puts_magenta(string)
  puts strip(string).magenta
end
puts_yellow(string) click to toggle source

Prints string as yellow after stripping excess spacing @param [String] string the string to print @return [nil]

# File lib/earlgrey/configure_earlgrey.rb, line 101
def puts_yellow(string)
  puts strip(string).yellow
end
schemes_for_native_targets(project, targets) click to toggle source

Returns the schemes that contain the given targets

@param [Xcodeproj::Project] project @param [Array<Xcodeproj::PBXNativeTarget>] targets @return [Array<Xcodeproj::XCScheme>]

# File lib/earlgrey/configure_earlgrey.rb, line 181
def schemes_for_native_targets(project, targets)
  schemes = Dir[File.join(XCScheme.shared_data_dir(project.path), XCSCHEME_EXT)] +
            Dir[File.join(XCScheme.user_data_dir(project.path), XCSCHEME_EXT)]

  schemes = schemes.map { |scheme| [scheme, Xcodeproj::XCScheme.new(scheme)] }

  targets_names = targets.map(&:name)
  schemes.select do |scheme|
    scheme[1].test_action.testables.any? do |testable|
      testable.buildable_references.any? do |buildable|
        targets_names.include? buildable.target_name
      end
    end
  end
end
set_defaults(project_name, test_target_name, scheme_file, opts = {}) click to toggle source
# File lib/earlgrey/configure_earlgrey.rb, line 105
    def set_defaults(project_name, test_target_name, scheme_file, opts = {})
      @swift = opts.fetch(:swift, false)
      @carthage = opts.fetch(:carthage, false)
      @swift_version = opts.fetch(:swift_version, '4.0')

      puts_magenta "Checking and Updating #{project_name} for EarlGrey."
      project_file = path_for project_name, '.xcodeproj'

      raise 'No test target provided' unless test_target_name

      if project_file.nil?
        error <<-E
          The target's xcodeproj file could not be found. Please check if the
          correct PROJECT_NAME is being passed in the Podfile.
          Current PROJECT_NAME is: #{project_name}
        E
      end

      @project_name = project_name
      @test_target_name = test_target_name
      @scheme_file = File.basename(scheme_file, '.*') + '.xcscheme'
      @user_project = Xcodeproj::Project.open(project_file)
      all_targets = user_project.targets
      @test_target = all_targets.find { |target| target.name == test_target_name }
      unless test_target
        error <<-E
          Unable to find target: #{test_target_name}.
          Targets are: #{all_targets.map(&:name)}
        E
      end
    end
strip(string) click to toggle source

Strips each line in a string @param [String] string the string to process @return [String] the modified string

# File lib/earlgrey/configure_earlgrey.rb, line 80
def strip(string)
  string.split("\n").map(&:strip).join("\n")
end