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

Sign up to get free protection for your applications and to get access to all the features.
Files changed (26) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +4 -0
  3. data/lib/fastlane/plugin/test_center.rb +1 -1
  4. data/lib/fastlane/plugin/test_center/actions/collate_test_result_bundles.rb +1 -1
  5. data/lib/fastlane/plugin/test_center/actions/multi_scan.rb +109 -25
  6. data/lib/fastlane/plugin/test_center/actions/restart_core_simulator_service.rb +38 -0
  7. data/lib/fastlane/plugin/test_center/actions/tests_from_xctestrun.rb +16 -4
  8. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager.rb +5 -0
  9. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/device_manager.rb +30 -0
  10. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/interstitial.rb +143 -0
  11. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/parallel_test_batch_worker.rb +27 -0
  12. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/report_collator.rb +115 -0
  13. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/retrying_scan.rb +74 -0
  14. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/retrying_scan_helper.rb +255 -0
  15. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/runner.rb +356 -0
  16. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/simulator_helper.rb +49 -0
  17. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/simulator_manager.rb +317 -0
  18. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/test_batch_worker.rb +20 -0
  19. data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/test_batch_worker_pool.rb +129 -0
  20. data/lib/fastlane/plugin/test_center/helper/reportname_helper.rb +15 -6
  21. data/lib/fastlane/plugin/test_center/helper/test_collector.rb +49 -3
  22. data/lib/fastlane/plugin/test_center/helper/xcodebuild_string.rb +4 -0
  23. data/lib/fastlane/plugin/test_center/helper/xctestrun_info.rb +42 -0
  24. data/lib/fastlane/plugin/test_center/version.rb +1 -1
  25. metadata +38 -10
  26. data/lib/fastlane/plugin/test_center/helper/correcting_scan_helper.rb +0 -293
