Class: Fastlane::Actions::MultiScanAction

Inherits:
Action
  • Object
show all
Defined in:
lib/fastlane/plugin/test_center/actions/multi_scan.rb

Documentation collapse

Class Method Summary collapse

Class Method Details

.authorsObject



276
277
278
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 276

def self.authors
  ["lyndsey-ferguson/@lyndseydf"]
end

.available_optionsObject



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 251

def self.available_options
  scan_options + [
    FastlaneCore::ConfigItem.new(
      key: :try_count,
      env_name: "FL_MULTI_SCAN_TRY_COUNT",
      description: "The number of times to retry running tests via scan",
      type: Integer,
      is_string: false,
      default_value: 1
    ),
    FastlaneCore::ConfigItem.new(
      key: :batch_count,
      env_name: "FL_MULTI_SCAN_BATCH_COUNT",
      description: "The number of test batches to run through scan. Can be combined with :try_count",
      type: Integer,
      is_string: false,
      default_value: 1,
      optional: true,
      verify_block: proc do |count|
        UI.user_error!("Error: Batch counts must be greater than zero") unless count > 0
      end
    )
  ]
end

.build_for_testing(scan_options) ⇒ Object



161
162
163
164
165
166
167
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 161

def self.build_for_testing(scan_options)
  scan_options.delete(:test_without_building)
  scan_options[:build_for_testing] = true
  config = FastlaneCore::Configuration.create(Fastlane::Actions::ScanAction.available_options, scan_options)
  Fastlane::Actions::ScanAction.run(config)
  scan_options[:derived_data_path] = Scan.config[:derived_data_path]
end

.collate_reports(scan_options) ⇒ Object



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 61

def self.collate_reports(scan_options)
  extension = junit_report_fileextension(scan_options)
  report_files = Dir.glob("#{scan_options[:output_directory]}/*#{extension}").map do |relative_filepath|
    File.absolute_path(relative_filepath)
  end
  if report_files.size > 1
    r = Regexp.new("^(?<filename>.*)-(\\d+)\\.(junit|xml|html)")
    final_report_name = junit_report_filename(scan_options)
    match = r.match(final_report_name)
    if match
      final_report_name = "#{match[:filename]}#{extension}"
    end
    other_action.collate_junit_reports(
      reports: report_files.sort { |f1, f2| File.mtime(f1) <=> File.mtime(f2) },
      collated_report: File.absolute_path(File.join(scan_options[:output_directory], final_report_name))
    )
  end
  FileUtils.rm_f(Dir.glob("#{scan_options[:output_directory]}/*-[1-9]*#{extension}"))
end

.config_has_junit_report(config) ⇒ Object



169
170
171
172
173
174
175
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 169

def self.config_has_junit_report(config)
  output_types = config.fetch(:output_types, '').to_s.split(',')
  output_filenames = config.fetch(:output_files, '').to_s.split(',')

  output_type_file_count_match = output_types.size == output_filenames.size
  output_types.include?('junit') && (output_type_file_count_match || config[:custom_report_file_name].to_s.strip.length > 0)
end

.config_with_junit_report(config) ⇒ Object



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 177

def self.config_with_junit_report(config)
  return config if config_has_junit_report(config)

  if config[:output_types].to_s.strip.empty? || config[:custom_report_file_name]
    config[:custom_report_file_name] ||= 'report.xml'
    config[:output_types] = 'junit'
  elsif config[:output_types].strip == 'junit' && config[:output_files].to_s.strip.empty?
    config[:custom_report_file_name] ||= 'report.xml'
  elsif !config[:output_types].split(',').include?('junit')
    config[:output_types] << ',junit'
    config[:output_files] << ',report.xml'
  elsif config[:output_files].nil?
    config[:output_files] = config[:output_types].split(',').map { |type| "report.#{type}" }.join(',')
  end
  config
end

.descriptionObject



239
240
241
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 239

def self.description
  "Uses scan to run Xcode tests a given number of times: only re-testing failing tests."
end

.detailsObject



