fastlane-plugin-test_center 3.7.0 → 3.8.0.parallelizing.beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +71 -6
  3. data/lib/fastlane/plugin/test_center/actions/collate_html_reports.rb +2 -0
  4. data/lib/fastlane/plugin/test_center/actions/collate_json_reports.rb +2 -0
  5. data/lib/fastlane/plugin/test_center/actions/collate_junit_reports.rb +2 -1
  6. data/lib/fastlane/plugin/test_center/actions/collate_test_result_bundles.rb +2 -0
  7. data/lib/fastlane/plugin/test_center/actions/multi_scan.rb +127 -32
  8. data/lib/fastlane/plugin/test_center/actions/quit_core_simulator_service.rb +40 -0
  9. data/lib/fastlane/plugin/test_center/actions/suppress_tests.rb +2 -0
  10. data/lib/fastlane/plugin/test_center/actions/suppress_tests_from_junit.rb +3 -1
  11. data/lib/fastlane/plugin/test_center/actions/suppressed_tests.rb +2 -0
  12. data/lib/fastlane/plugin/test_center/actions/tests_from_junit.rb +2 -0
  13. data/lib/fastlane/plugin/test_center/actions/tests_from_xctestrun.rb +2 -0
  14. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/device_manager.rb +30 -0
  15. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/parallel_test_batch_worker.rb +73 -0
  16. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/report_collator.rb +129 -0
  17. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/retrying_scan.rb +77 -0
  18. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/retrying_scan_helper.rb +272 -0
  19. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/runner.rb +109 -0
  20. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/simulator_helper.rb +49 -0
  21. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/test_batch_worker.rb +24 -0
  22. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/test_batch_worker_pool.rb +133 -0
  23. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager.rb +3 -0
  24. data/lib/fastlane/plugin/test_center/helper/reportname_helper.rb +15 -6
  25. data/lib/fastlane/plugin/test_center/helper/test_collector.rb +55 -14
  26. data/lib/fastlane/plugin/test_center/helper/xctestrun_info.rb +42 -0
  27. data/lib/fastlane/plugin/test_center/version.rb +1 -1
  28. data/lib/fastlane/plugin/test_center.rb +1 -1
  29. metadata +22 -11
  30. data/lib/fastlane/plugin/test_center/helper/correcting_scan_helper.rb +0 -315