@@ -0,0 +1,27 @@
1
+ module TestCenter
2
+ module Helper
3
+ module MultiScanManager
4
+ class ParallelTestBatchWorker < TestBatchWorker
5
+ attr_reader :pid
6
+
7
+ def state=(new_state)
8
+ super(new_state)
9
+ @pid = nil unless new_state == :working
10
+ end
11
+
12
+ def run(run_options)
13
+ self.state = :working
14
+ @pid = Process.fork do
15
+ begin
16
+ super(run_options)
17
+ ensure
18
+ exit!
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+
@@ -0,0 +1,115 @@
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
+ def create_config(klass, options)
39
+ FastlaneCore::Configuration.create(klass.available_options, options)
40
+ end
41
+
42
+ def collate_junit_reports
43
+ glob = "#{@source_reports_directory_glob}/#{@reportnamer.junit_fileglob}"
44
+ report_files = sort_globbed_files(glob)
45
+ if report_files.size > 1
46
+ collated_file = File.absolute_path(File.join(@output_directory, @reportnamer.junit_reportname(@suffix)))
47
+ config = create_config(
48
+ CollateJunitReportsAction,
49
+ {
50
+ reports: report_files,
51
+ collated_report: collated_file
52
+ }
53
+ )
54
+ CollateJunitReportsAction.run(config)
55
+ FileUtils.rm_rf(report_files - [collated_file])
56
+ end
57
+ end
58
+
59
+ def collate_html_reports
60
+ return unless @reportnamer.includes_html?
61
+
62
+ report_files = sort_globbed_files("#{@source_reports_directory_glob}/#{@reportnamer.html_fileglob}")
63
+ if report_files.size > 1
64
+ collated_file = File.absolute_path(File.join(@output_directory, @reportnamer.html_reportname(@suffix)))
65
+ config = create_config(
66
+ CollateJunitReportsAction,
67
+ {
68
+ reports: report_files,
69
+ collated_report: collated_file
70
+ }
71
+ )
72
+ CollateHtmlReportsAction.run(config)
73
+ FileUtils.rm_rf(report_files - [collated_file])
74
+ end
75
+ end
76
+
77
+ def collate_json_reports
78
+ return unless @reportnamer.includes_json?
79
+
80
+ report_files = sort_globbed_files("#{@source_reports_directory_glob}/#{@reportnamer.json_fileglob}")
81
+ if report_files.size > 1
82
+ collated_file = File.absolute_path(File.join(@output_directory, @reportnamer.json_reportname(@suffix)))
83
+ config = create_config(
84
+ CollateJsonReportsAction,
85
+ {
86
+ reports: report_files,
87
+ collated_report: collated_file
88
+ }
89
+ )
90
+ CollateJsonReportsAction.run(config)
91
+ FileUtils.rm_rf(report_files - [collated_file])
92
+ end
93
+ end
94
+
95
+ def collate_test_result_bundles
96
+ return unless @result_bundle
97
+
98
+ test_result_bundlepaths = sort_globbed_files("#{@source_reports_directory_glob}/#{@scheme}*.test_result")
99
+ if test_result_bundlepaths.size > 1
100
+ collated_test_result_bundlepath = File.absolute_path("#{File.join(@output_directory, @scheme)}.test_result")
101
+ config = create_config(
102
+ CollateTestResultBundlesAction,
103
+ {
104
+ bundles: test_result_bundlepaths,
105
+ collated_bundle: collated_test_result_bundlepath
106
+ }
107
+ )
108
+ CollateTestResultBundlesAction.run(config)
109
+ FileUtils.rm_rf(test_result_bundlepaths - [collated_test_result_bundlepath])
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,74 @@
1
+ module TestCenter
2
+ module Helper
3
+ module MultiScanManager
4
+
5
+ class RetryingScan
6
+ def initialize(options = {})
7
+ @options = options
8
+ @retrying_scan_helper = RetryingScanHelper.new(options)
9
+ end
10
+
11
+ def scan_config
12
+ Scan.config
13
+ end
14
+
15
+ def scan_cache
16
+ Scan.cache
17
+ end
18
+
19
+ def prepare_scan_config_for_destination
20
+ # this allows multi_scan's `destination` option to be picked up by `scan`
21
+ scan_config._values.delete(:device)
22
+ scan_config._values.delete(:devices)
23
+ scan_cache.clear
24
+ end
25
+
26
+ def update_scan_options
27
+ valid_scan_keys = Fastlane::Actions::ScanAction.available_options.map(&:key)
28
+ scan_options = @options.select { |k,v| valid_scan_keys.include?(k) }
29
+ .merge(@retrying_scan_helper.scan_options)
30
+
31
+ prepare_scan_config_for_destination
32
+ scan_options.each do |k,v|
33
+ scan_config.set(k,v) unless v.nil?
34
+ end
35
+ end
36
+
37
+ def self.run(options)
38
+ RetryingScan.new(options).run
39
+ end
40
+
41
+ def run
42
+ try_count = @options[:try_count] || 1
43
+ begin
44
+ # TODO move delete_xcresults to `before_testrun`
45
+ @retrying_scan_helper.before_testrun
46
+ update_scan_options
47
+
48
+ values = scan_config.values(ask: false)
49
+ values[:xcode_path] = File.expand_path("../..", FastlaneCore::Helper.xcode_path)
50
+ FastlaneCore::PrintTable.print_values(
51
+ config: values,
52
+ hide_keys: [:destination, :slack_url],
53
+ title: "Summary for scan #{Fastlane::VERSION}"
54
+ ) unless FastlaneCore::Helper.test?
55
+
56
+ Scan::Runner.new.run
57
+ @retrying_scan_helper.after_testrun
58
+ true
59
+ rescue FastlaneCore::Interface::FastlaneTestFailure => e
60
+ FastlaneCore::UI.message("retrying_scan after test failure")
61
+ @retrying_scan_helper.after_testrun(e)
62
+ retry if @retrying_scan_helper.testrun_count < try_count
63
+ false
64
+ rescue FastlaneCore::Interface::FastlaneBuildFailure => e
65
+ FastlaneCore::UI.message("retrying_scan after build failure")
66
+ @retrying_scan_helper.after_testrun(e)
67
+ retry if @retrying_scan_helper.testrun_count < try_count
68
+ false
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,255 @@
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
+
27
+ set_json_env
28
+ print_starting_scan_message
29
+ end
30
+
31
+ def delete_xcresults
32
+ derived_data_path = File.expand_path(@options[:derived_data_path] || Scan.config[:derived_data_path])
33
+ xcresults = Dir.glob("#{derived_data_path}/Logs/Test/*.xcresult")
34
+ FastlaneCore::UI.message("Deleting xcresults: #{xcresults}")
35
+ FileUtils.rm_rf(xcresults)
36
+ end
37
+
38
+ def output_directory
39
+ absolute_output_directory = File.absolute_path(@options[:output_directory])
40
+ if @options[:batch]
41
+ testable = @options.fetch(:only_testing, ['']).first.split('/').first || ''
42
+ absolute_output_directory = File.join(absolute_output_directory, "#{testable}-batch-#{@options[:batch]}")
43
+ end
44
+ absolute_output_directory
45
+ end
46
+
47
+ def print_starting_scan_message
48
+ scan_message = "Starting scan ##{@testrun_count + 1} with #{@options.fetch(:only_testing, []).size} tests"
49
+ scan_message << " for batch ##{@options[:batch]}" unless @options[:batch].nil?
50
+ FastlaneCore::UI.message("#{scan_message}.")
51
+ end
52
+
53
+ def set_json_env
54
+ return unless @reportnamer.includes_json?
55
+
56
+ ENV['XCPRETTY_JSON_FILE_OUTPUT'] = File.join(
57
+ output_directory,
58
+ @reportnamer.json_last_reportname
59
+ )
60
+ end
61
+
62
+ def reset_json_env
63
+ return unless @reportnamer.includes_json?
64
+
65
+ ENV['XCPRETTY_JSON_FILE_OUTPUT'] = @xcpretty_json_file_output
66
+ end
67
+
68
+ def remove_preexisting_test_result_bundles
69
+ return unless @options[:result_bundle]
70
+
71
+ glob_pattern = "#{output_directory}/*.test_result"
72
+ preexisting_test_result_bundles = Dir.glob(glob_pattern)
73
+ FileUtils.rm_rf(preexisting_test_result_bundles)
74
+ end
75
+
76
+ def scan_options
77
+ valid_scan_keys = Fastlane::Actions::ScanAction.available_options.map(&:key)
78
+ @options.select { |k,v| valid_scan_keys.include?(k) }
79
+ .merge(@reportnamer.scan_options)
80
+ .merge(output_directory: output_directory)
81
+ end
82
+
83
+ # after_testrun methods
84
+
85
+ def after_testrun(exception = nil)
86
+ @testrun_count = @testrun_count + 1
87
+ if exception.kind_of?(FastlaneCore::Interface::FastlaneTestFailure)
88
+ handle_test_failure
89
+ elsif exception.kind_of?(FastlaneCore::Interface::FastlaneBuildFailure)
90
+ handle_build_failure(exception)
91
+ else
92
+ handle_success
93
+ end
94
+ collate_reports
95
+ end
96
+
97
+ def handle_success
98
+ send_callback_testrun_info
99
+ move_test_result_bundle_for_next_run
100
+ reset_json_env
101
+ end
102
+
103
+ def collate_reports
104
+
105
+ report_collator_options = {
106
+ source_reports_directory_glob: output_directory,
107
+ output_directory: output_directory,
108
+ reportnamer: @reportnamer,
109
+ scheme: @options[:scheme],
110
+ result_bundle: @options[:result_bundle]
111
+ }
112
+ TestCenter::Helper::MultiScanManager::ReportCollator.new(report_collator_options).collate
113
+ end
114
+
115
+ def handle_test_failure
116
+ send_callback_testrun_info
117
+ reset_simulators
118
+ move_test_result_bundle_for_next_run
119
+ update_scan_options
120
+ @reportnamer.increment
121
+ end
122
+
123
+ def send_callback_testrun_info(additional_info = {})
124
+ return unless @options[:testrun_completed_block]
125
+
126
+ report_filepath = nil
127
+ junit_results = {}
128
+ unless additional_info.key?(:test_operation_failure)
129
+ report_filepath = File.join(output_directory, @reportnamer.junit_last_reportname)
130
+
131
+ config = FastlaneCore::Configuration.create(
132
+ Fastlane::Actions::TestsFromJunitAction.available_options,
133
+ {
134
+ junit: File.absolute_path(report_filepath)
135
+ }
136
+ )
137
+ junit_results = Fastlane::Actions::TestsFromJunitAction.run(config)
138
+ end
139
+
140
+ info = {
141
+ failed: junit_results[:failed],
142
+ passing: junit_results[:passing],
143
+ batch: @options[:batch] || 1,
144
+ try_count: @testrun_count,
145
+ report_filepath: report_filepath
146
+ }.merge(additional_info)
147
+
148
+ if @reportnamer.includes_html?
149
+ html_report_filepath = File.join(output_directory, @reportnamer.html_last_reportname)
150
+ info[:html_report_filepath] = html_report_filepath
151
+ end
152
+ if @reportnamer.includes_json?
153
+ json_report_filepath = File.join(output_directory, @reportnamer.json_last_reportname)
154
+ info[:json_report_filepath] = json_report_filepath
155
+ end
156
+ if @options[:result_bundle]
157
+ test_result_suffix = '.test_result'
158
+ test_result_suffix.prepend("-#{@reportnamer.report_count}") unless @reportnamer.report_count.zero?
159
+ test_result_bundlepath = File.join(output_directory, @options[:scheme]) + test_result_suffix
160
+ info[:test_result_bundlepath] = test_result_bundlepath
161
+ end
162
+ @options[:testrun_completed_block].call(info)
163
+ end
164
+
165
+ def update_scan_options
166
+ update_only_testing
167
+ turn_off_code_coverage
168
+ end
169
+
170
+ def turn_off_code_coverage
171
+ # Turn off code coverage as code coverage reports are not merged and
172
+ # the first, more valuable, report will be overwritten
173
+ @options.delete(:code_coverage)
174
+ end
175
+
176
+ def update_only_testing
177
+ report_filepath = File.join(output_directory, @reportnamer.junit_last_reportname)
178
+ config = FastlaneCore::Configuration.create(
179
+ Fastlane::Actions::TestsFromJunitAction.available_options,
180
+ {
181
+ junit: File.absolute_path(report_filepath)
182
+ }
183
+ )
184
+ @options[:only_testing] = Fastlane::Actions::TestsFromJunitAction.run(config)[:failed]
185
+ if @options[:invocation_based_tests]
186
+ @options[:only_testing] = @options[:only_testing].map(&:strip_testcase).uniq
187
+ end
188
+ end
189
+
190
+ def reset_simulators
191
+ return unless @options[:reset_simulators]
192
+
193
+ @options[:simulators].each(&:reset)
194
+ end
195
+
196
+ def handle_build_failure(exception)
197
+ test_operation_failure = ''
198
+
199
+ test_session_last_messages = last_lines_of_test_session_log
200
+ test_operation_failure_match = /Test operation failure: (?<test_operation_failure>.*)$/ =~ test_session_last_messages
201
+ if test_operation_failure_match.nil?
202
+ test_operation_failure = 'Unknown test operation failure'
203
+ end
204
+
205
+ case test_operation_failure
206
+ when /Test runner exited before starting test execution/
207
+ FastlaneCore::UI.error(test_operation_failure)
208
+ when /Lost connection to testmanagerd/
209
+ FastlaneCore::UI.error(test_operation_failure)
210
+ FastlaneCore::UI.important("com.apple.CoreSimulator.CoreSimulatorService may have become corrupt, consider quitting it")
211
+ if @options[:quit_core_simulator_service]
212
+ Fastlane::Actions::RestartCoreSimulatorServiceAction.run
213
+ end
214
+ else
215
+ FastlaneCore::UI.error(test_operation_failure)
216
+ send_callback_testrun_info(test_operation_failure: test_operation_failure)
217
+ raise exception
218
+ end
219
+ if @options[:reset_simulators]
220
+ @options[:simulators].each do |simulator|
221
+ simulator.reset
222
+ end
223
+ end
224
+ send_callback_testrun_info(test_operation_failure: test_operation_failure)
225
+ end
226
+
227
+ def last_lines_of_test_session_log
228
+ derived_data_path = File.expand_path(@options[:derived_data_path])
229
+ test_session_logs = Dir.glob("#{derived_data_path}/Logs/Test/*.xcresult/*_Test/Diagnostics/**/Session-*.log")
230
+ test_session_logs.sort! { |logfile1, logfile2| File.mtime(logfile1) <=> File.mtime(logfile2) }
231
+ test_session = File.open(test_session_logs.last)
232
+ backwards_seek_offset = -1 * [1000, test_session.stat.size].min
233
+ test_session.seek(backwards_seek_offset, IO::SEEK_END)
234
+ test_session_last_messages = test_session.read
235
+ end
236
+
237
+ def move_test_result_bundle_for_next_run
238
+ return unless @options[:result_bundle]
239
+
240
+ glob_pattern = "#{output_directory}/*.test_result"
241
+ preexisting_test_result_bundles = Dir.glob(glob_pattern)
242
+ unnumbered_test_result_bundles = preexisting_test_result_bundles.reject do |test_result|
243
+ test_result =~ /.*-\d+\.test_result/
244
+ end
245
+ src_test_bundle = unnumbered_test_result_bundles.first
246
+ dst_test_bundle_parent_dir = File.dirname(src_test_bundle)
247
+ dst_test_bundle_basename = File.basename(src_test_bundle, '.test_result')
248
+ dst_test_bundle = "#{dst_test_bundle_parent_dir}/#{dst_test_bundle_basename}-#{@testrun_count}.test_result"
249
+ FileUtils.mkdir_p(dst_test_bundle)
250
+ FileUtils.mv(src_test_bundle, dst_test_bundle)
251
+ end
252
+ end
253
+ end
254
+ end
255
+ end