243
244
245
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 243

def self.details
  "Use this action to run your tests if you have fragile tests that fail sporadically."
end

.increment_report_filename(filename) ⇒ Object



211
212
213
214
215
216
217
218
219
220
221
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 211

def self.increment_report_filename(filename)
  new_report_number = 2
  basename = File.basename(filename, '.*')
  r = Regexp.new("^(?<filename>.*)-(?<report_number>\\d+)\\.(junit|xml|html)")
  match = r.match(filename)
  if match
    new_report_number = match[:report_number].to_i + 1
    basename = match[:filename]
  end
  "#{basename}-#{new_report_number}#{File.extname(filename)}"
end

.increment_report_filenames(config) ⇒ Object



223
224
225
226
227
228
229
230
231
232
233
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 223

def self.increment_report_filenames(config)
  if config[:output_files]
    output_files = config[:output_files].to_s.split(',')
    output_files = output_files.map do |output_file|
      increment_report_filename(output_file)
    end
    config[:output_files] = output_files.join(',')
  elsif config[:custom_report_file_name]
    config[:custom_report_file_name] = increment_report_filename(config[:custom_report_file_name])
  end
end

.is_supported?(platform) ⇒ Boolean

Returns:

  • (Boolean)


280
281
282
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 280

def self.is_supported?(platform)
  platform == :ios
end

.junit_report_fileextension(config) ⇒ Object



203
204
205
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 203

def self.junit_report_fileextension(config)
  File.extname(junit_report_filename(config))
end

.junit_report_filename(config) ⇒ Object



194
195
196
197
198
199
200
201
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 194

def self.junit_report_filename(config)
  report_filename = config[:custom_report_file_name]
  if report_filename.nil?
    junit_index = config[:output_types].split(',').find_index('junit')
    report_filename = config[:output_files].to_s.split(',')[junit_index]
  end
  report_filename
end

.junit_report_filepath(config) ⇒ Object



207
208
209
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 207

def self.junit_report_filepath(config)
  File.absolute_path(File.join(config[:output_directory], junit_report_filename(config)))
end

.kill(program) ⇒ Object



81
82
83
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 81

def self.kill(program)
  Actions.sh("killall -9 '#{program}' &> /dev/null || true", log: false)
end

.quit_simulatorsObject



85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 85

def self.quit_simulators
  kill('iPhone Simulator')
  kill('Simulator')
  kill('SimulatorBridge')

  launchctl_list_count = 0
  while Actions.sh('launchctl list | grep com.apple.CoreSimulator.CoreSimulatorService || true', log: false) != ''
    break if (launchctl_list_count += 1) > 10
    Actions.sh('launchctl remove com.apple.CoreSimulator.CoreSimulatorService &> /dev/null || true', log: false)
    sleep(1)
  end
end

.remove_skipped_tests(tests_to_batch, tests_to_skip) ⇒ Object



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 111

def self.remove_skipped_tests(tests_to_batch, tests_to_skip)
  if tests_to_skip.nil?
    return tests_to_batch
  end

  if tests_to_skip
    tests_to_skip = tests_to_skip
    testsuites_to_skip = tests_to_skip.select do |testsuite|
      testsuite.count('/') == 1 # just the testable/testsuite
    end
    tests_to_skip -= testsuites_to_skip
    tests_to_batch.reject! do |test_identifier|
      test_suite = test_identifier.split('/').first(2).join('/')
      testsuites_to_skip.include?(test_suite)
    end
    tests_to_batch -= tests_to_skip
  end
  tests_to_batch
end

.run(params) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 9

