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.
@@ -0,0 +1,102 @@
1
+ module Fastlane
2
+ module Actions
3
+ class SuppressTestsFromJunitAction < Action
4
+ def self.run(params)
5
+ project_path = params[:xcodeproj]
6
+ scheme = params[:scheme]
7
+
8
+ scheme_filepaths = Dir.glob("#{project_path}/{xcshareddata,xcuserdata}/**/xcschemes/#{scheme || '*'}.xcscheme")
9
+ if scheme_filepaths.length.zero?
10
+ UI.user_error!("Error: cannot find any scheme named #{scheme}") unless scheme.nil?
11
+ UI.user_error!("Error: cannot find any schemes in the Xcode project")
12
+ end
13
+
14
+ testables = Hash.new([])
15
+ desired_passed_status = (params[:suppress_type] == :passing)
16
+
17
+ report = ::TestCenter::Helper::XcodeJunit::Report.new(params[:junit])
18
+
19
+ report.testables.each do |testable|
20
+ testables[testable.name] = []
21
+ testable.testsuites.each do |testsuite|
22
+ testsuite.testcases.each do |testcase|
23
+ if testcase.passed? == desired_passed_status
24
+ testables[testable.name] << testcase.skipped_test
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ scheme_filepaths.each do |scheme_filepath|
31
+ is_dirty = false
32
+ xcscheme = Xcodeproj::XCScheme.new(scheme_filepath)
33
+
34
+ xcscheme.test_action.testables.each do |testable|
35
+ buildable_name = testable.buildable_references[0].buildable_name
36
+ testables[buildable_name].each do |skipped_test|
37
+ testable.add_skipped_test(skipped_test)
38
+ is_dirty = true
39
+ end
40
+ end
41
+ xcscheme.save! if is_dirty
42
+ end
43
+ end
44
+
45
+ #####################################################
46
+ # @!group Documentation
47
+ #####################################################
48
+
49
+ def self.description
50
+ "Uses a junit xml report file to suppress either passing or failing tests in an Xcode Scheme"
51
+ end
52
+
53
+ def self.available_options
54
+ [
55
+ FastlaneCore::ConfigItem.new(
56
+ key: :xcodeproj,
57
+ env_name: "FL_SUPPRESS_TESTS_FROM_JUNIT_XCODE_PROJECT",
58
+ description: "The file path to the Xcode project file to modify",
59
+ verify_block: proc do |path|
60
+ UI.user_error!("Error: Xcode project file path not given!") unless path and !path.empty?
61
+ UI.user_error!("Error: Xcode project '#{path}' not found!") unless Dir.exist?(path)
62
+ end
63
+ ),
64
+ FastlaneCore::ConfigItem.new(
65
+ key: :junit,
66
+ env_name: "FL_SUPPRESS_TESTS_FROM_JUNIT_JUNIT_REPORT",
67
+ description: "The junit xml report file from which to collect the tests to suppress",
68
+ verify_block: proc do |path|
69
+ UI.user_error!("Error: cannot find the junit xml report file '#{path}'") unless File.exist?(path)
70
+ end
71
+ ),
72
+ FastlaneCore::ConfigItem.new(
73
+ key: :scheme,
74
+ optional: true,
75
+ env_name: "FL_SUPPRESS_TESTS_FROM_JUNIT_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
+ FastlaneCore::ConfigItem.new(
82
+ key: :suppress_type,
83
+ type: Symbol,
84
+ env_name: "FL_SUPPRESS_TESTS_FROM_JUNIT_SUPPRESS_TYPE",
85
+ description: "Tests to suppress are either :failed or :passing",
86
+ verify_block: proc do |type|
87
+ UI.user_error!("Error: suppress type ':#{type}' is invalid! Only :failed or :passing are valid types") unless %i[failed passing].include?(type)
88
+ end
89
+ )
90
+ ]
91
+ end
92
+
93
+ def self.authors
94
+ ["lyndsey-ferguson/@lyndseydf"]
95
+ end
96
+
97
+ def self.is_supported?(platform)
98
+ platform == :ios
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,72 @@
1
+ module Fastlane
2
+ module Actions
3
+ class SuppressedTestsAction < Action
4
+ require 'set'
5
+
6
+ def self.run(params)
7
+ project_path = params[:xcodeproj]
8
+ scheme = params[:scheme]
9
+
10
+ scheme_filepaths = Dir.glob("#{project_path}/{xcshareddata,xcuserdata}/**/xcschemes/#{scheme || '*'}.xcscheme")
11
+ if scheme_filepaths.length.zero?
12
+ UI.user_error!("Error: cannot find any scheme named #{scheme}") unless scheme.nil?
13
+ UI.user_error!("Error: cannot find any schemes in the Xcode project")
14
+ end
15
+
16
+ skipped_tests = Set.new
17
+ scheme_filepaths.each do |scheme_filepath|
18
+ xcscheme = Xcodeproj::XCScheme.new(scheme_filepath)
19
+ xcscheme.test_action.testables.each do |testable|
20
+ buildable_name = testable.buildable_references[0]
21
+ .buildable_name
22
+
23
+ buildable_name = File.basename(buildable_name, '.xctest')
24
+ testable.skipped_tests.map do |skipped_test|
25
+ skipped_tests.add("#{buildable_name}/#{skipped_test.identifier}")
26
+ end
27
+ end
28
+ end
29
+ skipped_tests.to_a
30
+ end
31
+
32
+ #####################################################
33
+ # @!group Documentation
34
+ #####################################################
35
+
36
+ def self.description
37
+ "Retrieves a list of tests that are suppressed in a specific or all Xcode Schemes in a project"
38
+ end
39
+
40
+ def self.available_options
41
+ [
42
+ FastlaneCore::ConfigItem.new(
43
+ key: :xcodeproj,
44
+ env_name: "FL_SUPPRESSED_TESTS_XCODE_PROJECT",
45
+ description: "The file path to the Xcode project file to read the skipped tests from",
46
+ verify_block: proc do |path|
47
+ UI.user_error!("Error: Xcode project file path not given!") unless path and !path.empty?
48
+ UI.user_error!("Error: Xcode project '#{path}' not found!") unless Dir.exist?(path)
49
+ end
50
+ ),
51
+ FastlaneCore::ConfigItem.new(
52
+ key: :scheme,
53
+ optional: true,
54
+ env_name: "FL_SUPPRESSED_TESTS_SCHEME_TO_UPDATE",
55
+ description: "The Xcode scheme where the suppressed tests may be",
56
+ verify_block: proc do |scheme_name|
57
+ UI.user_error!("Error: Xcode Scheme '#{scheme_name}' is not valid!") if scheme_name and scheme_name.empty?
58
+ end
59
+ )
60
+ ]
61
+ end
62
+
63
+ def self.authors
64
+ ["lyndsey-ferguson/@lyndseydf"]
65
+ end
66
+
67
+ def self.is_supported?(platform)
68
+ platform == :ios
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,59 @@
1
+ module Fastlane
2
+ module Actions
3
+ class TestsFromJunitAction < Action
4
+ def self.run(params)
5
+ report = ::TestCenter::Helper::XcodeJunit::Report.new(params[:junit])
6
+ passing = []
7
+ failed = []
8
+ report.testables.each do |testable|
9
+ testable.testsuites.each do |testsuite|
10
+ testsuite.testcases.each do |testcase|
11
+ if testcase.passed?
12
+ passing << testcase.identifier
13
+ else
14
+ failed << testcase.identifier
15
+ end
16
+ end
17
+ end
18
+ end
19
+ {
20
+ failed: failed,
21
+ passing: passing
22
+ }
23
+ end
24
+
25
+ #####################################################
26
+ # @!group Documentation
27
+ #####################################################
28
+
29
+ def self.description
30
+ "Retrieves the failing and passing tests as reported in a junit xml file"
31
+ end
32
+
33
+ def self.available_options
34
+ [
35
+ FastlaneCore::ConfigItem.new(
36
+ key: :junit,
37
+ env_name: "FL_SUPPRESS_TESTS_FROM_JUNIT_JUNIT_REPORT", # The name of the environment variable
38
+ description: "The junit xml report file from which to collect the tests to suppress",
39
+ verify_block: proc do |path|
40
+ UI.user_error!("Error: cannot find the junit xml report file '#{path}'") unless File.exist?(path)
41
+ end
42
+ )
43
+ ]
44
+ end
45
+
46
+ def self.return_value
47
+ "A Hash with an Array of :passing and :failed tests"
48
+ end
49
+
50
+ def self.authors
51
+ ["lyndsey-ferguson/lyndseydf"]
52
+ end
53
+
54
+ def self.is_supported?(platform)
55
+ platform == :ios
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,65 @@
1
+ require 'plist'
2
+
3
+ module Fastlane
4
+ module Actions
5
+ class TestsFromXctestrunAction < Action
6
+ def self.run(params)
7
+ return xctestrun_tests(params[:xctestrun])
8
+ end
9
+
10
+ def self.xctestrun_tests(xctestrun_path)
11
+ xctestrun = Plist.parse_xml(xctestrun_path)
12
+ xctestrun_rootpath = File.dirname(xctestrun_path)
13
+ tests = Hash.new([])
14
+ xctestrun.each do |testable_name, xctestrun_config|
15
+ test_identifiers = XCTestList.tests(xctest_bundle_path(xctestrun_rootpath, xctestrun_config))
16
+ if xctestrun_config.key?('SkipTestIdentifiers')
17
+ test_identifiers.reject! { |test_identifier| xctestrun_config['SkipTestIdentifiers'].include?(test_identifier) }
18
+ end
19
+ tests[testable_name] = test_identifiers.map do |test_identifier|
20
+ "#{testable_name.shellescape}/#{test_identifier}"
21
+ end
22
+ end
23
+ tests
24
+ end
25
+
26
+ def self.xctest_bundle_path(xctestrun_rootpath, xctestrun_config)
27
+ xctest_host_path = xctestrun_config['TestHostPath'].sub('__TESTROOT__', xctestrun_rootpath)
28
+ xctestrun_config['TestBundlePath'].sub('__TESTHOST__', xctest_host_path).sub('__TESTROOT__', xctestrun_rootpath)
29
+ end
30
+
31
+ #####################################################
32
+ # @!group Documentation
33
+ #####################################################
34
+
35
+ def self.description
36
+ "Retrieves all of the tests from xctest bundles referenced by the xctestrun file"
37
+ end
38
+
39
+ def self.available_options
40
+ [
41
+ FastlaneCore::ConfigItem.new(
42
+ key: :xctestrun,
43
+ env_name: "FL_SUPPRESS_TESTS_FROM_XCTESTRUN_FILE",
44
+ description: "The xctestrun file to use to find where the xctest bundle file is for test retrieval",
45
+ verify_block: proc do |path|
46
+ UI.user_error!("Error: cannot find the xctestrun file '#{path}'") unless File.exist?(path)
47
+ end
48
+ )
49
+ ]
50
+ end
51
+
52
+ def self.return_value
53
+ "A Hash of testable => tests, where testable is the name of the test target and tests is an array of test identifiers"
54
+ end
55
+
56
+ def self.authors
57
+ ["lyndsey-ferguson/lyndseydf"]
58
+ end
59
+
60
+ def self.is_supported?(platform)
61
+ platform == :ios
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,164 @@
1
+ module TestCenter
2
+ module Helper
3
+ require 'fastlane_core/ui/ui.rb'
4
+ require 'plist'
5
+
6
+ class CorrectingScanHelper
7
+ attr_reader :retry_total_count
8
+
9
+ def initialize(multi_scan_options)
10
+ @batch_count = multi_scan_options[:batch_count] || 1
11
+ @output_directory = multi_scan_options[:output_directory] || 'test_results'
12
+ @try_count = multi_scan_options[:try_count]
13
+ @retry_total_count = 0
14
+ @testrun_completed_block = multi_scan_options[:testrun_completed_block]
15
+ @given_custom_report_file_name = multi_scan_options[:custom_report_file_name]
16
+ @given_output_types = multi_scan_options[:output_types]
17
+ @given_output_files = multi_scan_options[:output_files]
18
+ @scan_options = multi_scan_options.reject do |option, _|
19
+ %i[
20
+ output_directory
21
+ only_testing
22
+ skip_testing
23
+ clean
24
+ try_count
25
+ batch_count
26
+ custom_report_file_name
27
+ fail_build
28
+ testrun_completed_block
29
+ ].include?(option)
30
+ end
31
+ @test_collector = TestCollector.new(multi_scan_options)
32
+ end
33
+
34
+ def scan
35
+ tests_passed = true
36
+ @testables_count = @test_collector.testables.size
37
+ @test_collector.testables.each do |testable|
38
+ tests_passed = scan_testable(testable) && tests_passed
39
+ end
40
+ tests_passed
41
+ end
42
+
43
+ def scan_testable(testable)
44
+ tests_passed = true
45
+ reportnamer = ReportNameHelper.new(
46
+ @given_output_types,
47
+ @given_output_files,
48
+ @given_custom_report_file_name
49
+ )
50
+ output_directory = @output_directory
51
+ testable_tests = @test_collector.testables_tests[testable]
52
+ if @batch_count > 1 || @testables_count > 1
53
+ current_batch = 1
54
+ testable_tests.each_slice((testable_tests.length / @batch_count.to_f).round).to_a.each do |tests_batch|
55
+ if @testables_count > 1
56
+ output_directory = File.join(@output_directory, "results-#{testable}")
57
+ end
58
+ FastlaneCore::UI.header("Starting test run on testable '#{testable}'")
59
+ tests_passed = correcting_scan(
60
+ {
61
+ only_testing: tests_batch,
62
+ output_directory: output_directory
63
+ },
64
+ current_batch,
65
+ reportnamer
66
+ ) && tests_passed
67
+ current_batch += 1
68
+ reportnamer.increment
69
+ end
70
+ else
71
+ options = {
72
+ output_directory: output_directory,
73
+ only_testing: testable_tests
74
+ }
75
+ tests_passed = correcting_scan(options, 1, reportnamer) && tests_passed
76
+ end
77
+ collate_reports(output_directory, reportnamer)
78
+ tests_passed
79
+ end
80
+
81
+ def collate_reports(output_directory, reportnamer)
82
+ report_files = Dir.glob("#{output_directory}/*#{reportnamer.junit_filextension}").map do |relative_filepath|
83
+ File.absolute_path(relative_filepath)
84
+ end
85
+ if report_files.size > 1
86
+ config = FastlaneCore::Configuration.create(
87
+ Fastlane::Actions::CollateJunitReportsAction.available_options,
88
+ {
89
+ reports: report_files.sort { |f1, f2| File.mtime(f1) <=> File.mtime(f2) },
90
+ collated_report: File.absolute_path(File.join(output_directory, reportnamer.junit_reportname))
91
+ }
92
+ )
93
+ Fastlane::Actions::CollateJunitReportsAction.run(config)
94
+ end
95
+ retried_junit_reportfiles = Dir.glob("#{output_directory}/**/*-[1-9]*#{reportnamer.junit_filextension}")
96
+ FileUtils.rm_f(retried_junit_reportfiles)
97
+ end
98
+
99
+ def correcting_scan(scan_run_options, batch, reportnamer)
100
+ scan_options = @scan_options.merge(scan_run_options)
101
+ try_count = 0
102
+ tests_passed = true
103
+ begin
104
+ try_count += 1
105
+ config = FastlaneCore::Configuration.create(
106
+ Fastlane::Actions::ScanAction.available_options,
107
+ scan_options.merge(reportnamer.scan_options)
108
+ )
109
+ quit_simulators
110
+ Fastlane::Actions::ScanAction.run(config)
111
+ @testrun_completed_block && @testrun_completed_block.call(
112
+ testrun_info(batch, try_count, reportnamer, scan_options[:output_directory])
113
+ )
114
+ tests_passed = true
115
+ rescue FastlaneCore::Interface::FastlaneTestFailure => e
116
+ FastlaneCore::UI.verbose("Scan failed with #{e}")
117
+ if try_count < @try_count
118
+ @retry_total_count += 1
119
+
120
+ info = testrun_info(batch, try_count, reportnamer, scan_options[:output_directory])
121
+ @testrun_completed_block && @testrun_completed_block.call(
122
+ info
123
+ )
124
+ scan_options[:only_testing] = info[:failed].map(&:shellescape)
125
+ FastlaneCore::UI.message('Re-running scan on only failed tests')
126
+ reportnamer.increment
127
+ retry
128
+ end
129
+ tests_passed = false
130
+ end
131
+ tests_passed
132
+ end
133
+
134
+ def testrun_info(batch, try_count, reportnamer, output_directory)
135
+ report_filepath = File.join(output_directory, reportnamer.junit_last_reportname)
136
+ config = FastlaneCore::Configuration.create(
137
+ Fastlane::Actions::TestsFromJunitAction.available_options,
138
+ {
139
+ junit: File.absolute_path(report_filepath)
140
+ }
141
+ )
142
+ junit_results = Fastlane::Actions::TestsFromJunitAction.run(config)
143
+
144
+ {
145
+ failed: junit_results[:failed],
146
+ passing: junit_results[:passing],
147
+ batch: batch,
148
+ try_count: try_count,
149
+ report_filepath: report_filepath
150
+ }
151
+ end
152
+
153
+ def quit_simulators
154
+ Fastlane::Actions.sh("killall -9 'iPhone Simulator' 'Simulator' 'SimulatorBridge' &> /dev/null || true", log: false)
155
+ launchctl_list_count = 0
156
+ while Fastlane::Actions.sh('launchctl list | grep com.apple.CoreSimulator.CoreSimulatorService || true', log: false) != ''
157
+ break if (launchctl_list_count += 1) > 10
158
+ Fastlane::Actions.sh('launchctl remove com.apple.CoreSimulator.CoreSimulatorService &> /dev/null || true', log: false)
159
+ sleep(1)
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end