fastlane 2.225.0 → 2.227.0

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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +98 -98
  3. data/cert/lib/cert/options.rb +7 -2
  4. data/cert/lib/cert/runner.rb +23 -11
  5. data/deliver/lib/deliver/options.rb +1 -1
  6. data/fastlane/lib/fastlane/actions/app_store_build_number.rb +1 -1
  7. data/fastlane/lib/fastlane/actions/docs/sync_code_signing.md +1 -1
  8. data/fastlane/lib/fastlane/actions/import_from_git.rb +11 -4
  9. data/fastlane/lib/fastlane/actions/latest_testflight_build_number.rb +1 -1
  10. data/fastlane/lib/fastlane/actions/notarize.rb +4 -0
  11. data/fastlane/lib/fastlane/actions/onesignal.rb +1 -1
  12. data/fastlane/lib/fastlane/actions/register_device.rb +1 -1
  13. data/fastlane/lib/fastlane/actions/register_devices.rb +1 -1
  14. data/fastlane/lib/fastlane/actions/setup_ci.rb +14 -4
  15. data/fastlane/lib/fastlane/actions/testfairy.rb +41 -4
  16. data/fastlane/lib/fastlane/actions/unlock_keychain.rb +6 -1
  17. data/fastlane/lib/fastlane/fast_file.rb +9 -6
  18. data/fastlane/lib/fastlane/version.rb +1 -1
  19. data/fastlane/swift/Actions.swift +1 -1
  20. data/fastlane/swift/Appfile.swift +1 -1
  21. data/fastlane/swift/ArgumentProcessor.swift +1 -1
  22. data/fastlane/swift/Atomic.swift +1 -1
  23. data/fastlane/swift/ControlCommand.swift +1 -1
  24. data/fastlane/swift/Deliverfile.swift +2 -2
  25. data/fastlane/swift/DeliverfileProtocol.swift +2 -2
  26. data/fastlane/swift/Fastlane.swift +39 -13
  27. data/fastlane/swift/Gymfile.swift +2 -2
  28. data/fastlane/swift/GymfileProtocol.swift +2 -2
  29. data/fastlane/swift/LaneFileProtocol.swift +1 -1
  30. data/fastlane/swift/MainProcess.swift +1 -1
  31. data/fastlane/swift/Matchfile.swift +2 -2
  32. data/fastlane/swift/MatchfileProtocol.swift +2 -2
  33. data/fastlane/swift/OptionalConfigValue.swift +1 -1
  34. data/fastlane/swift/Plugins.swift +1 -1
  35. data/fastlane/swift/Precheckfile.swift +2 -2
  36. data/fastlane/swift/PrecheckfileProtocol.swift +2 -2
  37. data/fastlane/swift/RubyCommand.swift +1 -1
  38. data/fastlane/swift/RubyCommandable.swift +1 -1
  39. data/fastlane/swift/Runner.swift +1 -1
  40. data/fastlane/swift/RunnerArgument.swift +1 -1
  41. data/fastlane/swift/Scanfile.swift +2 -2
  42. data/fastlane/swift/ScanfileProtocol.swift +2 -2
  43. data/fastlane/swift/Screengrabfile.swift +2 -2
  44. data/fastlane/swift/ScreengrabfileProtocol.swift +2 -2
  45. data/fastlane/swift/Snapshotfile.swift +2 -2
  46. data/fastlane/swift/SnapshotfileProtocol.swift +2 -2
  47. data/fastlane/swift/SocketClient.swift +1 -1
  48. data/fastlane/swift/SocketClientDelegateProtocol.swift +1 -1
  49. data/fastlane/swift/SocketResponse.swift +1 -1
  50. data/fastlane/swift/main.swift +1 -1
  51. data/fastlane_core/lib/fastlane_core/helper.rb +6 -1
  52. data/match/lib/assets/READMETemplate.md +2 -2
  53. data/match/lib/match/generator.rb +2 -2
  54. data/match/lib/match/runner.rb +1 -1
  55. data/precheck/lib/precheck/options.rb +1 -1
  56. data/produce/lib/produce/options.rb +1 -1
  57. data/spaceship/lib/spaceship/connect_api/models/certificate.rb +1 -0
  58. data/supply/lib/supply/uploader.rb +22 -11
  59. data/trainer/lib/trainer/legacy_xcresult.rb +586 -0
  60. data/trainer/lib/trainer/options.rb +5 -0
  61. data/trainer/lib/trainer/plist_test_summary_parser.rb +84 -0
  62. data/trainer/lib/trainer/test_parser.rb +12 -293
  63. data/trainer/lib/trainer/xcresult/helper.rb +53 -0
  64. data/trainer/lib/trainer/xcresult/repetition.rb +39 -0
  65. data/trainer/lib/trainer/xcresult/test_case.rb +221 -0
  66. data/trainer/lib/trainer/xcresult/test_case_attributes.rb +49 -0
  67. data/trainer/lib/trainer/xcresult/test_plan.rb +91 -0
  68. data/trainer/lib/trainer/xcresult/test_suite.rb +134 -0
  69. data/trainer/lib/trainer/xcresult.rb +31 -388
  70. data/trainer/lib/trainer.rb +3 -1
  71. metadata +31 -23
