Module: EarlGrey

Defined in:
lib/earlgrey/version.rb,
lib/earlgrey/cli.rb,
lib/earlgrey/configure_earlgrey.rb,
lib/earlgrey/extensions/analyzer_extensions.rb,
lib/earlgrey/extensions/aggregate_target_extensions.rb

Overview

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.

Defined Under Namespace

Modules: AggregateTargetExtensions, AnalyzerExtension Classes: CLI

Constant Summary collapse

VERSION =
'1.16.0'.freeze
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 Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.carthageObject (readonly)

Returns the value of attribute carthage


56
57
58
# File 'lib/earlgrey/configure_earlgrey.rb', line 56

def carthage
  @carthage
end

.installerObject (readonly)

Returns the value of attribute installer


56
57
58
# File 'lib/earlgrey/configure_earlgrey.rb', line 56

def installer
  @installer
end

.project_nameObject (readonly)

Returns the value of attribute project_name


56
57
58
# File 'lib/earlgrey/configure_earlgrey.rb', line 56

def project_name
  @project_name
end

.scheme_fileObject (readonly)

Returns the value of attribute scheme_file


56
57
58
# File 'lib/earlgrey/configure_earlgrey.rb', line 56

def scheme_file
  @scheme_file
end

.swiftObject (readonly)

Returns the value of attribute swift


56
57
58
# File 'lib/earlgrey/configure_earlgrey.rb', line 56

def swift
  @swift
end

.swift_versionObject (readonly)

Returns the value of attribute swift_version


56
57
58
# File 'lib/earlgrey/configure_earlgrey.rb', line 56

def swift_version
  @swift_version
end

.test_targetObject (readonly)

Returns the value of attribute test_target


56
57
58
# File 'lib/earlgrey/configure_earlgrey.rb', line 56

def test_target
  @test_target
end

.test_target_nameObject (readonly)

Returns the value of attribute test_target_name


56
57
58
# File 'lib/earlgrey/configure_earlgrey.rb', line 56

def test_target_name
  @test_target_name
end

.user_projectObject (readonly)

Returns the value of attribute user_project


56
57
58
# File 'lib/earlgrey/configure_earlgrey.rb', line 56

def user_project
  @user_project
end

Class Method Details

.add_carthage_copy_phase(target) ⇒ Object

Add Carthage copy phase

Parameters:

  • target (PBXNativeTarget)

327
328
329
330
331
332
333
334
335
336
# 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) ⇒ PBXNativeTarget

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.

Parameters:

  • target (PBXNativeTarget)

Returns:

  • (PBXNativeTarget)

    target


310
311
312
313
314
315
316
317
318
319
320
321
322
# 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) ⇒ Object

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

Parameters:

  • target (PBXNativeTarget)

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

  • framework_ref (PBXFileReference)

    the framework reference pointing to the EarlGrey.framework


288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# 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) ⇒ Object

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

Parameters:

  • target (PBXNativeTarget)
  • framework_ref (PBXFileReference)

    the framework reference pointing to the EarlGrey.framework


343
344
345
346
# 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) ⇒ Object

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

Parameters:

  • project (Xcodeproj::Project)

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

  • carthage (Boolean)

    if the project is carthage


262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# 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) ⇒ Object

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

Parameters:

  • name (String)
  • scheme (Xcodeproj::XCScheme)

218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# 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 = {}) ⇒ nil

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

Parameters:

  • project_name (String)

    the xcodeproj file name

  • test_target_name (String)

    the test target name contained in xcodeproj

  • scheme_file (String)

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

Returns:

  • (nil)

146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# 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) ⇒ Object

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

Parameters:

  • project (Xcodeproj::Project)
  • target (PBXNativeTarget)

364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# 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_pathString

Returns the project's directory. If CocoaPods hasn't had it passed in, then the current directory is chosen.

Returns:

  • (String)

    directory path for the Xcode project


73
74
75
# File 'lib/earlgrey/configure_earlgrey.rb', line 73

def dir_path
  installer ? installer.config.installation_root : Dir.pwd
end

.error(message) ⇒ nil

Raise error message after removing excessive spaces.

Parameters:

  • message (String)

    the message to raise

Returns:

  • (nil)

87
88
89
# File 'lib/earlgrey/configure_earlgrey.rb', line 87

def error(message)
  raise strip(message)
end

.has_swift?(target) ⇒ Boolean

Check if the target contains a swift source file rubocop:disable Style/PredicateName

Parameters:

  • target (PBXNativeTarget)

Returns:

  • (Boolean)

352
353
354
355
356
# 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) ⇒ Array<String, Xcodeproj::XCScheme>

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

Parameters:

  • project (Xcodeproj::Project)
  • targets (Array<Xcodeproj::PBXNativeTarget>)

Returns:

  • (Array<String, Xcodeproj::XCScheme>)

204
205
206
207
208
209
210
211
# 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) ⇒ String

Returns path to Xcode file, prepending current working dir if necessary.

Parameters:

  • project_name (String)

    name of the .xcodeproj file

  • ext (String)

    xcode file extension

Returns:

  • (String)

    path to Xcode file


63
64
65
66
67
68
# 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) ⇒ nil

Prints string as magenta after stripping excess spacing

Parameters:

  • string (String)

    the string to print

Returns:

  • (nil)

94
95
96
# File 'lib/earlgrey/configure_earlgrey.rb', line 94

def puts_magenta(string)
  puts strip(string).magenta
end

.puts_yellow(string) ⇒ nil

Prints string as yellow after stripping excess spacing

Parameters:

  • string (String)

    the string to print

Returns:

  • (nil)

101
102
103
# File 'lib/earlgrey/configure_earlgrey.rb', line 101

def puts_yellow(string)
  puts strip(string).yellow
end

.schemes_for_native_targets(project, targets) ⇒ Array<Xcodeproj::XCScheme>

Returns the schemes that contain the given targets

Parameters:

  • project (Xcodeproj::Project)
  • targets (Array<Xcodeproj::PBXNativeTarget>)

Returns:

  • (Array<Xcodeproj::XCScheme>)

181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# 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 = {}) ⇒ Object


105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# 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) ⇒ String

Strips each line in a string

Parameters:

  • string (String)

    the string to process

Returns:

  • (String)

    the modified string


80
81
82
# File 'lib/earlgrey/configure_earlgrey.rb', line 80

def strip(string)
  string.split("\n").map(&:strip).join("\n")
end