fastlane-plugin-retry_tests 1.0.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 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