fastlane-plugin-test_center 3.7.0.parallelizing.alpha.4 → 3.7.0

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 (23) hide show
  1. checksums.yaml +4 -4
  2. data/lib/fastlane/plugin/test_center.rb +1 -1
  3. data/lib/fastlane/plugin/test_center/actions/collate_test_result_bundles.rb +1 -1
  4. data/lib/fastlane/plugin/test_center/actions/multi_scan.rb +34 -89
  5. data/lib/fastlane/plugin/test_center/helper/correcting_scan_helper.rb +315 -0
  6. data/lib/fastlane/plugin/test_center/helper/reportname_helper.rb +6 -15
  7. data/lib/fastlane/plugin/test_center/helper/test_collector.rb +11 -48
  8. data/lib/fastlane/plugin/test_center/version.rb +1 -1
  9. metadata +11 -24
  10. data/lib/fastlane/plugin/test_center/actions/restart_core_simulator_service.rb +0 -38
  11. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager.rb +0 -5
  12. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/device_manager.rb +0 -30
  13. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/interstitial.rb +0 -143
  14. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/parallel_test_batch_worker.rb +0 -27
  15. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/report_collator.rb +0 -115
  16. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/retrying_scan.rb +0 -74
  17. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/retrying_scan_helper.rb +0 -255
  18. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/runner.rb +0 -356
  19. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/simulator_helper.rb +0 -49
  20. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/simulator_manager.rb +0 -317
  21. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/test_batch_worker.rb +0 -20
  22. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/test_batch_worker_pool.rb +0 -129
  23. data/lib/fastlane/plugin/test_center/helper/xctestrun_info.rb +0 -42