@@ -0,0 +1,129 @@
1
+ module TestCenter
2
+ module Helper
3
+ module MultiScanManager
4
+ class ReportCollator
5
+ CollateJunitReportsAction = Fastlane::Actions::CollateJunitReportsAction
6
+ CollateHtmlReportsAction = Fastlane::Actions::CollateHtmlReportsAction
7
+ CollateJsonReportsAction = Fastlane::Actions::CollateJsonReportsAction
8
+ CollateTestResultBundlesAction = Fastlane::Actions::CollateTestResultBundlesAction
9
+
10
+ def initialize(params)
11
+ @source_reports_directory_glob = params[:source_reports_directory_glob]
12
+ @output_directory = params[:output_directory]
13
+ @reportnamer = params[:reportnamer]
14
+ @scheme = params[:scheme]
15
+ @result_bundle = params[:result_bundle]
16
+ @suffix = params[:suffix] || ''
17
+ end
18
+
19
+ def collate
20
+ collate_junit_reports
21
+ collate_html_reports
22
+ collate_json_reports
23
+ collate_test_result_bundles
24
+ end
25
+
26
+ def sort_globbed_files(glob)
27
+ files = Dir.glob(glob).map do |relative_filepath|
28
+ File.absolute_path(relative_filepath)
29
+ end
30
+ files.sort! { |f1, f2| File.mtime(f1) <=> File.mtime(f2) }
31
+ end
32
+
33
+ def delete_globbed_intermediatefiles(glob)
34
+ retried_reportfiles = Dir.glob(glob)
35
+ FileUtils.rm_f(retried_reportfiles)
36
+ end
37
+
38
+ # :nocov:
39
+ def create_config(klass, options)
40
+ FastlaneCore::Configuration.create(klass.available_options, options)
41
+ end
42
+ # :nocov:
43
+
44
+ def collate_junit_reports
45
+ glob = "#{@source_reports_directory_glob}/#{@reportnamer.junit_fileglob}"
46
+ report_files = sort_globbed_files(glob)
47
+ collated_file = File.absolute_path(File.join(@output_directory, @reportnamer.junit_reportname(@suffix)))
48
+ if report_files.size > 1
49
+ config = create_config(
50
+ CollateJunitReportsAction,
51
+ {
52
+ reports: report_files,
53
+ collated_report: collated_file
54
+ }
55
+ )
56
+ CollateJunitReportsAction.run(config)
57
+ FileUtils.rm_rf(report_files - [collated_file])
58
+ elsif report_files.size == 1 && report_files.first != collated_file
59
+ FileUtils.mkdir_p(File.dirname(collated_file))
60
+ FileUtils.mv(report_files.first, collated_file)
61
+ end
62
+ end
63
+
64
+ def collate_html_reports
65
+ return unless @reportnamer.includes_html?
66
+
67
+ report_files = sort_globbed_files("#{@source_reports_directory_glob}/#{@reportnamer.html_fileglob}")
68
+ collated_file = File.absolute_path(File.join(@output_directory, @reportnamer.html_reportname(@suffix)))
69
+ if report_files.size > 1
70
+ config = create_config(
71
+ CollateJunitReportsAction,
72
+ {
73
+ reports: report_files,
74
+ collated_report: collated_file
75
+ }
76
+ )
77
+ CollateHtmlReportsAction.run(config)
78
+ FileUtils.rm_rf(report_files - [collated_file])
79
+ elsif report_files.size == 1 && report_files.first != collated_file
80
+ FileUtils.mkdir_p(File.dirname(collated_file))
81
+ FileUtils.mv(report_files.first, collated_file)
82
+ end
83
+ end
84
+
85
+ def collate_json_reports
86
+ return unless @reportnamer.includes_json?
87
+
88
+ report_files = sort_globbed_files("#{@source_reports_directory_glob}/#{@reportnamer.json_fileglob}")
89
+ collated_file = File.absolute_path(File.join(@output_directory, @reportnamer.json_reportname(@suffix)))
90
+ if report_files.size > 1
91
+ config = create_config(
92
+ CollateJsonReportsAction,
93
+ {
94
+ reports: report_files,
95
+ collated_report: collated_file
96
+ }
97
+ )
98
+ CollateJsonReportsAction.run(config)
99
+ FileUtils.rm_rf(report_files - [collated_file])
100
+ elsif report_files.size == 1 && report_files.first != collated_file
101
+ FileUtils.mkdir_p(File.dirname(collated_file))
102
+ FileUtils.mv(report_files.first, collated_file)
103
+ end
104
+ end
105
+
106
+ def collate_test_result_bundles
107
+ return unless @result_bundle
108
+
109
+ test_result_bundlepaths = sort_globbed_files("#{@source_reports_directory_glob}/#{@scheme}*.test_result")
110
+ collated_test_result_bundlepath = File.absolute_path("#{File.join(@output_directory, @scheme)}.test_result")
111
+ if test_result_bundlepaths.size > 1
112
+ config = create_config(
113
+ CollateTestResultBundlesAction,
114
+ {
115
+ bundles: test_result_bundlepaths,
116
+ collated_bundle: collated_test_result_bundlepath
117
+ }
118
+ )
119
+ CollateTestResultBundlesAction.run(config)
120
+ FileUtils.rm_rf(test_result_bundlepaths - [collated_test_result_bundlepath])
121
+ elsif test_result_bundlepaths.size == 1 && test_result_bundlepaths.first != collated_test_result_bundlepath
122
+ FileUtils.mkdir_p(File.dirname(collated_test_result_bundlepath))
123
+ FileUtils.mv(test_result_bundlepaths.first, collated_test_result_bundlepath)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,77 @@
1
+ module TestCenter
2
+ module Helper
3
+ module MultiScanManager
4
+ class RetryingScan
5
+ def initialize(options = {})
6
+ @options = options
7
+ @retrying_scan_helper = RetryingScanHelper.new(@options)
8
+ end
9
+
10
+ # :nocov:
11
+ def scan_config
12
+ Scan.config
13
+ end
14
+
15
+ def scan_cache
16
+ Scan.cache
17
+ end
18
+ # :nocov:
19
+
20
+ def prepare_scan_config_for_destination
21
+ # this allows multi_scan's `destination` option to be picked up by `scan`
22
+ scan_config._values.delete(:device)
23
+ scan_config._values.delete(:devices)
24
+ scan_cache.clear
25
+ end
26
+
27
+ def update_scan_options
28
+ valid_scan_keys = Fastlane::Actions::ScanAction.available_options.map(&:key)
29
+ scan_options = @options.select { |k,v| valid_scan_keys.include?(k) }
30
+ .merge(@retrying_scan_helper.scan_options)
31
+
32
+ prepare_scan_config_for_destination
33
+ scan_options.each do |k,v|
34
+ scan_config.set(k,v) unless v.nil?
35
+ end
36
+ end
37
+
38
+ # :nocov:
39
+ def self.run(options)
40
+ RetryingScan.new(options).run
41
+ end
42
+ # :nocov:
43
+
44
+ def run
45
+ try_count = @options[:try_count] || 1
46
+ begin
47
+ # TODO move delete_xcresults to `before_testrun`
48
+ @retrying_scan_helper.before_testrun
49
+ update_scan_options
50
+
51
+ values = scan_config.values(ask: false)
52
+ values[:xcode_path] = File.expand_path("../..", FastlaneCore::Helper.xcode_path)
53
+ FastlaneCore::PrintTable.print_values(
54
+ config: values,
55
+ hide_keys: [:destination, :slack_url],
56
+ title: "Summary for scan #{Fastlane::VERSION}"
57
+ ) unless FastlaneCore::Helper.test?
58
+
59
+ Scan::Runner.new.run
60
+ @retrying_scan_helper.after_testrun
61
+ true
62
+ rescue FastlaneCore::Interface::FastlaneTestFailure => e
63
+ FastlaneCore::UI.message("retrying_scan after test failure")
64
+ @retrying_scan_helper.after_testrun(e)
65
+ retry if @retrying_scan_helper.testrun_count < try_count
66
+ false
67
+ rescue FastlaneCore::Interface::FastlaneBuildFailure => e
68
+ FastlaneCore::UI.message("retrying_scan after build failure")
69
+ @retrying_scan_helper.after_testrun(e)
70
+ retry if @retrying_scan_helper.testrun_count < try_count
71
+ false
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,272 @@
1
+ module TestCenter
2
+ module Helper
3
+ module MultiScanManager
4
+ require_relative 'device_manager'
5
+
6
+ class RetryingScanHelper
7
+
8
+ attr_reader :testrun_count
9
+
10
+ def initialize(options)
11
+ raise ArgumentError, 'Do not use the :device or :devices option. Instead use the :destination option.' if (options.key?(:device) or options.key?(:devices))
12
+
13
+ @options = options
14
+ @testrun_count = 0
15
+ @xcpretty_json_file_output = ENV['XCPRETTY_JSON_FILE_OUTPUT']
16
+ @reportnamer = ReportNameHelper.new(
17
+ @options[:output_types],
18
+ @options[:output_files],
19
+ @options[:custom_report_file_name]
20
+ )
21
+ end
22
+
23
+ def before_testrun
24
+ remove_preexisting_test_result_bundles
25
+ delete_xcresults # has to be performed _after_ moving a *.test_result
26
+ quit_simulator
27
+ set_json_env
28
+ print_starting_scan_message
29
+ end
30
+
31
+ def quit_simulator
32
+ return unless @options[:quit_simulators]
33
+
34
+ @options.fetch(:destination).each do |destination|
35
+ if /id=(?<udid>\w+),?/ =~ destination
36
+ `xcrun simctl shutdown #{udid} 2>/dev/null`
37
+ end
38
+ end
39
+ end
40
+
41
+ def delete_xcresults
42
+ derived_data_path = File.expand_path(@options[:derived_data_path] || Scan.config[:derived_data_path])
43
+ xcresults = Dir.glob("#{derived_data_path}/Logs/Test/*.xcresult")
44
+ FastlaneCore::UI.message("Deleting xcresults: #{xcresults}")
45
+ FileUtils.rm_rf(xcresults)
46
+ end
47
+
48
+ def output_directory
49
+ absolute_output_directory = File.absolute_path(@options[:output_directory])
50
+ if @options[:batch]
51
+ testable = @options.fetch(:only_testing, ['']).first.split('/').first || ''
52
+ absolute_output_directory = File.join(absolute_output_directory, "#{testable}-batch-#{@options[:batch]}")
53
+ end
54
+ absolute_output_directory
55
+ end
56
+
57
+ def print_starting_scan_message
58
+ scan_message = "Starting scan ##{@testrun_count + 1} with #{@options.fetch(:only_testing, []).size} tests"
59
+ scan_message << " for batch ##{@options[:batch]}" unless @options[:batch].nil?
60
+ FastlaneCore::UI.message("#{scan_message}.")
61
+ end
62
+
63
+ def set_json_env
64
+ return unless @reportnamer.includes_json?
65
+
66
+ ENV['XCPRETTY_JSON_FILE_OUTPUT'] = File.join(
67
+ output_directory,
68
+ @reportnamer.json_last_reportname
69
+ )
70
+ end
71
+
72
+ def reset_json_env
73
+ return unless @reportnamer.includes_json?
74
+
75
+ ENV['XCPRETTY_JSON_FILE_OUTPUT'] = @xcpretty_json_file_output
76
+ end
77
+
78
+ def remove_preexisting_test_result_bundles
79
+ return unless @options[:result_bundle]
80
+
81
+ glob_pattern = "#{output_directory}/*.test_result"
82
+ preexisting_test_result_bundles = Dir.glob(glob_pattern)
83
+ FileUtils.rm_rf(preexisting_test_result_bundles)
84
+ end
85
+
86
+ def scan_options
87
+ valid_scan_keys = Fastlane::Actions::ScanAction.available_options.map(&:key)
88
+ xcargs = @options[:xcargs]
89
+ retrying_scan_options = @reportnamer.scan_options.merge(
90
+ {
91
+ output_directory: output_directory,
92
+ xcargs: "#{xcargs} -parallel-testing-enabled NO"
93
+ }
94
+ )
95
+ @options.select { |k,v| valid_scan_keys.include?(k) }
96
+ .merge(retrying_scan_options)
97
+ end
98
+
99
+ # after_testrun methods
100
+
101
+ def after_testrun(exception = nil)
102
+ @testrun_count = @testrun_count + 1
103
+ if exception.kind_of?(FastlaneCore::Interface::FastlaneTestFailure)
104
+ handle_test_failure
105
+ elsif exception.kind_of?(FastlaneCore::Interface::FastlaneBuildFailure)
106
+ handle_build_failure(exception)
107
+ else
108
+ handle_success
109
+ end
110
+ collate_reports
111
+ end
112
+
113
+ def handle_success
114
+ send_callback_testrun_info
115
+ move_test_result_bundle_for_next_run
116
+ reset_json_env
117
+ end
118
+
119
+ def collate_reports
120
+ report_collator_options = {
121
+ source_reports_directory_glob: output_directory,
122
+ output_directory: output_directory,
123
+ reportnamer: @reportnamer,
124
+ scheme: @options[:scheme],
125
+ result_bundle: @options[:result_bundle]
126
+ }
127
+ TestCenter::Helper::MultiScanManager::ReportCollator.new(report_collator_options).collate
128
+ end
129
+
130
+ def handle_test_failure
131
+ send_callback_testrun_info
132
+ reset_simulators
133
+ move_test_result_bundle_for_next_run
134
+ update_scan_options
135
+ @reportnamer.increment
136
+ end
137
+
138
+ def send_callback_testrun_info(additional_info = {})
139
+ return unless @options[:testrun_completed_block]
140
+
141
+ report_filepath = nil
142
+ junit_results = {}
143
+ unless additional_info.key?(:test_operation_failure)
144
+ report_filepath = File.join(output_directory, @reportnamer.junit_last_reportname)
145
+
146
+ config = FastlaneCore::Configuration.create(
147
+ Fastlane::Actions::TestsFromJunitAction.available_options,
148
+ {
149
+ junit: File.absolute_path(report_filepath)
150
+ }
151
+ )
152
+ junit_results = Fastlane::Actions::TestsFromJunitAction.run(config)
153
+ end
154
+
155
+ info = {
156
+ failed: junit_results[:failed],
157
+ passing: junit_results[:passing],
158
+ batch: @options[:batch] || 1,
159
+ try_count: @testrun_count,
160
+ report_filepath: report_filepath
161
+ }.merge(additional_info)
162
+
163
+ if @reportnamer.includes_html?
164
+ html_report_filepath = File.join(output_directory, @reportnamer.html_last_reportname)
165
+ info[:html_report_filepath] = html_report_filepath
166
+ end
167
+ if @reportnamer.includes_json?
168
+ json_report_filepath = File.join(output_directory, @reportnamer.json_last_reportname)
169
+ info[:json_report_filepath] = json_report_filepath
170
+ end
171
+ if @options[:result_bundle]
172
+ test_result_suffix = '.test_result'
173
+ test_result_suffix.prepend("-#{@reportnamer.report_count}") unless @reportnamer.report_count.zero?
174
+ test_result_bundlepath = File.join(output_directory, @options[:scheme]) + test_result_suffix
175
+ info[:test_result_bundlepath] = test_result_bundlepath
176
+ end
177
+ @options[:testrun_completed_block].call(info)
178
+ end
179
+
180
+ def update_scan_options
181
+ update_only_testing
182
+ turn_off_code_coverage
183
+ end
184
+
185
+ def turn_off_code_coverage
186
+ # Turn off code coverage as code coverage reports are not merged and
187
+ # the first, more valuable, report will be overwritten
188
+ @options.delete(:code_coverage)
189
+ end
190
+
191
+ def update_only_testing
192
+ report_filepath = File.join(output_directory, @reportnamer.junit_last_reportname)
193
+ config = FastlaneCore::Configuration.create(
194
+ Fastlane::Actions::TestsFromJunitAction.available_options,
195
+ {
196
+ junit: File.absolute_path(report_filepath)
197
+ }
198
+ )
199
+ @options[:only_testing] = Fastlane::Actions::TestsFromJunitAction.run(config)[:failed]
200
+ if @options[:invocation_based_tests]
201
+ @options[:only_testing] = @options[:only_testing].map(&:strip_testcase).uniq
202
+ end
203
+ end
204
+
205
+ def reset_simulators
206
+ return unless @options[:reset_simulators]
207
+
208
+ @options[:simulators].each(&:reset)
209
+ end
210
+
211
+ def handle_build_failure(exception)
212
+ test_operation_failure = ''
213
+
214
+ test_session_last_messages = last_lines_of_test_session_log
215
+ test_operation_failure_match = /Test operation failure: (?<test_operation_failure>.*)$/ =~ test_session_last_messages
216
+ if test_operation_failure_match.nil?
217
+ test_operation_failure = 'Unknown test operation failure'
218
+ end
219
+
220
+ case test_operation_failure
221
+ when /Test runner exited before starting test execution/
222
+ FastlaneCore::UI.error(test_operation_failure)
223
+ when /Lost connection to testmanagerd/
224
+ FastlaneCore::UI.error(test_operation_failure)
225
+ FastlaneCore::UI.important("com.apple.CoreSimulator.CoreSimulatorService may have become corrupt, consider quitting it")
226
+ if @options[:quit_core_simulator_service]
227
+ Fastlane::Actions::RestartCoreSimulatorServiceAction.run
228
+ end
229
+ else
230
+ FastlaneCore::UI.error(test_operation_failure)
231
+ send_callback_testrun_info(test_operation_failure: test_operation_failure)
232
+ raise exception
233
+ end
234
+ if @options[:reset_simulators]
235
+ @options[:simulators].each do |simulator|
236
+ simulator.reset
237
+ end
238
+ end
239
+ send_callback_testrun_info(test_operation_failure: test_operation_failure)
240
+ end
241
+
242
+ def last_lines_of_test_session_log
243
+ derived_data_path = File.expand_path(@options[:derived_data_path])
244
+ test_session_logs = Dir.glob("#{derived_data_path}/Logs/Test/*.xcresult/*_Test/Diagnostics/**/Session-*.log")
245
+ return '' if test_session_logs.empty?
246
+
247
+ test_session_logs.sort! { |logfile1, logfile2| File.mtime(logfile1) <=> File.mtime(logfile2) }
248
+ test_session = File.open(test_session_logs.last)
249
+ backwards_seek_offset = -1 * [1000, test_session.stat.size].min
250
+ test_session.seek(backwards_seek_offset, IO::SEEK_END)
251
+ test_session_last_messages = test_session.read
252
+ end
253
+
254
+ def move_test_result_bundle_for_next_run
255
+ return unless @options[:result_bundle]
256
+
257
+ glob_pattern = "#{output_directory}/*.test_result"
258
+ preexisting_test_result_bundles = Dir.glob(glob_pattern)
259
+ unnumbered_test_result_bundles = preexisting_test_result_bundles.reject do |test_result|
260
+ test_result =~ /.*-\d+\.test_result/
261
+ end
262
+ src_test_bundle = unnumbered_test_result_bundles.first
263
+ dst_test_bundle_parent_dir = File.dirname(src_test_bundle)
264
+ dst_test_bundle_basename = File.basename(src_test_bundle, '.test_result')
265
+ dst_test_bundle = "#{dst_test_bundle_parent_dir}/#{dst_test_bundle_basename}-#{@testrun_count}.test_result"
266
+ FileUtils.mkdir_p(dst_test_bundle)
267
+ FileUtils.mv(src_test_bundle, dst_test_bundle)
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end
@@ -0,0 +1,109 @@
1
+
2
+ module TestCenter
3
+ module Helper
4
+ module MultiScanManager
5
+ require 'fastlane_core/ui/ui.rb'
6
+ require 'plist'
7
+ require 'json'
8
+ require 'shellwords'
9
+ require 'snapshot/reset_simulators'
10
+
11
+ class Runner
12
+ attr_reader :retry_total_count
13
+
14
+ def initialize(multi_scan_options)
15
+ @test_collector = TestCenter::Helper::TestCollector.new(multi_scan_options)
16
+ @options = multi_scan_options.merge(
17
+ clean: false,
18
+ disable_concurrent_testing: true
19
+ )
20
+ @batch_count = @test_collector.test_batches.size
21
+ end
22
+
23
+ def output_directory
24
+ @options.fetch(:output_directory, 'test_results')
25
+ end
26
+
27
+ def run
28
+ if @options[:invocation_based_tests]
29
+ run_invocation_based_tests
30
+ else
31
+ run_test_batches
32
+ end
33
+ end
34
+
35
+ def run_invocation_based_tests
36
+ @options[:only_testing] = @options[:only_testing]&.map(&:strip_testcase)&.uniq
37
+ @options[:skip_testing] = @options[:skip_testing]&.map(&:strip_testcase)&.uniq
38
+
39
+ RetryingScan.run(@options.reject { |key| %i[device devices force_quit_simulator].include?(key) } )
40
+ end
41
+
42
+ def run_test_batches
43
+ test_batch_results = []
44
+ pool_options = @options.reject { |key| %i[device devices force_quit_simulator].include?(key) }
45
+ pool_options[:test_batch_results] = test_batch_results
46
+
47
+ pool = TestBatchWorkerPool.new(pool_options)
48
+ pool.setup_workers
49
+
50
+ remaining_test_batches = @test_collector.test_batches.clone
51
+ remaining_test_batches.each_with_index do |test_batch, current_batch_index|
52
+ worker = pool.wait_for_worker
53
+ FastlaneCore::UI.message("Starting test run #{current_batch_index}")
54
+ worker.run(scan_options_for_worker(test_batch, current_batch_index))
55
+ end
56
+ pool.wait_for_all_workers
57
+ collate_batched_reports
58
+ test_batch_results.reduce(true) { |a, t| a && t }
59
+ end
60
+
61
+ def scan_options_for_worker(test_batch, batch_index)
62
+ {
63
+ only_testing: test_batch.map(&:shellsafe_testidentifier),
64
+ output_directory: output_directory,
65
+ try_count: @options[:try_count],
66
+ batch: batch_index + 1,
67
+ xctestrun: @test_collector.xctestrun_path
68
+ }
69
+ end
70
+
71
+ def collate_batched_reports
72
+ return false unless @batch_count > 1
73
+
74
+ @test_collector.testables.each do |testable|
75
+ collate_batched_reports_for_testable(testable)
76
+ end
77
+ true
78
+ end
79
+
80
+ def collate_batched_reports_for_testable(testable)
81
+ absolute_output_directory = File.join(
82
+ File.absolute_path(output_directory),
83
+ testable
84
+ )
85
+ source_reports_directory_glob = "#{absolute_output_directory}-batch-*"
86
+
87
+ given_custom_report_file_name = @options[:custom_report_file_name]
88
+ given_output_types = @options[:output_types]
89
+ given_output_files = @options[:output_files]
90
+
91
+ TestCenter::Helper::MultiScanManager::ReportCollator.new(
92
+ source_reports_directory_glob: source_reports_directory_glob,
93
+ output_directory: absolute_output_directory,
94
+ reportnamer: ReportNameHelper.new(
95
+ given_output_types,
96
+ given_output_files,
97
+ given_custom_report_file_name
98
+ ),
99
+ scheme: @options[:scheme],
100
+ result_bundle: @options[:result_bundle]
101
+ ).collate
102
+ FileUtils.rm_rf(Dir.glob(source_reports_directory_glob))
103
+ true
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+
@@ -0,0 +1,49 @@
1
+ module TestCenter
2
+ module Helper
3
+ module MultiScanManager
4
+ class SimulatorHelper
5
+ def initialize(options)
6
+ @options = options
7
+ end
8
+
9
+ def setup
10
+ if @options[:parallel_testrun_count] > 1
11
+ delete_multi_scan_cloned_simulators
12
+ end
13
+ end
14
+
15
+ def clone_destination_simulators
16
+ cloned_simulators = []
17
+
18
+ run_count = @options[:parallel_testrun_count] || 0
19
+ destination_simulator_ids = Scan.config[:destination].map do |destination|
20
+ destination.split(',id=').last
21
+ end
22
+ original_simulators = FastlaneCore::DeviceManager.simulators('iOS').find_all do |simulator|
23
+ destination_simulator_ids.include?(simulator.udid)
24
+ end
25
+ original_simulators.each(&:shutdown)
26
+ (0...run_count).each do |batch_index|
27
+ cloned_simulators << []
28
+ original_simulators.each do |simulator|
29
+ FastlaneCore::UI.verbose("Cloning simulator")
30
+ cloned_simulator = simulator.clone
31
+ new_first_name = simulator.name.sub(/( ?\(.*| ?$)/, " Clone #{batch_index}")
32
+ new_last_name = "#{self.class.name}<#{self.object_id}>"
33
+ cloned_simulator.rename("#{new_first_name} #{new_last_name}")
34
+
35
+ cloned_simulators.last << cloned_simulator
36
+ end
37
+ end
38
+ cloned_simulators
39
+ end
40
+
41
+ def delete_multi_scan_cloned_simulators
42
+ FastlaneCore::DeviceManager.simulators('iOS').each do |simulator|
43
+ simulator.delete if /#{self.class.name}<\d+>/ =~ simulator.name
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,24 @@
1
+ module TestCenter
2
+ module Helper
3
+ module MultiScanManager
4
+ class TestBatchWorker
5
+ attr_accessor :state
6
+
7
+ def initialize(options)
8
+ @options = options
9
+ @state = :ready_to_work
10
+ end
11
+
12
+ def process_results
13
+ @state = :ready_to_work
14
+ end
15
+
16
+ def run(run_options)
17
+ self.state = :working
18
+ @options[:test_batch_results] << RetryingScan.run(@options.merge(run_options))
19
+ self.state = :ready_to_work
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end