fastlane-plugin-retry_tests 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c836a5f93edff7bdd7d09d6e5d7a4e62e075352b
4
+ data.tar.gz: aacd3dcc668bb57dbf6e99162e23cd78e95ba49a
5
+ SHA512:
6
+ metadata.gz: 6f37a54bedb06e4a9bb3027e51781294c5df4d0465fbe1ccb7c717e9eb2aafe6890a63d7040693e41e91c103bb431bf00806ac04c34ac4f3fe86558640ffa7f5
7
+ data.tar.gz: 4bb410cacd5a8e72187d27e449e04369a56b3a19b44e181b4f1d34257df19d910cc1c2bd4288016162bfdcaad8c5a394b2f700d9e31002e4462917a4f9e6d95d
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Lyndsey Ferguson <lyndsey.ferguson@appian.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # fastlane-plugin-retry
2
+ A fastlane plugin for retrying iOS automation tests. Based on the "fastlane-plugin-test_center" plugin repository by Github user lyndsey-ferguson.
@@ -0,0 +1,16 @@
1
+ require 'fastlane/plugin/retry_tests/version'
2
+
3
+ module Fastlane
4
+ module RetryTests
5
+ # Return all .rb files inside the "actions" and "helper" directory
6
+ def self.all_classes
7
+ Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))]
8
+ end
9
+ end
10
+ end
11
+
12
+ # By default we want to import all available actions and helpers
13
+ # A plugin can contain any number of actions and plugins
14
+ Fastlane::RetryTests.all_classes.each do |current|
15
+ require current
16
+ end
@@ -0,0 +1,177 @@
1
+ #Adapted from the "fastlane-plugin-test_center" plugin
2
+
3
+ require 'nokogiri'
4
+ require 'nokogiri-plist'
5
+ require 'FileUtils'
6
+ module Fastlane
7
+ module Actions
8
+ class CollateJunitReportsAction < Action
9
+
10
+ def self.run(params)
11
+ report_filepaths = params[:reports].reverse
12
+ if report_filepaths.size == 1
13
+ FileUtils.cp(report_filepaths[0], params[:collated_report])
14
+ else
15
+ #Convert .plist reports to Nokogiri XML objects
16
+ target_report = File.open(report_filepaths.shift) {|f| Nokogiri::XML(f)}
17
+ file_name = params[:collated_report] + "/#{params[:scheme]}.test_result/1_Test/action_TestSummaries.plist"
18
+ reports = report_filepaths.map { |report_filepath| Nokogiri::XML(Nokogiri::PList(open(report_filepath)).to_plist_xml) }
19
+
20
+ #Merge .plist reports
21
+ reports.each do |retry_report|
22
+ mergeLists(target_report, retry_report, params)
23
+ end
24
+ end
25
+ merge_assets(params[:assets], params[:collated_report] + "/Attachments")
26
+ merge_logs(params[:logs], params[:collated_report] + "/")
27
+ end
28
+
29
+ def self.mergeLists(target_report, retry_report, params)
30
+ UI.verbose("Merging retried results...")
31
+ Dir.mkdir(params[:collated_report]) unless File.exists?(params[:collated_report])
32
+ file_name = params[:collated_report] + "/action_TestSummaries.plist"
33
+ retried_tests = retry_report.xpath("//key[contains(.,'TestSummaryGUID')]/..")
34
+ current_node = retried_tests.shift
35
+
36
+ #Copy retried test results into the base report
37
+ while (current_node != nil)
38
+ testName = get_test_name(current_node)
39
+ matching_node = target_report.at_xpath("//string[contains(.,'#{testName}')]/..")
40
+
41
+ if (!matching_node.nil?)
42
+ matching_node.previous.next.replace(current_node)
43
+ write_report_to_file(target_report, file_name)
44
+ end
45
+ current_node = retried_tests.shift
46
+ end
47
+ end
48
+
49
+ def self.merge_assets(asset_files, assets_folder)
50
+ UI.verbose ("Merging screenshot folders...")
51
+
52
+ #copy screenshots from all retries into the results folder
53
+ Dir.mkdir(assets_folder) unless File.exists?(assets_folder)
54
+ asset_files.each do |folder|
55
+ FileUtils.cp_r(Dir[folder + '/*'], assets_folder)
56
+ end
57
+ end
58
+
59
+ def self.merge_logs(log_files, logs_folder)
60
+ UI.verbose("Merging console logs...")
61
+
62
+ #copy console logs from all retries into the results folder
63
+ target_log = log_files.shift
64
+ log_files.each do |log|
65
+ to_append = File.read(log)
66
+ File.open(target_log, "a") do |handle|
67
+ handle.puts to_append
68
+ end
69
+ FileUtils.cp_r(target_log, logs_folder)
70
+ end
71
+ end
72
+
73
+ def self.write_report_to_file(report, file_name)
74
+ UI.verbose("Writing merged results to file...")
75
+ File.new(file_name, 'w')
76
+ File.open(file_name, 'w') do |f|
77
+ f.write(report.to_xml)
78
+ end
79
+ end
80
+
81
+ def self.get_test_name(test_data)
82
+ test_name = test_data.xpath("(//key[contains(.,'TestSummaryGUID')])/../key[contains(.,'TestName')]/following-sibling::string").to_a[0].to_s
83
+ test_name = test_name[8..-10]
84
+ test_name
85
+ end
86
+
87
+ #####################################################
88
+ # @!group Documentation
89
+ #####################################################
90
+
91
+ def self.description
92
+ "Combines and combines tests from multiple junit report files"
93
+ end
94
+
95
+ def self.details
96
+ "The first junit report is used as the base report. Testcases " \
97
+ "from other reports are added if they do not already exist, or " \
98
+ "if the testcases already exist, they are replaced." \
99
+ "" \
100
+ "This is done because it is assumed that fragile tests, when " \
101
+ "re-run will often succeed due to less interference from other " \
102
+ "tests and the subsequent junit reports will have more passed tests." \
103
+ "" \
104
+ "Inspired by Derek Yang's fastlane-plugin-merge_junit_report"
105
+ end
106
+
107
+ def self.available_options
108
+ [
109
+ FastlaneCore::ConfigItem.new(
110
+ key: :scheme,
111
+ env_name: 'SCHEME',
112
+ description: 'The test scheme being run',
113
+ optional: false,
114
+ default_value: '',
115
+ type: String
116
+ ),
117
+ FastlaneCore::ConfigItem.new(
118
+ key: :reports,
119
+ env_name: 'COLLATE_JUNIT_REPORTS_REPORTS',
120
+ description: 'An array of junit reports to collate. The first report is used as the base into which other reports are merged in',
121
+ optional: false,
122
+ type: Array,
123
+ verify_block: proc do |reports|
124
+ UI.user_error!('No junit report files found') if reports.empty?
125
+ reports.each do |report|
126
+ UI.user_error!("Error: junit report not found: '#{report}'") unless File.exist?(report)
127
+ end
128
+ end
129
+ ),
130
+ FastlaneCore::ConfigItem.new(
131
+ key: :collated_report,
132
+ env_name: 'COLLATE_JUNIT_REPORTS_COLLATED_REPORT',
133
+ description: 'The final junit report file where all testcases will be merged into',
134
+ optional: true,
135
+ default_value: 'result.xml',
136
+ type: String
137
+ ),
138
+ FastlaneCore::ConfigItem.new(
139
+ key: :assets,
140
+ env_name: 'COLLATE_JUNIT_REPORTS_ASSETS',
141
+ description: 'An array of junit reports to collate. The first report is used as the base into which other reports are merged in',
142
+ optional: false,
143
+ type: Array,
144
+ verify_block: proc do |assets|
145
+ UI.user_error!('No junit report files found') if assets.empty?
146
+ assets.each do |asset|
147
+ UI.user_error!("Error: junit report not found: '#{asset}'") unless File.exist?(asset)
148
+ end
149
+ end
150
+ ),
151
+ FastlaneCore::ConfigItem.new(
152
+ key: :logs,
153
+ env_name: 'COLLATE_JUNIT_REPORTS_LOGS',
154
+ description: 'An array of junit reports to collate. The first report is used as the base into which other reports are merged in',
155
+ optional: false,
156
+ type: Array,
157
+ verify_block: proc do |logs|
158
+ UI.user_error!('No junit report files found') if logs.empty?
159
+ logs.each do |log|
160
+ UI.user_error!("Error: junit report not found: '#{log}'") unless File.exist?(log)
161
+ end
162
+ end
163
+ )
164
+ ]
165
+ end
166
+
167
+ def self.authors
168
+ #Adapted from the "fastlane-plugin-test_center" plugin by:
169
+ ["lyndsey-ferguson/@lyndseydf"]
170
+ end
171
+
172
+ def self.is_supported?(platform)
173
+ platform == :ios
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,146 @@
1
+ #Adapted from the "fastlane-plugin-test_center" plugin
2
+
3
+ module Fastlane
4
+ module Actions
5
+ require 'fastlane/actions/scan'
6
+ require 'shellwords'
7
+
8
+ class MultiScanAction < Action
9
+ def self.run(params)
10
+ try_count = 0
11
+ scan_options = params.values.reject { |k| k == :try_count }
12
+ final_report_path = scan_options[:result_bundle]
13
+ unless Helper.test?
14
+ FastlaneCore::PrintTable.print_values(
15
+ config: params._values.reject { |k, v| scan_options.key?(k) },
16
+ title: "Summary for multi_scan (test_center v#{Fastlane::TestCenter::VERSION})"
17
+ )
18
+ end
19
+
20
+ scan_options = config_with_junit_report(scan_options)
21
+
22
+ begin
23
+ try_count += 1
24
+ scan_options = config_with_retry(scan_options, try_count)
25
+ config = FastlaneCore::Configuration.create(Fastlane::Actions::ScanAction.available_options, scan_options)
26
+ Fastlane::Actions::ScanAction.run(config)
27
+ rescue FastlaneCore::Interface::FastlaneTestFailure => e
28
+ UI.verbose("Scan failed with error #{e}")
29
+ #Retry for the specified number of times if there were failed tests
30
+ if try_count < params[:try_count]
31
+ report_filepath = junit_report_filepath(scan_options)
32
+ failed_tests = other_action.tests_from_junit(junit: report_filepath)[:failed]
33
+ scan_options[:only_testing] = failed_tests.map(&:shellescape)
34
+ retry
35
+ end
36
+ end
37
+ merge_reports(scan_options, final_report_path)
38
+ end
39
+
40
+ def self.merge_reports(scan_options, final_report_path)
41
+ folder = get_folder_root(scan_options[:output_directory])
42
+ report_files = Dir.glob("#{folder}*/#{scan_options[:scheme]}.test_result/1_Test/action_TestSummaries.plist")
43
+ asset_files = Dir.glob("#{folder}*/#{scan_options[:scheme]}.test_result/1_Test/Attachments")
44
+ log_files = Dir.glob("#{folder}*/#{scan_options[:scheme]}.test_result/1_Test/action.xcactivitylog")
45
+ #Merge all reports, screenshots, and logs if there were retried tests
46
+ if report_files.size > 1
47
+ other_action.collate_junit_reports(
48
+ scheme: scan_options[:scheme],
49
+ reports: report_files,
50
+ collated_report: final_report_path,
51
+ assets: asset_files,
52
+ logs: log_files,
53
+ )
54
+ end
55
+ end
56
+
57
+ def self.config_has_junit_report(config)
58
+ output_types = config.fetch(:output_types, '').to_s.split(',')
59
+ output_filenames = config.fetch(:output_files, '').to_s.split(',')
60
+
61
+ output_type_file_count_match = output_types.size == output_filenames.size
62
+ output_types.include?('junit') && (output_type_file_count_match || config[:custom_report_file_name].to_s.strip.length > 0)
63
+ end
64
+
65
+ def self.config_with_retry(config, count)
66
+ folder = get_folder_root(config[:result_bundle])
67
+ config[:result_bundle] = (folder + count.to_s)
68
+ config[:output_directory] = (folder + count.to_s)
69
+ config
70
+ end
71
+
72
+ def self.get_folder_root(folder)
73
+ folder = folder.gsub(/ *\d+$/, '')
74
+ folder
75
+ end
76
+
77
+ def self.config_with_junit_report(config)
78
+ return config if config_has_junit_report(config)
79
+
80
+ if config[:output_types].to_s.strip.empty? || config[:custom_report_file_name]
81
+ config[:custom_report_file_name] ||= 'report.xml'
82
+ config[:output_types] = 'junit'
83
+ elsif config[:output_types].strip == 'junit' && config[:output_files].to_s.strip.empty?
84
+ config[:custom_report_file_name] ||= 'report.xml'
85
+ elsif !config[:output_types].split(',').include?('junit')
86
+ config[:output_types] << ',junit'
87
+ config[:output_files] << ',report.xml'
88
+ elsif config[:output_files].nil?
89
+ config[:output_files] = config[:output_types].split(',').map { |type| "report.#{type}" }.join(',')
90
+ end
91
+ config
92
+ end
93
+
94
+ def self.junit_report_filename(config)
95
+ report_filename = config[:custom_report_file_name]
96
+ if report_filename.nil?
97
+ junit_index = config[:output_types].split(',').find_index('junit')
98
+ report_filename = config[:output_files].to_s.split(',')[junit_index]
99
+ end
100
+ report_filename
101
+ end
102
+
103
+ def self.junit_report_filepath(config)
104
+ File.absolute_path(File.join(config[:output_directory], junit_report_filename(config)))
105
+ end
106
+
107
+ #####################################################
108
+ # @!group Documentation
109
+ #####################################################
110
+
111
+ def self.description
112
+ "Uses scan to run Xcode tests a given number of times: only re-testing failing tests."
113
+ end
114
+
115
+ def self.details
116
+ "Use this action to run your tests if you have fragile tests that fail sporadically."
117
+ end
118
+
119
+ def self.scan_options
120
+ ScanAction.available_options
121
+ end
122
+
123
+ def self.available_options
124
+ scan_options + [
125
+ FastlaneCore::ConfigItem.new(
126
+ key: :try_count,
127
+ env_name: "FL_MULTI_SCAN_TRY_COUNT",
128
+ description: "The number of times to retry running tests via scan",
129
+ type: Integer,
130
+ is_string: false,
131
+ default_value: 1
132
+ )
133
+ ]
134
+ end
135
+
136
+ def self.authors
137
+ #Adapted from the "fastlane-plugin-test_center" plugin by:
138
+ ["lyndsey-ferguson/@lyndseydf"]
139
+ end
140
+
141
+ def self.is_supported?(platform)
142
+ platform == :ios
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,93 @@
1
+ module Fastlane
2
+ module Actions
3
+ class SuppressTestsAction < Action
4
+ require 'xcodeproj'
5
+
6
+ def self.run(params)
7
+ project_path = params[:xcodeproj]
8
+ all_tests_to_skip = params[:tests]
9
+ scheme = params[:scheme]
10
+
11
+ scheme_filepaths = Dir.glob("#{project_path}/{xcshareddata,xcuserdata}/**/xcschemes/#{scheme || '*'}.xcscheme")
12
+ if scheme_filepaths.length.zero?
13
+ UI.user_error!("Error: cannot find any scheme named #{scheme}") unless scheme.nil?
14
+ UI.user_error!("Error: cannot find any schemes in the Xcode project")
15
+ end
16
+
17
+ scheme_filepaths.each do |scheme_filepath|
18
+ is_dirty = false
19
+ xcscheme = Xcodeproj::XCScheme.new(scheme_filepath)
20
+ xcscheme.test_action.testables.each do |testable|
21
+ buildable_name = File.basename(testable.buildable_references[0].buildable_name, '.xctest')
22
+
23
+ tests_to_skip = all_tests_to_skip.select { |test| test.start_with?(buildable_name) }
24
+ .map { |test| test.sub("#{buildable_name}/", '') }
25
+
26
+ tests_to_skip.each do |test_to_skip|
27
+ skipped_test = Xcodeproj::XCScheme::TestAction::TestableReference::SkippedTest.new
28
+ skipped_test.identifier = test_to_skip
29
+ testable.add_skipped_test(skipped_test)
30
+ is_dirty = true
31
+ end
32
+ end
33
+ xcscheme.save! if is_dirty
34
+ end
35
+ nil
36
+ end
37
+
38
+ #####################################################
39
+ # @!group Documentation
40
+ #####################################################
41
+
42
+ def self.description
43
+ "Suppresses specific tests in a specific or all Xcode Schemes in a given project"
44
+ end
45
+
46
+ def self.available_options
47
+ [
48
+ FastlaneCore::ConfigItem.new(
49
+ key: :xcodeproj,
50
+ env_name: "FL_SUPPRESS_TESTS_XCODE_PROJECT",
51
+ description: "The file path to the Xcode project file to modify",
52
+ verify_block: proc do |path|
53
+ UI.user_error!("Error: Xcode project file path not given!") unless path and !path.empty?
54
+ UI.user_error!("Error: Xcode project '#{path}' not found!") unless Dir.exist?(path)
55
+ end
56
+ ),
57
+ FastlaneCore::ConfigItem.new(
58
+ key: :tests,
59
+ env_name: "FL_SUPPRESS_TESTS_TESTS_TO_SUPPRESS",
60
+ description: "A list of tests to suppress",
61
+ verify_block: proc do |tests|
62
+ UI.user_error!("Error: no tests were given to suppress!") unless tests and !tests.empty?
63
+ tests.each do |test_identifier|
64
+ is_valid_test_identifier = %r{^[a-zA-Z][a-zA-Z0-9]+\/[a-zA-Z][a-zA-Z0-9]+(\/test[a-zA-Z0-9]+)?$} =~ test_identifier
65
+ unless is_valid_test_identifier
66
+ UI.user_error!("Error: invalid test identifier '#{test_identifier}'. It must be in the format of 'Testable/TestSuiteToSuppress' or 'Testable/TestSuiteToSuppress/testToSuppress'")
67
+ end
68
+ end
69
+ end,
70
+ type: Array
71
+ ),
72
+ FastlaneCore::ConfigItem.new(
73
+ key: :scheme,
74
+ optional: true,
75
+ env_name: "FL_SUPPRESS_TESTS_SCHEME_TO_UPDATE",
76
+ description: "The Xcode scheme where the tests should be suppressed",
77
+ verify_block: proc do |scheme_name|
78
+ UI.user_error!("Error: Xcode Scheme '#{scheme_name}' is not valid!") if scheme_name and scheme_name.empty?
79
+ end
80
+ )
81
+ ]
82
+ end
83
+
84
+ def self.authors
85
+ ["lyndsey-ferguson/@lyndseydf"]
86
+ end
87
+
88
+ def self.is_supported?(platform)
89
+ platform == :ios
90
+ end
91
+ end
92
+ end
93
+ end