@@ -1,356 +0,0 @@
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
- require_relative './simulator_manager'
11
-
12
- class Runner
13
- Parallelization = TestCenter::Helper::MultiScanManager::Parallelization
14
-
15
- attr_reader :retry_total_count
16
-
17
- def initialize(multi_scan_options)
18
- @output_directory = multi_scan_options[:output_directory] || 'test_results'
19
- @try_count = multi_scan_options[:try_count]
20
- @retry_total_count = 0
21
- @testrun_completed_block = multi_scan_options[:testrun_completed_block]
22
- @given_custom_report_file_name = multi_scan_options[:custom_report_file_name]
23
- @given_output_types = multi_scan_options[:output_types]
24
- @given_output_files = multi_scan_options[:output_files]
25
- @invocation_based_tests = multi_scan_options[:invocation_based_tests]
26
- @parallelize = multi_scan_options[:parallelize]
27
- if ENV['USE_REFACTORED_PARALLELIZED_MULTI_SCAN']
28
- @test_collector = TestCenter::Helper::TestCollector.new(multi_scan_options)
29
- @scan_options = multi_scan_options
30
- else
31
- @test_collector = TestCenter::Helper::TestCollector.new(multi_scan_options)
32
- @scan_options = multi_scan_options.reject do |option, _|
33
- %i[
34
- output_directory
35
- only_testing
36
- skip_testing
37
- clean
38
- try_count
39
- batch_count
40
- custom_report_file_name
41
- fail_build
42
- testrun_completed_block
43
- output_types
44
- output_files
45
- parallelize
46
- quit_simulators
47
- invocation_based_tests
48
- ].include?(option)
49
- end
50
- end
51
- @scan_options[:clean] = false
52
- @scan_options[:disable_concurrent_testing] = true
53
- @scan_options[:xctestrun] = @test_collector.xctestrun_path
54
- @batch_count = @test_collector.test_batches.size
55
- if @parallelize
56
- @scan_options.delete(:derived_data_path)
57
- @parallelizer = Parallelization.new(@batch_count, @output_directory, @testrun_completed_block)
58
- end
59
- end
60
-
61
- def run
62
- if @invocation_based_tests
63
- run_invocation_based_tests
64
- else
65
- run_test_batches
66
- end
67
- end
68
-
69
- def run_invocation_based_tests
70
- @scan_options[:only_testing] = @scan_options[:only_testing]&.map(&:strip_testcase)&.uniq
71
- @scan_options[:skip_testing] = @scan_options[:skip_testing]&.map(&:strip_testcase)&.uniq
72
-
73
- RetryingScan.run(@scan_options)
74
- end
75
-
76
- def run_test_batches
77
- all_tests_passed = true
78
- pool_options = @scan_options.reject { |key| %i[device devices].include?(key) }
79
- pool = TestBatchWorkerPool.new(pool_options)
80
- pool.setup_workers
81
-
82
- remaining_test_batches = @test_collector.test_batches.clone
83
- remaining_test_batches.each_with_index do |test_batch, current_batch_index|
84
- worker = pool.wait_for_worker
85
-
86
- testrun_passed = worker.run(
87
- {
88
- only_testing: test_batch.map(&:shellsafe_testidentifier),
89
- output_directory: @output_directory,
90
- try_count: @try_count,
91
- batch: current_batch_index + 1
92
- }
93
- )
94
- all_tests_passed = testrun_passed && all_tests_passed
95
- end
96
- pool.wait_for_all_workers
97
- collate_batched_reports
98
- all_tests_passed
99
- end
100
-
101
- def scan
102
- return run if ENV['USE_REFACTORED_PARALLELIZED_MULTI_SCAN']
103
-
104
- all_tests_passed = true
105
- @testables_count = @test_collector.testables.size
106
- FileUtils.rm_rf(Dir.glob("#{@output_directory}/**/*.{junit,html,xml,json}"))
107
- all_tests_passed = each_batch do |test_batch, current_batch_index|
108
- output_directory = testrun_output_directory(@output_directory, test_batch, current_batch_index)
109
- reset_for_new_testable(output_directory)
110
- FastlaneCore::UI.header("Starting test run on batch '#{current_batch_index}'")
111
- @interstitial.batch = current_batch_index
112
- @interstitial.output_directory = output_directory
113
- @interstitial.before_all
114
- testrun_passed = correcting_scan(
115
- {
116
- only_testing: test_batch.map(&:shellsafe_testidentifier),
117
- output_directory: output_directory
118
- },
119
- current_batch_index,
120
- @reportnamer
121
- )
122
- all_tests_passed = testrun_passed && all_tests_passed
123
- TestCenter::Helper::MultiScanManager::ReportCollator.new(
124
- source_reports_directory_glob: output_directory,
125
- output_directory: output_directory,
126
- reportnamer: @reportnamer,
127
- scheme: @scan_options[:scheme],
128
- result_bundle: @scan_options[:result_bundle]
129
- ).collate
130
- testrun_passed && all_tests_passed
131
- end
132
- all_tests_passed
133
- end
134
-
135
- def run_test_batch_through_retrying_scan(test_batch, current_batch_index)
136
- FastlaneCore::UI.important("Investigate using rbspy for perf problems: https://github.com/rbspy/rbspy")
137
- retrying_scan = TestCenter::Helper::MultiScanManager::RetryingScan.new(
138
- @scan_options.merge(
139
- only_testing: test_batch.map(&:shellsafe_testidentifier),
140
- output_directory: @output_directory,
141
- try_count: @try_count,
142
- batch: current_batch_index + 1
143
- ).reject { |key| %i[device devices].include?(key) }
144
- )
145
- retrying_scan.run
146
- end
147
-
148
- def collate_batched_reports
149
- return false unless @batch_count > 1
150
-
151
- @test_collector.testables.each do |testable|
152
- collate_batched_reports_for_testable(testable)
153
- end
154
- true
155
- end
156
-
157
- def collate_batched_reports_for_testable(testable)
158
- absolute_output_directory = File.join(
159
- File.absolute_path(@output_directory),
160
- testable
161
- )
162
- source_reports_directory_glob = "#{absolute_output_directory}-batch-*"
163
-
164
- TestCenter::Helper::MultiScanManager::ReportCollator.new(
165
- source_reports_directory_glob: source_reports_directory_glob,
166
- output_directory: absolute_output_directory,
167
- reportnamer: @reportnamer = ReportNameHelper.new(
168
- @given_output_types,
169
- @given_output_files,
170
- @given_custom_report_file_name
171
- ),
172
- scheme: @scan_options[:scheme],
173
- result_bundle: @scan_options[:result_bundle]
174
- ).collate
175
- FileUtils.rm_rf(Dir.glob(source_reports_directory_glob))
176
- true
177
- end
178
-
179
- def each_batch
180
- tests_passed = true
181
- if @parallelize
182
- xctestrun_filename = File.basename(@test_collector.xctestrun_path)
183
- xcproduct_dirpath = File.dirname(@test_collector.xctestrun_path)
184
- tmp_xcproduct_dirpath = Dir.mktmpdir
185
-
186
- FileUtils.copy_entry(xcproduct_dirpath, tmp_xcproduct_dirpath)
187
-
188
- tmp_xctestrun_path = File.join(tmp_xcproduct_dirpath, xctestrun_filename)
189
- app_infoplist = XCTestrunInfo.new(tmp_xctestrun_path)
190
- @scan_options[:xctestrun] = tmp_xctestrun_path
191
- batch_deploymentversions = @test_collector.test_batches.map do |test_batch|
192
- testable = test_batch.first.split('/').first.gsub('\\', '')
193
- # TODO: investigate the reason for this call that doesn't seem to do
194
- # anything other than query for and then discard MinimumOSVersion
195
- app_infoplist.app_plist_for_testable(testable)['MinimumOSVersion']
196
- end
197
- @parallelizer.setup_simulators(@scan_options[:devices] || Array(@scan_options[:device]), batch_deploymentversions)
198
- @parallelizer.setup_pipes_for_fork
199
- @test_collector.test_batches.each_with_index do |test_batch, current_batch_index|
200
- fork do
201
- @parallelizer.connect_subprocess_endpoint(current_batch_index)
202
- begin
203
- @parallelizer.setup_scan_options_for_testrun(@scan_options, current_batch_index)
204
- # add output_directory to map of test-target: [ output_directories ]
205
- tests_passed = yield(test_batch, current_batch_index)
206
- ensure
207
- @parallelizer.send_subprocess_result(current_batch_index, tests_passed)
208
- end
209
- # processes to disconnect from the Simulator subsystems
210
- FastlaneCore::UI.message("batched scan #{current_batch_index} finishing")
211
- end
212
- end
213
- # @parallelizer.wait_for_subprocesses
214
- # tests_passed = @parallelizer.handle_subprocesses_results && tests_passed
215
- @parallelizer.handle_subprocesses
216
- @parallelizer.cleanup_simulators
217
- @test_collector.testables.each do |testable|
218
- # ReportCollator with a testable-batch glob pattern
219
- source_reports_directory_glob = batched_testable_output_directory(@output_directory, '*', testable)
220
- @reportnamer = ReportNameHelper.new(
221
- @given_output_types,
222
- @given_output_files,
223
- @given_custom_report_file_name
224
- )
225
- TestCenter::Helper::MultiScanManager::ReportCollator.new(
226
- source_reports_directory_glob: source_reports_directory_glob,
227
- output_directory: @output_directory,
228
- reportnamer: @reportnamer,
229
- scheme: @scan_options[:scheme],
230
- result_bundle: @scan_options[:result_bundle],
231
- suffix: testable
232
- ).collate
233
- FileUtils.rm_rf(Dir.glob(source_reports_directory_glob))
234
- end
235
- # for each key in test-target : [ output_directories ], call
236
- # collate_junit_reports for each key, the [ output_directories ] being
237
- # used to find the report files.
238
- # the resultant report file is to be placed in the originally requested
239
- # output_directory, with the name changed to include a suffix matching
240
- # the test target's name
241
- else
242
- @test_collector.test_batches.each_with_index do |test_batch, current_batch_index|
243
- tests_passed = yield(test_batch, current_batch_index)
244
- end
245
- end
246
- tests_passed
247
- end
248
-
249
- def batched_testable_output_directory(output_directory, batch_index, testable_name)
250
- File.join(output_directory, "results-#{testable_name}-batch-#{batch_index}")
251
- end
252
-
253
- def testrun_output_directory(base_output_directory, test_batch, batch_index)
254
- return base_output_directory if @batch_count == 1
255
-
256
- testable_name = test_batch.first.split('/').first.gsub(/\\/, '')
257
- batched_testable_output_directory(base_output_directory, batch_index, testable_name)
258
- end
259
-
260
- def reset_reportnamer
261
- @reportnamer = ReportNameHelper.new(
262
- @given_output_types,
263
- @given_output_files,
264
- @given_custom_report_file_name
265
- )
266
- end
267
-
268
- def test_run_completed_callback
269
- if @parallelize && @testrun_completed_block
270
- Proc.new do |info|
271
- puts "about to call @parallelizer.send_subprocess_tryinfo(#{info})"
272
- @parallelizer.send_subprocess_tryinfo(info)
273
- end
274
- else
275
- @testrun_completed_block
276
- end
277
- end
278
-
279
- def reset_interstitial(output_directory)
280
- @interstitial = TestCenter::Helper::MultiScanManager::Interstitial.new(
281
- @scan_options.merge(
282
- {
283
- output_directory: output_directory,
284
- reportnamer: @reportnamer,
285
- testrun_completed_block: @testrun_completed_block,
286
- parallelize: @parallelize
287
- }
288
- )
289
- )
290
- end
291
-
292
- def reset_for_new_testable(output_directory)
293
- reset_reportnamer
294
- reset_interstitial(output_directory)
295
- end
296
-
297
- def correcting_scan(scan_run_options, batch, reportnamer)
298
- scan_options = @scan_options.merge(scan_run_options)
299
- try_count = 0
300
- tests_passed = true
301
- begin
302
- try_count += 1
303
- config = FastlaneCore::Configuration.create(
304
- Fastlane::Actions::ScanAction.available_options,
305
- scan_options.merge(reportnamer.scan_options)
306
- )
307
- Fastlane::Actions::ScanAction.run(config)
308
- @interstitial.finish_try(try_count)
309
- tests_passed = true
310
- rescue FastlaneCore::Interface::FastlaneTestFailure => e
311
- FastlaneCore::UI.verbose("Scan failed with #{e}")
312
- if try_count < @try_count
313
- @retry_total_count += 1
314
- scan_options.delete(:code_coverage)
315
- tests_to_retry = failed_tests(reportnamer, scan_options[:output_directory])
316
-
317
- scan_options[:only_testing] = tests_to_retry.map(&:shellsafe_testidentifier)
318
- FastlaneCore::UI.message('Re-running scan on only failed tests')
319
- @interstitial.finish_try(try_count)
320
- retry
321
- end
322
- tests_passed = false
323
- end
324
- tests_passed
325
- end
326
-
327
- def failed_tests(reportnamer, output_directory)
328
- report_filepath = File.join(output_directory, reportnamer.junit_last_reportname)
329
- config = FastlaneCore::Configuration.create(
330
- Fastlane::Actions::TestsFromJunitAction.available_options,
331
- {
332
- junit: File.absolute_path(report_filepath)
333
- }
334
- )
335
- Fastlane::Actions::TestsFromJunitAction.run(config)[:failed]
336
- end
337
- end
338
- end
339
- end
340
- end
341
-
342
-
343
- module FastlaneCore
344
- class Shell < Interface
345
- def format_string(datetime = Time.now, severity = "")
346
- prefix = $batch_index.nil? ? '' : "#{$batch_index}: "
347
- if FastlaneCore::Globals.verbose?
348
- return "#{prefix}#{severity} [#{datetime.strftime('%Y-%m-%d %H:%M:%S.%2N')}]: "
349
- elsif FastlaneCore::Env.truthy?("FASTLANE_HIDE_TIMESTAMP")
350
- return prefix
351
- else
352
- return "#{prefix}[#{datetime.strftime('%H:%M:%S')}]: "
353
- end
354
- end
355
- end
356
- end
@@ -1,49 +0,0 @@
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[:batch_count] || @options[:parallel_simulator_fork_count] || 1) > 1
11
- delete_multi_scan_cloned_simulators
12
- end
13
- end
14
-
15
- def clone_destination_simulators
16
- cloned_simulators = []
17
-
18
- batch_count = @options[:batch_count] || @options[:parallel_simulator_fork_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...batch_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
@@ -1,317 +0,0 @@
1
- module TestCenter
2
- module Helper
3
- module MultiScanManager
4
- require 'scan'
5
- require 'colorize'
6
- require_relative './device_manager'
7
-
8
- class Parallelization
9
- def initialize(batch_count, output_directory, testrun_completed_block)
10
- @batch_count = batch_count
11
- @output_directory = output_directory
12
- @testrun_completed_block = testrun_completed_block
13
-
14
- @simulators ||= []
15
-
16
- if ENV['USE_REFACTORED_PARALLELIZED_MULTI_SCAN']
17
- @simhelper = SimulatorHelper.new(
18
- parallelize: true,
19
- batch_count: batch_count
20
- )
21
- @simhelper.setup
22
- end
23
- if @batch_count < 1
24
- raise FastlaneCore::FastlaneCrash.new({}), "batch_count (#{@batch_count}) < 1, this should never happen"
25
- end
26
- ObjectSpace.define_finalizer(self, self.class.finalize)
27
- end
28
-
29
- def self.finalize
30
- proc { cleanup_simulators }
31
- end
32
-
33
- def setup_simulators(devices, batch_deploymentversions)
34
- if ENV['USE_REFACTORED_PARALLELIZED_MULTI_SCAN']
35
- @simulators = @simhelper.clone_destination_simulators
36
- else
37
- FastlaneCore::DeviceManager.simulators('iOS').each do |simulator|
38
- simulator.delete if /-batchclone-/ =~ simulator.name
39
- end
40
-
41
- (0...@batch_count).each do |batch_index|
42
- found_simulator_devices = []
43
- if devices.count > 0
44
- found_simulator_devices = detect_simulator(devices, batch_deploymentversions[batch_index])
45
- else
46
- found_simulator_devices = Scan::DetectValues.detect_simulator(devices, 'iOS', 'IPHONEOS_DEPLOYMENT_TARGET', 'iPhone 5s', nil)
47
- end
48
- @simulators[batch_index] ||= []
49
- found_simulator_devices.each do |found_simulator_device|
50
- device_for_batch = found_simulator_device.clone
51
- new_name = "#{found_simulator_device.name.gsub(/[^a-zA-Z\d]/,'')}-batchclone-#{batch_index + 1}"
52
- device_for_batch.rename(new_name)
53
- device_for_batch.boot
54
- @simulators[batch_index] << device_for_batch
55
- end
56
- end
57
- end
58
- end
59
-
60
- def detect_simulator(devices, deployment_target_version)
61
- require 'set'
62
-
63
- simulators = Scan::DetectValues.filter_simulators(
64
- FastlaneCore::DeviceManager.simulators('iOS').tap do |array|
65
- if array.empty?
66
- FastlaneCore::UI.user_error!(['No', simulator_type_descriptor, 'simulators found on local machine'].reject(&:nil?).join(' '))
67
- end
68
- end,
69
- :greater_than_or_equal,
70
- deployment_target_version
71
- ).tap do |sims|
72
- if sims.empty?
73
- FastlaneCore::UI.error("No simulators found that are greater than or equal to the version of deployment target (#{deployment_target_version})")
74
- end
75
- end
76
-
77
- # At this point we have all simulators for the given deployment target (or higher)
78
-
79
- # We create 2 lambdas, which we iterate over later on
80
- # If the first lambda `matches` found a simulator to use
81
- # we'll never call the second one
82
-
83
- matches = lambda do
84
- set_of_simulators = devices.inject(
85
- Set.new # of simulators
86
- ) do |set, device_string|
87
- pieces = device_string.split(/\s(?=\([\d\.]+\)$)/)
88
-
89
- selector = ->(sim) { pieces.count > 0 && sim.name == pieces.first }
90
-
91
- set + (
92
- if pieces.count == 0
93
- [] # empty array
94
- elsif pieces.count == 1
95
- simulators
96
- .select(&selector)
97
- .reverse # more efficient, because `simctl` prints higher versions first
98
- .sort_by! { |sim| Gem::Version.new(sim.os_version) }
99
- .pop(1)
100
- else # pieces.count == 2 -- mathematically, because of the 'end of line' part of our regular expression
101
- version = pieces[1].tr('()', '')
102
- potential_emptiness_error = lambda do |sims|
103
- if sims.empty?
104
- FastlaneCore::UI.error("No simulators found that are equal to the version " \
105
- "of specifier (#{version}) and greater than or equal to the version " \
106
- "of deployment target (#{deployment_target_version})")
107
- end
108
- end
109
- Scan::DetectValues.filter_simulators(simulators, :equal, version).tap(&potential_emptiness_error).select(&selector)
110
- end
111
- ).tap do |array|
112
- FastlaneCore::UI.error("Ignoring '#{device_string}', couldn't find matching simulator") if array.empty?
113
- end
114
- end
115
-
116
- set_of_simulators.to_a
117
- end
118
-
119
- default = lambda do
120
- FastlaneCore::UI.error("Couldn't find any matching simulators for '#{devices}' - falling back to default simulator") if (devices || []).count > 0
121
-
122
- result = Array(
123
- simulators
124
- .select { |sim| sim.name == default_device_name }
125
- .reverse # more efficient, because `simctl` prints higher versions first
126
- .sort_by! { |sim| Gem::Version.new(sim.os_version) }
127
- .last || simulators.first
128
- )
129
-
130
- FastlaneCore::UI.message("Found simulator \"#{result.first.name} (#{result.first.os_version})\"") if result.first
131
-
132
- result
133
- end
134
-
135
- [matches, default].lazy.map { |x|
136
- arr = x.call
137
- arr unless arr.empty?
138
- }.reject(&:nil?).first
139
- end
140
-
141
- def cleanup_simulators
142
- @simulators.flatten.each(&:delete)
143
- @simulators = []
144
- end
145
-
146
- def destination_for_batch(batch_index)
147
- @simulators[batch_index]
148
- end
149
-
150
- def devices(batch_index)
151
- if batch_index > @batch_count
152
- simulator_count = [@batch_count, @simulators.count].max
153
- raise "Error: impossible to request devices for batch #{batch_index}, there are only #{simulator_count} set(s) of simulators"
154
- end
155
-
156
- if @simulators.count > 0
157
- @simulators[batch_index - 1].map do |simulator|
158
- "#{simulator.name} (#{simulator.os_version})"
159
- end
160
- else
161
- @scan_options[:devices] || Array(@scan_options[:device])
162
- end
163
- end
164
-
165
- def ensure_conflict_free_scanlogging(scan_options, batch_index)
166
- scan_options[:buildlog_path] = scan_options[:buildlog_path] + "-#{batch_index}"
167
- end
168
-
169
- def ensure_devices_cloned_for_testrun_are_used(scan_options, batch_index)
170
- scan_options.delete(:device)
171
- scan_options[:devices] = devices(batch_index)
172
- end
173
-
174
- def setup_scan_options_for_testrun(scan_options, batch_index)
175
- ensure_conflict_free_scanlogging(scan_options, batch_index)
176
- ensure_devices_cloned_for_testrun_are_used(scan_options, batch_index)
177
- end
178
-
179
- def setup_pipes_for_fork
180
- @pipe_endpoints = []
181
- (0...@batch_count).each do
182
- @pipe_endpoints << IO.pipe
183
- end
184
- end
185
-
186
- def connect_subprocess_endpoint(batch_index)
187
- mainprocess_reader, = @pipe_endpoints[batch_index]
188
- mainprocess_reader.close # we are now in the subprocess
189
- FileUtils.mkdir_p(@output_directory)
190
- subprocess_output_dir = Dir.mktmpdir
191
- subprocess_logfilepath = File.join(subprocess_output_dir, "batchscan_#{batch_index}.log")
192
- $subprocess_logfile = File.open(subprocess_logfilepath, 'w')
193
- $subprocess_logfile.sync = true
194
- $old_stdout = $stdout.dup
195
- $old_stderr = $stderr.dup
196
- $stdout.reopen($subprocess_logfile)
197
- $stderr.reopen($subprocess_logfile)
198
- end
199
-
200
- def disconnect_subprocess_endpoints
201
- # This is done from the parent process to close the pipe from its end so
202
- # that its reading of the pipe doesn't block waiting for more IO on the
203
- # writer.
204
- # This has to be done after the fork, because we don't want the subprocess
205
- # to receive its endpoint already closed.
206
- @pipe_endpoints.each { |_, subprocess_writer| subprocess_writer.close }
207
- end
208
-
209
- def send_subprocess_tryinfo(info)
210
- puts "in send_subprocess_tryinfo"
211
- _, subprocess_writer = @pipe_endpoints[batch_index]
212
- subprocess_output = {
213
- 'message_type' => 'tryinfo',
214
- 'batch_index' => batch_index,
215
- 'tryinfo' => info
216
- }
217
- subprocess_writer.puts subprocess_output.to_json
218
- end
219
-
220
- def send_subprocess_result(batch_index, result)
221
- $stdout = $old_stdout.dup
222
- $stderr = $old_stderr.dup
223
- _, subprocess_writer = @pipe_endpoints[batch_index]
224
-
225
- subprocess_output = {
226
- 'message_type' => 'completed',
227
- 'batch_index' => batch_index,
228
- 'subprocess_logfilepath' => $subprocess_logfile.path,
229
- 'tests_passed' => result
230
- }
231
- subprocess_writer.puts subprocess_output.to_json
232
- subprocess_writer.close
233
- $subprocess_logfile.close
234
- end
235
-
236
- def parse_subprocess_results(subprocess_index, subprocess_output)
237
- subprocess_result = {
238
- 'tests_passed' => false
239
- }
240
- if subprocess_output.empty?
241
- FastlaneCore::UI.error("Something went terribly wrong: no output from parallelized batch #{subprocess_index}!")
242
- else
243
- subprocess_result = JSON.parse(subprocess_output)
244
- end
245
- subprocess_result
246
- end
247
-
248
- def stream_subprocess_result_to_console(index, subprocess_logfilepath)
249
- puts '-' * 80
250
- if File.exist?(subprocess_logfilepath)
251
- simulator_prefix = "[Sim-#{index}]"
252
- unless ENV['FASTLANE_DISABLE_COLORS']
253
- colors = String.colors - [:default, :black, :white]
254
- color = colors.sample
255
- simulator_prefix = simulator_prefix.colorize(color)
256
- end
257
- File.foreach(subprocess_logfilepath) do |line|
258
- print simulator_prefix, line
259
- end
260
- end
261
- end
262
-
263
- def handle_subprocesses
264
- # disconnect_subprocess_endpoints # to ensure no blocking on pipe
265
- FastlaneCore::Helper.show_loading_indicator("Scanning in #{@batch_count} batches")
266
- loop do
267
- break if @pipe_endpoints.size.zero?
268
-
269
- remaining_pipe_endpoints = @pipe_endpoints.map(&:first).reject { |pe| pe.closed? }
270
- puts "there are #{remaining_pipe_endpoints.size} endpoints remaining".yellow
271
- read_array, _, error_array = IO.select(remaining_pipe_endpoints)
272
- read_array.each do |pipe_reader|
273
- endpoint_index = @pipe_endpoints.find_index { |pe| pe.first == pipe_reader }
274
- child_message_size = pipe_reader.stat.size
275
- child_message = pipe_reader.read_nonblock(child_message_size)
276
- subprocess_result = parse_subprocess_results(0, child_message)
277
- if subprocess_result['message_type'] == 'completed'
278
- stream_subprocess_result_to_console(subprocess_result['batch_index'], subprocess_result['subprocess_logfilepath'])
279
- pipe_reader.close
280
- @pipe_endpoints.delete_at(endpoint_index)
281
- elsif subprocess_result['message_type'] == 'tryinfo'
282
- puts "in simulator_manager, about to call @testrun_completed_block witth #{subprocess_result['tryinfo']}"
283
- @testrun_completed_block && @testrun_completed_block.call(subprocess_result['tryinfo'])
284
- end
285
- end
286
- error_array.each { |e| puts e }
287
- break if error_array.size > 0
288
- break if read_array.size.zero?
289
- end
290
- FastlaneCore::Helper.hide_loading_indicator
291
- end
292
-
293
- def wait_for_subprocesses
294
- disconnect_subprocess_endpoints # to ensure no blocking on the pipe
295
- FastlaneCore::Helper.show_loading_indicator("Scanning in #{@batch_count} batches")
296
- Process.waitall
297
- FastlaneCore::Helper.hide_loading_indicator
298
- end
299
-
300
- def handle_subprocesses_results
301
- tests_passed = false
302
- FastlaneCore::UI.header("Output from parallelized batch run")
303
- @pipe_endpoints.each_with_index do |endpoints, index|
304
- mainprocess_reader, = endpoints
305
- subprocess_result = parse_subprocess_results(index, mainprocess_reader.read)
306
- mainprocess_reader.close
307
- stream_subprocess_result_to_console(index, subprocess_result['subprocess_logfilepath'])
308
- tests_passed = subprocess_result['tests_passed']
309
- end
310
- puts '=' * 80
311
- tests_passed
312
- true
313
- end
314
- end
315
- end
316
- end
317
- end