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.
- checksums.yaml +4 -4
- data/README.md +71 -6
- data/lib/fastlane/plugin/test_center/actions/collate_html_reports.rb +2 -0
- data/lib/fastlane/plugin/test_center/actions/collate_json_reports.rb +2 -0
- data/lib/fastlane/plugin/test_center/actions/collate_junit_reports.rb +2 -1
- data/lib/fastlane/plugin/test_center/actions/collate_test_result_bundles.rb +2 -0
- data/lib/fastlane/plugin/test_center/actions/multi_scan.rb +127 -32
- data/lib/fastlane/plugin/test_center/actions/quit_core_simulator_service.rb +40 -0
- data/lib/fastlane/plugin/test_center/actions/suppress_tests.rb +2 -0
- data/lib/fastlane/plugin/test_center/actions/suppress_tests_from_junit.rb +3 -1
- data/lib/fastlane/plugin/test_center/actions/suppressed_tests.rb +2 -0
- data/lib/fastlane/plugin/test_center/actions/tests_from_junit.rb +2 -0
- data/lib/fastlane/plugin/test_center/actions/tests_from_xctestrun.rb +2 -0
- data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/device_manager.rb +30 -0
- data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/parallel_test_batch_worker.rb +73 -0
- data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/report_collator.rb +129 -0
- data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/retrying_scan.rb +77 -0
- data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/retrying_scan_helper.rb +272 -0
- data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/runner.rb +109 -0
- data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/simulator_helper.rb +49 -0
- data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/test_batch_worker.rb +24 -0
- data/lib/fastlane/plugin/test_center/helper/multi_scan_manager/test_batch_worker_pool.rb +133 -0
- data/lib/fastlane/plugin/test_center/helper/multi_scan_manager.rb +3 -0
- data/lib/fastlane/plugin/test_center/helper/reportname_helper.rb +15 -6
- data/lib/fastlane/plugin/test_center/helper/test_collector.rb +55 -14
- data/lib/fastlane/plugin/test_center/helper/xctestrun_info.rb +42 -0
- data/lib/fastlane/plugin/test_center/version.rb +1 -1
- data/lib/fastlane/plugin/test_center.rb +1 -1
- metadata +22 -11
- 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
|