fastlane-plugin-test_center 3.7.0 → 3.8.0.parallelizing.beta.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|