gem/lib/earlgrey/configure_earlgrey.rb
# frozen_string_literal: true
#
# Copyright 2017 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.
require 'colored'
require 'fileutils'
require 'xcodeproj'
def configure_for_earlgrey(*_)
EarlGrey.puts_yellow <<-S
configure_for_earlgrey in post_install is now deprecated, you should safely remove it now.
EarlGrey pod will automatically set up your project and schemes.
for more information, please visit our installation guideline:
https://github.com/google/EarlGrey/blob/master/docs/install-and-run.md
S
end
module EarlGrey
XCScheme = Xcodeproj::XCScheme
EnvironmentVariable = XCScheme::EnvironmentVariable
XCSCHEME_EXT = '*.xcscheme'.freeze
ENVIRONMENT_KEY = 'DYLD_INSERT_LIBRARIES'.freeze
ENVIRONMENT_VALUE = '@executable_path/EarlGrey.framework/EarlGrey'.freeze
FRAMEWORK_SEARCH_PATHS = 'FRAMEWORK_SEARCH_PATHS'.freeze
HEADER_SEARCH_PATHS = 'HEADER_SEARCH_PATHS'.freeze
SWIFT_FILETYPE = 'sourcecode.swift'.freeze
UNITTEST_PRODUCTTYPE = 'com.apple.product-type.bundle.unit-test'.freeze
CARTHAGE_BUILD_IOS = '$(SRCROOT)/Carthage/Build/iOS'.freeze
CARTHAGE_HEADERS_IOS = '$(SRCROOT)/Carthage/Build/iOS/**'.freeze
EARLGREY_FRAMEWORK = 'EarlGrey.framework'.freeze
CARTHAGE_FRAMEWORK_PATH =
'Carthage/Build/iOS/EarlGrey.framework'.freeze
POD_FRAMEWORK_PATH =
'Pods/EarlGrey/EarlGrey/EarlGrey.framework'.freeze
class << self
attr_reader :project_name, :installer, :test_target, :test_target_name,
:scheme_file, :user_project, :carthage, :swift, :swift_version
# 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
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
# 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
def dir_path
installer ? installer.config.installation_root : Dir.pwd
end
# Strips each line in a string
# @param [String] string the string to process
# @return [String] the modified string
def strip(string)
string.split("\n").map(&:strip).join("\n")
end
# Raise error message after removing excessive spaces.
# @param [String] message the message to raise
# @return [nil]
def error(message)
raise strip(message)
end
# Prints string as magenta after stripping excess spacing
# @param [String] string the string to print
# @return [nil]
def puts_magenta(string)
puts strip(string).magenta
end
# Prints string as yellow after stripping excess spacing
# @param [String] string the string to print
# @return [nil]
def puts_yellow(string)
puts strip(string).yellow
end
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
# 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]
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
# Returns the schemes that contain the given targets
#
# @param [Xcodeproj::Project] project
# @param [Array<Xcodeproj::PBXNativeTarget>] targets
# @return [Array<Xcodeproj::XCScheme>]
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
# 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>]
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
# 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
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
# 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
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
# 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
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
# 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
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 Carthage copy phase
#
# @param [PBXNativeTarget] target
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 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
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
# Check if the target contains a swift source file
# @param [PBXNativeTarget] target
# @return [Boolean]
# rubocop:disable Style/PredicateName
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
# rubocop:enable Style/PredicateName
# 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
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
end
end