def self.run(params)
  scan_options = params.values.reject { |k| %i[try_count batch_count].include?(k) }

  unless Helper.test?
    FastlaneCore::PrintTable.print_values(
      config: params._values.reject { |k, v| scan_options.key?(k) },
      title: "Summary for multi_scan (test_center v#{Fastlane::TestCenter::VERSION})"
    )
  end

  scan_options = config_with_junit_report(scan_options)

  unless scan_options[:test_without_building]
    build_for_testing(scan_options)
    scan_options.delete(:build_for_testing)
    scan_options[:test_without_building] = true
  end

  batch_count = params[:batch_count]
  if batch_count == 1
    try_scan_with_retry(scan_options, params[:try_count])
  else
    tests = tests_to_batch(scan_options)
    tests.each_slice((tests.length / batch_count.to_f).round).to_a.each do |tests_batch|
      scan_options.reject! { |key| key == :skip_testing }
      scan_options[:only_testing] = tests_batch
      try_scan_with_retry(scan_options, params[:try_count])
    end
  end
  collate_reports(scan_options)
end

.scan_optionsObject



247
248
249
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 247

def self.scan_options
  ScanAction.available_options
end

.test_products_path(derived_data_path) ⇒ Object



157
158
159
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 157

def self.test_products_path(derived_data_path)
  File.join(derived_data_path, "Build", "Products")
end

.testrun_path(scan_options) ⇒ Object



153
154
155
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 153

def self.testrun_path(scan_options)
  Dir.glob("#{scan_options[:derived_data_path]}/Build/Products/#{scan_options[:scheme]}*.xctestrun").first
end

.tests_to_batch(scan_options) ⇒ Object



98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 98

def self.tests_to_batch(scan_options)
  unless scan_options[:only_testing].nil?
    return scan_options[:only_testing]
  end

  xctestrun_path = scan_options[:xctestrun] || testrun_path(scan_options)
  if xctestrun_path.nil? || !File.exist?(xctestrun_path)
    UI.user_error!("Error: cannot find expected xctestrun file '#{xctestrun_path}'")
  end
  tests = xctestrun_tests(xctestrun_path)
  remove_skipped_tests(tests, scan_options[:skip_testing])
end

.try_scan_with_retry(scan_options, maximum_try_count) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 41

def self.try_scan_with_retry(scan_options, maximum_try_count)
  try_count = 0
  begin
    try_count += 1
    config = FastlaneCore::Configuration.create(Fastlane::Actions::ScanAction.available_options, scan_options)
    Fastlane::Actions::ScanAction.run(config)
  rescue FastlaneCore::Interface::FastlaneTestFailure => e
    UI.verbose("Scan failed with #{e}")
    if try_count < maximum_try_count
      report_filepath = junit_report_filepath(scan_options)
      failed_tests = other_action.tests_from_junit(junit: report_filepath)[:failed]
      scan_options[:only_testing] = failed_tests.map(&:shellescape)
      increment_report_filenames(scan_options)
      retry
    end
  else
    increment_report_filenames(scan_options)
  end
end

.xctest_bundle_path(xctestrun_rootpath, xctestrun_config) ⇒ Object



147
148
149
150
151
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 147

def self.xctest_bundle_path(xctestrun_rootpath, xctestrun_config)
  xctest_host_path = xctestrun_config['TestHostPath'].sub('__TESTROOT__', xctestrun_rootpath)
  xctestrun_config['TestBundlePath'].sub!('__TESTHOST__', xctest_host_path)
  xctestrun_config['TestBundlePath'].sub('__TESTROOT__', xctestrun_rootpath)
end

.xctestrun_tests(xctestrun_path) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/fastlane/plugin/test_center/actions/multi_scan.rb', line 131

def self.xctestrun_tests(xctestrun_path)
  xctestrun = Plist.parse_xml(xctestrun_path)
  xctestrun_rootpath = File.dirname(xctestrun_path)
  tests = []
  xctestrun.each do |testable_name, xctestrun_config|
    test_identifiers = XCTestList.tests(xctest_bundle_path(xctestrun_rootpath, xctestrun_config))
    if xctestrun_config.key?('SkipTestIdentifiers')
      test_identifiers.reject! { |test_identifier| xctestrun_config['SkipTestIdentifiers'].include?(test_identifier) }
    end
    tests += test_identifiers.map do |test_identifier|
      "#{testable_name.shellescape}/#{test_identifier}"
    end
  end
  tests
end