@@ -0,0 +1,221 @@
1
+ require_relative 'helper'
2
+ require_relative 'test_case_attributes'
3
+ require_relative 'repetition'
4
+
5
+ module Trainer
6
+ module XCResult
7
+ # Represents a test case, including its retries (aka repetitions)
8
+ class TestCase
9
+ include TestCaseAttributes
10
+
11
+ attr_reader :name
12
+ attr_reader :identifier
13
+ attr_reader :duration
14
+ attr_reader :result
15
+ attr_reader :classname
16
+ attr_reader :argument
17
+ # @return [Array<Repetition>] Array of retry attempts for this test case, **including the initial attempt**
18
+ # This will be `nil` if the test case was not run multiple times, but will contain all repetitions if it was run more than once.
19
+ attr_reader :retries
20
+ attr_reader :failure_messages
21
+ attr_reader :source_references
22
+ attr_reader :attachments
23
+ attr_reader :tags
24
+
25
+ def initialize(
26
+ name:, identifier:, duration:, result:, classname:, argument: nil, tags: [], retries: nil,
27
+ failure_messages: [], source_references: [], attachments: []
28
+ )
29
+ @name = name
30
+ @identifier = identifier
31
+ @duration = duration
32
+ @result = result
33
+ @classname = classname
34
+ @argument = argument
35
+ @tags = tags
36
+ @retries = retries
37
+ @failure_messages = failure_messages
38
+ @source_references = source_references
39
+ @attachments = attachments
40
+ end
41
+
42
+ def self.from_json(node:)
43
+ # Handle test case arguments
44
+ argument_nodes = Helper.find_json_children(node, 'Arguments')
45
+ argument_nodes = [nil] if argument_nodes.empty?
46
+
47
+ # Generate test cases for each argument
48
+ argument_nodes.map do |arg_node|
49
+ # For repetition nodes, failure messages, source refs, attachments and result attributes,
50
+ # Search them as children of the argument child node if present, of the test case node otherwise.
51
+ node_for_attributes = arg_node || node
52
+
53
+ retries = Helper.find_json_children(node_for_attributes, 'Repetition', 'Test Case Run')
54
+ &.map { |rep_node| Repetition.from_json(node: rep_node) } || []
55
+
56
+ failure_messages = if retries.empty?
57
+ extract_failure_messages(node_for_attributes)
58
+ else
59
+ retries.flat_map(&:failure_messages)
60
+ end
61
+
62
+ source_references = if retries.empty?
63
+ extract_source_references(node_for_attributes)
64
+ else
65
+ retries.flat_map(&:source_references)
66
+ end
67
+
68
+ attachments = if retries.empty?
69
+ extract_attachments(node_for_attributes)
70
+ else
71
+ retries.flat_map(&:attachments)
72
+ end
73
+
74
+ new(
75
+ name: node['name'],
76
+ identifier: node['nodeIdentifier'],
77
+ duration: parse_duration(node['duration']),
78
+ result: node_for_attributes['result'],
79
+ classname: extract_classname(node),
80
+ argument: arg_node&.[]('name'), # Only set if there is an argument
81
+ tags: node['tags'] || [],
82
+ retries: retries,
83
+ failure_messages: failure_messages,
84
+ source_references: source_references,
85
+ attachments: attachments
86
+ )
87
+ end
88
+ end
89
+
90
+ # Generates XML nodes for the test case
91
+ #
92
+ # @return [Array<REXML::Element>] An array of XML <testcase> elements
93
+ #
94
+ # - If no retries, the array contains a single <testcase> element
95
+ # - If retries, the array contains one <testcase> element per retry
96
+ def to_xml_nodes
97
+ runs = @retries.nil? || @retries.empty? ? [nil] : @retries
98
+
99
+ runs.map do |run|
100
+ Helper.create_xml_element('testcase',
101
+ name: if @argument.nil?
102
+ @name
103
+ else
104
+ @name.match?(/(\(.*\))/) ? @name.gsub(/(\(.*\))/, "(#{@argument})") : "#{@name} (#{@argument})"
105
+ end,
106
+ classname: @classname,
107
+ time: (run || self).duration.to_s).tap do |testcase|
108
+ add_xml_result_elements(testcase, run || self)
109
+ add_properties_to_xml(testcase, repetition_name: run&.name)
110
+ end
111
+ end
112
+ end
113
+
114
+ def retries_count
115
+ @retries&.count || 0
116
+ end
117
+
118
+ def total_tests_count
119
+ retries_count > 0 ? retries_count : 1
120
+ end
121
+
122
+ def total_failures_count
123
+ if retries_count > 0
124
+ @retries.count(&:failed?)
125
+ elsif failed?
126
+ 1
127
+ else
128
+ 0
129
+ end
130
+ end
131
+
132
+ def self.extract_classname(node)
133
+ return nil if node['nodeIdentifier'].nil?
134
+
135
+ parts = node['nodeIdentifier'].split('/')
136
+ parts[0...-1].join('.')
137
+ end
138
+ private_class_method :extract_classname
139
+
140
+ private
141
+
142
+ # Adds <properties> element to the XML <testcase> element
143
+ #
144
+ # @param testcase [REXML::Element] The XML testcase element to add properties to
145
+ # @param repetition_name [String, nil] Name of the retry attempt, if this is a retry
146
+ #
147
+ # Properties added:
148
+ # - if argument is present:
149
+ # - `testname`: Raw test name (as in such case, <testcase name="…"> would contain a mix of the test name and the argument)
150
+ # - `argument`: Test argument value
151
+ # - `repetitionN`: Name of the retry attempt if present
152
+ # - `source_referenceN`: Source code references (file/line) for failures
153
+ # - `attachmentN`: Test attachments like screenshots
154
+ # - `tagN`: Test tags/categories
155
+ #
156
+ # <properties> element is only added to the XML if at least one property exists
157
+ def add_properties_to_xml(testcase, repetition_name: nil)
158
+ properties = Helper.create_xml_element('properties')
159
+
160
+ # Add argument as property
161
+ if @argument
162
+ name_prop = Helper.create_xml_element('property', name: "testname", value: @name)
163
+ properties.add_element(name_prop)
164
+ prop = Helper.create_xml_element('property', name: "argument", value: @argument)
165
+ properties.add_element(prop)
166
+ end
167
+
168
+ # Add repetition as property
169
+ if repetition_name
170
+ prop = Helper.create_xml_element('property', name: "repetition", value: repetition_name)
171
+ properties.add_element(prop)
172
+ end
173
+
174
+ # Add source references as properties
175
+ (@source_references || []).each_with_index do |ref, index|
176
+ prop = Helper.create_xml_element('property', name: "source_reference#{index + 1}", value: ref)
177
+ properties.add_element(prop)
178
+ end
179
+
180
+ # Add attachments as properties
181
+ (@attachments || []).each_with_index do |attachment, index|
182
+ prop = Helper.create_xml_element('property', name: "attachment#{index + 1}", value: attachment)
183
+ properties.add_element(prop)
184
+ end
185
+
186
+ # Add tags as properties
187
+ (@tags || []).sort.each_with_index do |tag, index|
188
+ prop = Helper.create_xml_element('property', name: "tag#{index + 1}", value: tag)
189
+ properties.add_element(prop)
190
+ end
191
+
192
+ # Only add properties to testcase if it has child elements
193
+ testcase.add_element(properties) if properties.elements.any?
194
+ end
195
+
196
+ # Adds <failure> and <skipped> elements to the XML <testcase> element based on test status
197
+ #
198
+ # @param testcase [REXML::Element] The XML testcase element to add result elements to
199
+ # @param test_obj [Repetition, TestCase] Object representing the test result
200
+ # This can be either a Repetition object or the TestCase itself.
201
+ # Must respond to the following methods:
202
+ # - failed? [Boolean]: Indicates if the test failed
203
+ # - skipped? [Boolean]: Indicates if the test was skipped
204
+ # - failure_messages [Array<String>, nil]: List of failure messages (optional)
205
+ #
206
+ # Adds:
207
+ # - <failure> elements with messages for failed tests
208
+ # - <skipped> element for skipped tests
209
+ # - No elements added for passed tests
210
+ def add_xml_result_elements(testcase, test_obj)
211
+ if test_obj.failed?
212
+ (test_obj.failure_messages&.any? ? test_obj.failure_messages : [nil]).each do |msg|
213
+ testcase.add_element(Helper.create_xml_element('failure', message: msg))
214
+ end
215
+ elsif test_obj.skipped?
216
+ testcase.add_element(Helper.create_xml_element('skipped', message: test_obj.failure_messages&.first))
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,49 @@
1
+ module Trainer
2
+ module XCResult
3
+ # Mixin for shared attributes between TestCase and Repetition
4
+ module TestCaseAttributes
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ def passed?
10
+ @result == 'Passed'
11
+ end
12
+
13
+ def failed?
14
+ @result == 'Failed'
15
+ end
16
+
17
+ def skipped?
18
+ @result == 'Skipped'
19
+ end
20
+
21
+ module ClassMethods
22
+ def parse_duration(duration_str)
23
+ return 0.0 if duration_str.nil?
24
+
25
+ # Handle comma-separated duration, and remove 's' suffix
26
+ duration_str.gsub(',', '.').chomp('s').to_f
27
+ end
28
+
29
+ def extract_failure_messages(node)
30
+ node['children']
31
+ &.select { |child| child['nodeType'] == 'Failure Message' }
32
+ &.map { |msg| msg['name'] } || []
33
+ end
34
+
35
+ def extract_source_references(node)
36
+ node['children']
37
+ &.select { |child| child['nodeType'] == 'Source Code Reference' }
38
+ &.map { |ref| ref['name'] } || []
39
+ end
40
+
41
+ def extract_attachments(node)
42
+ node['children']
43
+ &.select { |child| child['nodeType'] == 'Attachment' }
44
+ &.map { |attachment| attachment['name'] } || []
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,91 @@
1
+ require_relative 'test_suite'
2
+
3
+ module Trainer
4
+ module XCResult
5
+ # Represents a collection of test suites + the configuration, and device used to run them
6
+ class TestPlan
7
+ attr_reader :test_suites, :name, :configurations, :devices
8
+ attr_accessor :output_remove_retry_attempts
9
+
10
+ def initialize(test_suites:, configurations: [], devices: [], output_remove_retry_attempts: false)
11
+ @test_suites = test_suites
12
+ @configurations = configurations
13
+ @devices = devices
14
+ @output_remove_retry_attempts = output_remove_retry_attempts
15
+ end
16
+
17
+ def self.from_json(json:)
18
+ # Extract configurations and devices
19
+ configurations = json['testPlanConfigurations'] || []
20
+ devices = json['devices'] || []
21
+
22
+ # Find the test plan node (root of test results)
23
+ test_plan_node = json['testNodes']&.find { |node| node['nodeType'] == 'Test Plan' }
24
+ return new(test_suites: []) if test_plan_node.nil?
25
+
26
+ # Convert test plan node's children (test bundles) to TestSuite objects
27
+ test_suites = test_plan_node['children']&.map do |test_bundle|
28
+ TestSuite.from_json(
29
+ node: test_bundle
30
+ )
31
+ end || []
32
+
33
+ new(
34
+ test_suites: test_suites,
35
+ configurations: configurations,
36
+ devices: devices
37
+ )
38
+ end
39
+
40
+ # Allows iteration over test suites. Used by TestParser to collect test results
41
+ include Enumerable
42
+ def each(&block)
43
+ test_suites.map(&:to_hash).each(&block)
44
+ end
45
+
46
+ # Generates a JUnit-compatible XML representation of the test plan
47
+ # See https://github.com/testmoapp/junitxml/
48
+ def to_xml
49
+ # Create the root testsuites element with calculated summary attributes
50
+ testsuites = Helper.create_xml_element('testsuites',
51
+ tests: test_suites.sum(&:test_cases_count).to_s,
52
+ failures: test_suites.sum(&:failures_count).to_s,
53
+ skipped: test_suites.sum(&:skipped_count).to_s,
54
+ time: test_suites.sum(&:duration).to_s)
55
+
56
+ # Create <properties> node for configuration and device, to be applied to each suite node
57
+ properties = Helper.create_xml_element('properties').tap do |node|
58
+ @configurations.each do |config|
59
+ config_prop = Helper.create_xml_element('property', name: 'Configuration', value: config['configurationName'])
60
+ node.add_element(config_prop)
61
+ end
62
+
63
+ @devices.each do |device|
64
+ device_prop = Helper.create_xml_element('property', name: 'device', value: "#{device.fetch('modelName', 'Unknown Device')} (#{device.fetch('osVersion', 'Unknown OS Version')})")
65
+ node.add_element(device_prop)
66
+ end
67
+ end
68
+
69
+ # Add each test suite to the root
70
+ test_suites.each do |suite|
71
+ suite_node = suite.to_xml(output_remove_retry_attempts: output_remove_retry_attempts)
72
+ # In JUnit conventions, the <testsuites> root element can't have properties
73
+ # So we add the <properties> node to each child <testsuite> node instead
74
+ suite_node.add_element(properties.dup) if properties.elements.any?
75
+ testsuites.add_element(suite_node)
76
+ end
77
+
78
+ # Convert to XML string with prologue
79
+ doc = REXML::Document.new
80
+ doc << REXML::XMLDecl.new('1.0', 'UTF-8')
81
+
82
+ doc.add(testsuites)
83
+
84
+ formatter = REXML::Formatters::Pretty.new
85
+ output = String.new
86
+ formatter.write(doc, output)
87
+ output
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,134 @@
1
+ require_relative 'test_case'
2
+
3
+ module Trainer
4
+ module XCResult
5
+ # Represents a test suite, including its test cases and sub-suites
6
+ class TestSuite
7
+ attr_reader :name
8
+ attr_reader :identifier
9
+ attr_reader :type
10
+ attr_reader :result
11
+ attr_reader :test_cases
12
+ attr_reader :sub_suites
13
+ attr_reader :tags
14
+
15
+ def initialize(name:, identifier:, type:, result:, tags: [], test_cases: [], sub_suites: [])
16
+ @name = name
17
+ @identifier = identifier
18
+ @type = type
19
+ @result = result
20
+ @tags = tags
21
+ @test_cases = test_cases
22
+ @sub_suites = sub_suites
23
+ end
24
+
25
+ def self.from_json(node:)
26
+ # Create initial TestSuite with basic attributes
27
+ test_suite = new(
28
+ name: node['name'],
29
+ identifier: node['nodeIdentifier'],
30
+ type: node['nodeType'],
31
+ result: node['result'],
32
+ tags: node['tags'] || []
33
+ )
34
+
35
+ # Process children to populate test_cases and sub_suites
36
+ test_suite.process_children(node['children'] || [])
37
+
38
+ test_suite
39
+ end
40
+
41
+ def passed?
42
+ @result == 'Passed'
43
+ end
44
+
45
+ def failed?
46
+ @result == 'Failed'
47
+ end
48
+
49
+ def skipped?
50
+ @result == 'Skipped'
51
+ end
52
+
53
+ def duration
54
+ @duration ||= @test_cases.sum(&:duration) + @sub_suites.sum(&:duration)
55
+ end
56
+
57
+ def test_cases_count
58
+ @test_cases_count ||= @test_cases.count + @sub_suites.sum(&:test_cases_count)
59
+ end
60
+
61
+ def failures_count
62
+ @failures_count ||= @test_cases.count(&:failed?) + @sub_suites.sum(&:failures_count)
63
+ end
64
+
65
+ def skipped_count
66
+ @skipped_count ||= @test_cases.count(&:skipped?) + @sub_suites.sum(&:skipped_count)
67
+ end
68
+
69
+ def total_tests_count
70
+ @test_cases.sum(&:total_tests_count) +
71
+ @sub_suites.sum(&:total_tests_count)
72
+ end
73
+
74
+ def total_failures_count
75
+ @test_cases.sum(&:total_failures_count) +
76
+ @sub_suites.sum(&:total_failures_count)
77
+ end
78
+
79
+ def total_retries_count
80
+ @test_cases.sum(&:retries_count) +
81
+ @sub_suites.sum(&:total_retries_count)
82
+ end
83
+
84
+ # Hash representation used by TestParser to collect test results
85
+ def to_hash
86
+ {
87
+ number_of_tests: total_tests_count,
88
+ number_of_failures: total_failures_count,
89
+ number_of_tests_excluding_retries: test_cases_count,
90
+ number_of_failures_excluding_retries: failures_count,
91
+ number_of_retries: total_retries_count,
92
+ number_of_skipped: skipped_count
93
+ }
94
+ end
95
+
96
+ # Generates a JUnit-compatible XML representation of the test suite
97
+ # See https://github.com/testmoapp/junitxml/
98
+ def to_xml(output_remove_retry_attempts: false)
99
+ testsuite = Helper.create_xml_element('testsuite',
100
+ name: @name,
101
+ time: duration.to_s,
102
+ tests: test_cases_count.to_s,
103
+ failures: failures_count.to_s,
104
+ skipped: skipped_count.to_s)
105
+
106
+ # Add test cases
107
+ @test_cases.each do |test_case|
108
+ runs = test_case.to_xml_nodes
109
+ runs = runs.last(1) if output_remove_retry_attempts
110
+ runs.each { |node| testsuite.add_element(node) }
111
+ end
112
+
113
+ # Add sub-suites
114
+ @sub_suites.each do |sub_suite|
115
+ testsuite.add_element(sub_suite.to_xml(output_remove_retry_attempts: output_remove_retry_attempts))
116
+ end
117
+
118
+ testsuite
119
+ end
120
+
121
+ def process_children(children)
122
+ children.each do |child|
123
+ case child['nodeType']
124
+ when 'Test Case'
125
+ # Use from_json to generate multiple test cases if needed
126
+ @test_cases.concat(TestCase.from_json(node: child))
127
+ when 'Test Suite', 'Unit test bundle', 'UI test bundle'
128
+ @sub_suites << TestSuite.from_json(node: child)
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end