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.
- checksums.yaml +4 -4
- data/README.md +98 -98
- data/cert/lib/cert/options.rb +7 -2
- data/cert/lib/cert/runner.rb +23 -11
- data/deliver/lib/deliver/options.rb +1 -1
- data/fastlane/lib/fastlane/actions/app_store_build_number.rb +1 -1
- data/fastlane/lib/fastlane/actions/docs/sync_code_signing.md +1 -1
- data/fastlane/lib/fastlane/actions/import_from_git.rb +11 -4
- data/fastlane/lib/fastlane/actions/latest_testflight_build_number.rb +1 -1
- data/fastlane/lib/fastlane/actions/notarize.rb +4 -0
- data/fastlane/lib/fastlane/actions/onesignal.rb +1 -1
- data/fastlane/lib/fastlane/actions/register_device.rb +1 -1
- data/fastlane/lib/fastlane/actions/register_devices.rb +1 -1
- data/fastlane/lib/fastlane/actions/setup_ci.rb +14 -4
- data/fastlane/lib/fastlane/actions/testfairy.rb +41 -4
- data/fastlane/lib/fastlane/actions/unlock_keychain.rb +6 -1
- data/fastlane/lib/fastlane/fast_file.rb +9 -6
- data/fastlane/lib/fastlane/version.rb +1 -1
- data/fastlane/swift/Actions.swift +1 -1
- data/fastlane/swift/Appfile.swift +1 -1
- data/fastlane/swift/ArgumentProcessor.swift +1 -1
- data/fastlane/swift/Atomic.swift +1 -1
- data/fastlane/swift/ControlCommand.swift +1 -1
- data/fastlane/swift/Deliverfile.swift +2 -2
- data/fastlane/swift/DeliverfileProtocol.swift +2 -2
- data/fastlane/swift/Fastlane.swift +39 -13
- data/fastlane/swift/Gymfile.swift +2 -2
- data/fastlane/swift/GymfileProtocol.swift +2 -2
- data/fastlane/swift/LaneFileProtocol.swift +1 -1
- data/fastlane/swift/MainProcess.swift +1 -1
- data/fastlane/swift/Matchfile.swift +2 -2
- data/fastlane/swift/MatchfileProtocol.swift +2 -2
- data/fastlane/swift/OptionalConfigValue.swift +1 -1
- data/fastlane/swift/Plugins.swift +1 -1
- data/fastlane/swift/Precheckfile.swift +2 -2
- data/fastlane/swift/PrecheckfileProtocol.swift +2 -2
- data/fastlane/swift/RubyCommand.swift +1 -1
- data/fastlane/swift/RubyCommandable.swift +1 -1
- data/fastlane/swift/Runner.swift +1 -1
- data/fastlane/swift/RunnerArgument.swift +1 -1
- data/fastlane/swift/Scanfile.swift +2 -2
- data/fastlane/swift/ScanfileProtocol.swift +2 -2
- data/fastlane/swift/Screengrabfile.swift +2 -2
- data/fastlane/swift/ScreengrabfileProtocol.swift +2 -2
- data/fastlane/swift/Snapshotfile.swift +2 -2
- data/fastlane/swift/SnapshotfileProtocol.swift +2 -2
- data/fastlane/swift/SocketClient.swift +1 -1
- data/fastlane/swift/SocketClientDelegateProtocol.swift +1 -1
- data/fastlane/swift/SocketResponse.swift +1 -1
- data/fastlane/swift/main.swift +1 -1
- data/fastlane_core/lib/fastlane_core/helper.rb +6 -1
- data/match/lib/assets/READMETemplate.md +2 -2
- data/match/lib/match/generator.rb +2 -2
- data/match/lib/match/runner.rb +1 -1
- data/precheck/lib/precheck/options.rb +1 -1
- data/produce/lib/produce/options.rb +1 -1
- data/spaceship/lib/spaceship/connect_api/models/certificate.rb +1 -0
- data/supply/lib/supply/uploader.rb +22 -11
- data/trainer/lib/trainer/legacy_xcresult.rb +586 -0
- data/trainer/lib/trainer/options.rb +5 -0
- data/trainer/lib/trainer/plist_test_summary_parser.rb +84 -0
- data/trainer/lib/trainer/test_parser.rb +12 -293
- data/trainer/lib/trainer/xcresult/helper.rb +53 -0
- data/trainer/lib/trainer/xcresult/repetition.rb +39 -0
- data/trainer/lib/trainer/xcresult/test_case.rb +221 -0
- data/trainer/lib/trainer/xcresult/test_case_attributes.rb +49 -0
- data/trainer/lib/trainer/xcresult/test_plan.rb +91 -0
- data/trainer/lib/trainer/xcresult/test_suite.rb +134 -0
- data/trainer/lib/trainer/xcresult.rb +31 -388
- data/trainer/lib/trainer.rb +3 -1
- metadata +31 -23
@@ -54,7 +54,7 @@ module Produce
|
|
54
54
|
optional: true,
|
55
55
|
default_value: "ios",
|
56
56
|
verify_block: proc do |value|
|
57
|
-
UI.user_error!("The platform can only be ios or
|
57
|
+
UI.user_error!("The platform can only be ios, osx or tvos") unless %w(ios osx tvos).include?(value)
|
58
58
|
end),
|
59
59
|
FastlaneCore::ConfigItem.new(key: :platforms,
|
60
60
|
short_option: "-J",
|
@@ -106,14 +106,14 @@ module Supply
|
|
106
106
|
end
|
107
107
|
end
|
108
108
|
|
109
|
-
def fetch_track_and_release!(track, version_code,
|
109
|
+
def fetch_track_and_release!(track, version_code, statuses = nil)
|
110
110
|
tracks = client.tracks(track)
|
111
111
|
return nil, nil if tracks.empty?
|
112
112
|
|
113
113
|
track = tracks.first
|
114
114
|
releases = track.releases
|
115
115
|
|
116
|
-
releases = releases.select { |r| r.status
|
116
|
+
releases = releases.select { |r| statuses.include?(r.status) } unless statuses.nil? || statuses.empty?
|
117
117
|
releases = releases.select { |r| (r.version_codes || []).map(&:to_s).include?(version_code.to_s) } if version_code
|
118
118
|
|
119
119
|
if releases.size > 1
|
@@ -124,7 +124,7 @@ module Supply
|
|
124
124
|
end
|
125
125
|
|
126
126
|
def update_rollout
|
127
|
-
track, release = fetch_track_and_release!(Supply.config[:track], Supply.config[:version_code], Supply::ReleaseStatus::IN_PROGRESS)
|
127
|
+
track, release = fetch_track_and_release!(Supply.config[:track], Supply.config[:version_code], [Supply::ReleaseStatus::IN_PROGRESS, Supply::ReleaseStatus::DRAFT])
|
128
128
|
UI.user_error!("Unable to find the requested track - '#{Supply.config[:track]}'") unless track
|
129
129
|
UI.user_error!("Unable to find the requested release on track - '#{Supply.config[:track]}'") unless release
|
130
130
|
|
@@ -133,12 +133,23 @@ module Supply
|
|
133
133
|
UI.message("Updating #{version_code}'s rollout to '#{Supply.config[:rollout]}' on track '#{Supply.config[:track]}'...")
|
134
134
|
|
135
135
|
if track && release
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
136
|
+
rollout = Supply.config[:rollout]
|
137
|
+
status = Supply.config[:release_status]
|
138
|
+
|
139
|
+
# If release_status not provided explicitly (and thus defaults to 'completed'), but rollout is provided with a value < 1.0, then set to 'inProgress' instead
|
140
|
+
status = Supply::ReleaseStatus::IN_PROGRESS if status == Supply::ReleaseStatus::COMPLETED && !rollout.nil? && rollout.to_f < 1
|
141
|
+
# If release_status is set to 'inProgress' but rollout is provided with a value = 1.0, then set to 'completed' instead
|
142
|
+
status = Supply::ReleaseStatus::COMPLETED if status == Supply::ReleaseStatus::IN_PROGRESS && rollout.to_f == 1
|
143
|
+
# If release_status is set to 'inProgress' but no rollout value is provided, error out
|
144
|
+
UI.user_error!("You need to provide a rollout value when release_status is set to 'inProgress'") if status == Supply::ReleaseStatus::IN_PROGRESS && rollout.nil?
|
145
|
+
release.status = status
|
146
|
+
# user_fraction is only valid for IN_PROGRESS or HALTED status
|
147
|
+
# https://googleapis.dev/ruby/google-api-client/latest/Google/Apis/AndroidpublisherV3/TrackRelease.html#user_fraction-instance_method
|
148
|
+
release.user_fraction = [Supply::ReleaseStatus::IN_PROGRESS, Supply::ReleaseStatus::HALTED].include?(release.status) ? rollout : nil
|
149
|
+
|
150
|
+
# It's okay to set releases to an array containing the newest release
|
151
|
+
# Google Play will keep previous releases there untouched
|
152
|
+
track.releases = [release]
|
142
153
|
else
|
143
154
|
UI.user_error!("Unable to find version to rollout in track '#{Supply.config[:track]}'")
|
144
155
|
end
|
@@ -209,7 +220,7 @@ module Supply
|
|
209
220
|
end
|
210
221
|
|
211
222
|
if track_to
|
212
|
-
#
|
223
|
+
# It's okay to set releases to an array containing the newest release
|
213
224
|
# Google Play will keep previous releases there this release is a partial rollout
|
214
225
|
track_to.releases = [release]
|
215
226
|
else
|
@@ -434,7 +445,7 @@ module Supply
|
|
434
445
|
tracks = client.tracks(Supply.config[:track])
|
435
446
|
track = tracks.first
|
436
447
|
if track
|
437
|
-
#
|
448
|
+
# It's okay to set releases to an array containing the newest release
|
438
449
|
# Google Play will keep previous releases there this release is a partial rollout
|
439
450
|
track.releases = [track_release]
|
440
451
|
else
|
@@ -0,0 +1,586 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
require_relative 'xcresult/helper'
|
4
|
+
|
5
|
+
module Trainer
|
6
|
+
module LegacyXCResult
|
7
|
+
# Model attributes and relationships taken from running the following command:
|
8
|
+
# xcrun xcresulttool formatDescription --legacy
|
9
|
+
|
10
|
+
class AbstractObject
|
11
|
+
attr_accessor :type
|
12
|
+
def initialize(data)
|
13
|
+
self.type = data["_type"]["_name"]
|
14
|
+
end
|
15
|
+
|
16
|
+
def fetch_value(data, key)
|
17
|
+
return (data[key] || {})["_value"]
|
18
|
+
end
|
19
|
+
|
20
|
+
def fetch_values(data, key)
|
21
|
+
return (data[key] || {})["_values"] || []
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# - ActionTestPlanRunSummaries
|
26
|
+
# * Kind: object
|
27
|
+
# * Properties:
|
28
|
+
# + summaries: [ActionTestPlanRunSummary]
|
29
|
+
class ActionTestPlanRunSummaries < AbstractObject
|
30
|
+
attr_accessor :summaries
|
31
|
+
def initialize(data)
|
32
|
+
self.summaries = fetch_values(data, "summaries").map do |summary_data|
|
33
|
+
ActionTestPlanRunSummary.new(summary_data)
|
34
|
+
end
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# - ActionAbstractTestSummary
|
40
|
+
# * Kind: object
|
41
|
+
# * Properties:
|
42
|
+
# + name: String?
|
43
|
+
class ActionAbstractTestSummary < AbstractObject
|
44
|
+
attr_accessor :name
|
45
|
+
def initialize(data)
|
46
|
+
self.name = fetch_value(data, "name")
|
47
|
+
super
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# - ActionTestPlanRunSummary
|
52
|
+
# * Supertype: ActionAbstractTestSummary
|
53
|
+
# * Kind: object
|
54
|
+
# * Properties:
|
55
|
+
# + testableSummaries: [ActionTestableSummary]
|
56
|
+
class ActionTestPlanRunSummary < ActionAbstractTestSummary
|
57
|
+
attr_accessor :testable_summaries
|
58
|
+
def initialize(data)
|
59
|
+
self.testable_summaries = fetch_values(data, "testableSummaries").map do |summary_data|
|
60
|
+
ActionTestableSummary.new(summary_data)
|
61
|
+
end
|
62
|
+
super
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# - ActionTestableSummary
|
67
|
+
# * Supertype: ActionAbstractTestSummary
|
68
|
+
# * Kind: object
|
69
|
+
# * Properties:
|
70
|
+
# + projectRelativePath: String?
|
71
|
+
# + targetName: String?
|
72
|
+
# + testKind: String?
|
73
|
+
# + tests: [ActionTestSummaryIdentifiableObject]
|
74
|
+
# + diagnosticsDirectoryName: String?
|
75
|
+
# + failureSummaries: [ActionTestFailureSummary]
|
76
|
+
# + testLanguage: String?
|
77
|
+
# + testRegion: String?
|
78
|
+
class ActionTestableSummary < ActionAbstractTestSummary
|
79
|
+
attr_accessor :project_relative_path
|
80
|
+
attr_accessor :target_name
|
81
|
+
attr_accessor :test_kind
|
82
|
+
attr_accessor :tests
|
83
|
+
def initialize(data)
|
84
|
+
self.project_relative_path = fetch_value(data, "projectRelativePath")
|
85
|
+
self.target_name = fetch_value(data, "targetName")
|
86
|
+
self.test_kind = fetch_value(data, "testKind")
|
87
|
+
self.tests = fetch_values(data, "tests").map do |tests_data|
|
88
|
+
ActionTestSummaryIdentifiableObject.create(tests_data, self)
|
89
|
+
end
|
90
|
+
super
|
91
|
+
end
|
92
|
+
|
93
|
+
def all_tests
|
94
|
+
return tests.map(&:all_subtests).flatten
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# - ActionTestSummaryIdentifiableObject
|
99
|
+
# * Supertype: ActionAbstractTestSummary
|
100
|
+
# * Kind: object
|
101
|
+
# * Properties:
|
102
|
+
# + identifier: String?
|
103
|
+
class ActionTestSummaryIdentifiableObject < ActionAbstractTestSummary
|
104
|
+
attr_accessor :identifier
|
105
|
+
attr_accessor :parent
|
106
|
+
def initialize(data, parent)
|
107
|
+
self.identifier = fetch_value(data, "identifier")
|
108
|
+
self.parent = parent
|
109
|
+
super(data)
|
110
|
+
end
|
111
|
+
|
112
|
+
def all_subtests
|
113
|
+
raise "Not overridden"
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.create(data, parent)
|
117
|
+
type = data["_type"]["_name"]
|
118
|
+
if type == "ActionTestSummaryGroup"
|
119
|
+
return ActionTestSummaryGroup.new(data, parent)
|
120
|
+
elsif type == "ActionTestMetadata"
|
121
|
+
return ActionTestMetadata.new(data, parent)
|
122
|
+
else
|
123
|
+
raise "Unsupported type: #{type}"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# - ActionTestSummaryGroup
|
129
|
+
# * Supertype: ActionTestSummaryIdentifiableObject
|
130
|
+
# * Kind: object
|
131
|
+
# * Properties:
|
132
|
+
# + duration: Double
|
133
|
+
# + subtests: [ActionTestSummaryIdentifiableObject]
|
134
|
+
class ActionTestSummaryGroup < ActionTestSummaryIdentifiableObject
|
135
|
+
attr_accessor :duration
|
136
|
+
attr_accessor :subtests
|
137
|
+
def initialize(data, parent)
|
138
|
+
self.duration = fetch_value(data, "duration").to_f
|
139
|
+
self.subtests = fetch_values(data, "subtests").map do |subtests_data|
|
140
|
+
ActionTestSummaryIdentifiableObject.create(subtests_data, self)
|
141
|
+
end
|
142
|
+
super(data, parent)
|
143
|
+
end
|
144
|
+
|
145
|
+
def all_subtests
|
146
|
+
return subtests.map(&:all_subtests).flatten
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# - ActionTestMetadata
|
151
|
+
# * Supertype: ActionTestSummaryIdentifiableObject
|
152
|
+
# * Kind: object
|
153
|
+
# * Properties:
|
154
|
+
# + testStatus: String
|
155
|
+
# + duration: Double?
|
156
|
+
# + summaryRef: Reference?
|
157
|
+
# + performanceMetricsCount: Int
|
158
|
+
# + failureSummariesCount: Int
|
159
|
+
# + activitySummariesCount: Int
|
160
|
+
class ActionTestMetadata < ActionTestSummaryIdentifiableObject
|
161
|
+
attr_accessor :test_status
|
162
|
+
attr_accessor :duration
|
163
|
+
attr_accessor :performance_metrics_count
|
164
|
+
attr_accessor :failure_summaries_count
|
165
|
+
attr_accessor :activity_summaries_count
|
166
|
+
def initialize(data, parent)
|
167
|
+
self.test_status = fetch_value(data, "testStatus")
|
168
|
+
self.duration = fetch_value(data, "duration").to_f
|
169
|
+
self.performance_metrics_count = fetch_value(data, "performanceMetricsCount")
|
170
|
+
self.failure_summaries_count = fetch_value(data, "failureSummariesCount")
|
171
|
+
self.activity_summaries_count = fetch_value(data, "activitySummariesCount")
|
172
|
+
super(data, parent)
|
173
|
+
end
|
174
|
+
|
175
|
+
def all_subtests
|
176
|
+
return [self]
|
177
|
+
end
|
178
|
+
|
179
|
+
def find_failure(failures)
|
180
|
+
sanitizer = proc { |name| name.gsub(/\W/, "_") }
|
181
|
+
sanitized_identifier = sanitizer.call(self.identifier)
|
182
|
+
if self.test_status == "Failure"
|
183
|
+
# Tries to match failure on test case name
|
184
|
+
# Example TestFailureIssueSummary:
|
185
|
+
# producingTarget: "TestThisDude"
|
186
|
+
# test_case_name: "TestThisDude.testFailureJosh2()" (when Swift)
|
187
|
+
# or "-[TestThisDudeTests testFailureJosh2]" (when Objective-C)
|
188
|
+
# Example ActionTestMetadata
|
189
|
+
# identifier: "TestThisDude/testFailureJosh2()" (when Swift)
|
190
|
+
# or identifier: "TestThisDude/testFailureJosh2" (when Objective-C)
|
191
|
+
|
192
|
+
found_failure = failures.find do |failure|
|
193
|
+
# Sanitize both test case name and identifier in a consistent fashion, then replace all non-word
|
194
|
+
# chars with underscore, and compare them
|
195
|
+
sanitized_test_case_name = sanitizer.call(failure.test_case_name)
|
196
|
+
sanitized_identifier == sanitized_test_case_name
|
197
|
+
end
|
198
|
+
return found_failure
|
199
|
+
else
|
200
|
+
return nil
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# - ActionsInvocationRecord
|
206
|
+
# * Kind: object
|
207
|
+
# * Properties:
|
208
|
+
# + metadataRef: Reference?
|
209
|
+
# + metrics: ResultMetrics
|
210
|
+
# + issues: ResultIssueSummaries
|
211
|
+
# + actions: [ActionRecord]
|
212
|
+
# + archive: ArchiveInfo?
|
213
|
+
class ActionsInvocationRecord < AbstractObject
|
214
|
+
attr_accessor :actions
|
215
|
+
attr_accessor :issues
|
216
|
+
def initialize(data)
|
217
|
+
self.actions = fetch_values(data, "actions").map do |action_data|
|
218
|
+
ActionRecord.new(action_data)
|
219
|
+
end
|
220
|
+
self.issues = ResultIssueSummaries.new(data["issues"])
|
221
|
+
super
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# - ActionRecord
|
226
|
+
# * Kind: object
|
227
|
+
# * Properties:
|
228
|
+
# + schemeCommandName: String
|
229
|
+
# + schemeTaskName: String
|
230
|
+
# + title: String?
|
231
|
+
# + startedTime: Date
|
232
|
+
# + endedTime: Date
|
233
|
+
# + runDestination: ActionRunDestinationRecord
|
234
|
+
# + buildResult: ActionResult
|
235
|
+
# + actionResult: ActionResult
|
236
|
+
class ActionRecord < AbstractObject
|
237
|
+
attr_accessor :scheme_command_name
|
238
|
+
attr_accessor :scheme_task_name
|
239
|
+
attr_accessor :title
|
240
|
+
attr_accessor :build_result
|
241
|
+
attr_accessor :action_result
|
242
|
+
def initialize(data)
|
243
|
+
self.scheme_command_name = fetch_value(data, "schemeCommandName")
|
244
|
+
self.scheme_task_name = fetch_value(data, "schemeTaskName")
|
245
|
+
self.title = fetch_value(data, "title")
|
246
|
+
self.build_result = ActionResult.new(data["buildResult"])
|
247
|
+
self.action_result = ActionResult.new(data["actionResult"])
|
248
|
+
super
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# - ActionResult
|
253
|
+
# * Kind: object
|
254
|
+
# * Properties:
|
255
|
+
# + resultName: String
|
256
|
+
# + status: String
|
257
|
+
# + metrics: ResultMetrics
|
258
|
+
# + issues: ResultIssueSummaries
|
259
|
+
# + coverage: CodeCoverageInfo
|
260
|
+
# + timelineRef: Reference?
|
261
|
+
# + logRef: Reference?
|
262
|
+
# + testsRef: Reference?
|
263
|
+
# + diagnosticsRef: Reference?
|
264
|
+
class ActionResult < AbstractObject
|
265
|
+
attr_accessor :result_name
|
266
|
+
attr_accessor :status
|
267
|
+
attr_accessor :issues
|
268
|
+
attr_accessor :timeline_ref
|
269
|
+
attr_accessor :log_ref
|
270
|
+
attr_accessor :tests_ref
|
271
|
+
attr_accessor :diagnostics_ref
|
272
|
+
def initialize(data)
|
273
|
+
self.result_name = fetch_value(data, "resultName")
|
274
|
+
self.status = fetch_value(data, "status")
|
275
|
+
self.issues = ResultIssueSummaries.new(data["issues"])
|
276
|
+
|
277
|
+
self.timeline_ref = Reference.new(data["timelineRef"]) if data["timelineRef"]
|
278
|
+
self.log_ref = Reference.new(data["logRef"]) if data["logRef"]
|
279
|
+
self.tests_ref = Reference.new(data["testsRef"]) if data["testsRef"]
|
280
|
+
self.diagnostics_ref = Reference.new(data["diagnosticsRef"]) if data["diagnosticsRef"]
|
281
|
+
super
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
# - Reference
|
286
|
+
# * Kind: object
|
287
|
+
# * Properties:
|
288
|
+
# + id: String
|
289
|
+
# + targetType: TypeDefinition?
|
290
|
+
class Reference < AbstractObject
|
291
|
+
attr_accessor :id
|
292
|
+
attr_accessor :target_type
|
293
|
+
def initialize(data)
|
294
|
+
self.id = fetch_value(data, "id")
|
295
|
+
self.target_type = TypeDefinition.new(data["targetType"]) if data["targetType"]
|
296
|
+
super
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# - TypeDefinition
|
301
|
+
# * Kind: object
|
302
|
+
# * Properties:
|
303
|
+
# + name: String
|
304
|
+
# + supertype: TypeDefinition?
|
305
|
+
class TypeDefinition < AbstractObject
|
306
|
+
attr_accessor :name
|
307
|
+
attr_accessor :supertype
|
308
|
+
def initialize(data)
|
309
|
+
self.name = fetch_value(data, "name")
|
310
|
+
self.supertype = TypeDefinition.new(data["supertype"]) if data["supertype"]
|
311
|
+
super
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# - DocumentLocation
|
316
|
+
# * Kind: object
|
317
|
+
# * Properties:
|
318
|
+
# + url: String
|
319
|
+
# + concreteTypeName: String
|
320
|
+
class DocumentLocation < AbstractObject
|
321
|
+
attr_accessor :url
|
322
|
+
attr_accessor :concrete_type_name
|
323
|
+
def initialize(data)
|
324
|
+
self.url = fetch_value(data, "url")
|
325
|
+
self.concrete_type_name = data["concreteTypeName"]["_value"]
|
326
|
+
super
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# - IssueSummary
|
331
|
+
# * Kind: object
|
332
|
+
# * Properties:
|
333
|
+
# + issueType: String
|
334
|
+
# + message: String
|
335
|
+
# + producingTarget: String?
|
336
|
+
# + documentLocationInCreatingWorkspace: DocumentLocation?
|
337
|
+
class IssueSummary < AbstractObject
|
338
|
+
attr_accessor :issue_type
|
339
|
+
attr_accessor :message
|
340
|
+
attr_accessor :producing_target
|
341
|
+
attr_accessor :document_location_in_creating_workspace
|
342
|
+
def initialize(data)
|
343
|
+
self.issue_type = fetch_value(data, "issueType")
|
344
|
+
self.message = fetch_value(data, "message")
|
345
|
+
self.producing_target = fetch_value(data, "producingTarget")
|
346
|
+
self.document_location_in_creating_workspace = DocumentLocation.new(data["documentLocationInCreatingWorkspace"]) if data["documentLocationInCreatingWorkspace"]
|
347
|
+
super
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
# - ResultIssueSummaries
|
352
|
+
# * Kind: object
|
353
|
+
# * Properties:
|
354
|
+
# + analyzerWarningSummaries: [IssueSummary]
|
355
|
+
# + errorSummaries: [IssueSummary]
|
356
|
+
# + testFailureSummaries: [TestFailureIssueSummary]
|
357
|
+
# + warningSummaries: [IssueSummary]
|
358
|
+
class ResultIssueSummaries < AbstractObject
|
359
|
+
attr_accessor :analyzer_warning_summaries
|
360
|
+
attr_accessor :error_summaries
|
361
|
+
attr_accessor :test_failure_summaries
|
362
|
+
attr_accessor :warning_summaries
|
363
|
+
def initialize(data)
|
364
|
+
self.analyzer_warning_summaries = fetch_values(data, "analyzerWarningSummaries").map do |summary_data|
|
365
|
+
IssueSummary.new(summary_data)
|
366
|
+
end
|
367
|
+
self.error_summaries = fetch_values(data, "errorSummaries").map do |summary_data|
|
368
|
+
IssueSummary.new(summary_data)
|
369
|
+
end
|
370
|
+
self.test_failure_summaries = fetch_values(data, "testFailureSummaries").map do |summary_data|
|
371
|
+
TestFailureIssueSummary.new(summary_data)
|
372
|
+
end
|
373
|
+
self.warning_summaries = fetch_values(data, "warningSummaries").map do |summary_data|
|
374
|
+
IssueSummary.new(summary_data)
|
375
|
+
end
|
376
|
+
super
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
# - TestFailureIssueSummary
|
381
|
+
# * Supertype: IssueSummary
|
382
|
+
# * Kind: object
|
383
|
+
# * Properties:
|
384
|
+
# + testCaseName: String
|
385
|
+
class TestFailureIssueSummary < IssueSummary
|
386
|
+
attr_accessor :test_case_name
|
387
|
+
def initialize(data)
|
388
|
+
self.test_case_name = fetch_value(data, "testCaseName")
|
389
|
+
super
|
390
|
+
end
|
391
|
+
|
392
|
+
def failure_message
|
393
|
+
new_message = self.message
|
394
|
+
if self.document_location_in_creating_workspace&.url
|
395
|
+
file_path = self.document_location_in_creating_workspace.url.gsub("file://", "")
|
396
|
+
new_message += " (#{file_path})"
|
397
|
+
end
|
398
|
+
|
399
|
+
return new_message
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
module Parser
|
404
|
+
class << self
|
405
|
+
def parse_xcresult(path:, output_remove_retry_attempts: false)
|
406
|
+
require 'json'
|
407
|
+
|
408
|
+
# Executes xcresulttool to get JSON format of the result bundle object
|
409
|
+
# Hotfix: From Xcode 16 beta 3 'xcresulttool get --format json' has been deprecated; '--legacy' flag required to keep on using the command
|
410
|
+
xcresulttool_cmd = generate_cmd_parse_xcresult(path)
|
411
|
+
|
412
|
+
result_bundle_object_raw = execute_cmd(xcresulttool_cmd)
|
413
|
+
result_bundle_object = JSON.parse(result_bundle_object_raw)
|
414
|
+
|
415
|
+
# Parses JSON into ActionsInvocationRecord to find a list of all ids for ActionTestPlanRunSummaries
|
416
|
+
actions_invocation_record = Trainer::LegacyXCResult::ActionsInvocationRecord.new(result_bundle_object)
|
417
|
+
test_refs = actions_invocation_record.actions.map do |action|
|
418
|
+
action.action_result.tests_ref
|
419
|
+
end.compact
|
420
|
+
ids = test_refs.map(&:id)
|
421
|
+
|
422
|
+
# Maps ids into ActionTestPlanRunSummaries by executing xcresulttool to get JSON
|
423
|
+
# containing specific information for each test summary,
|
424
|
+
summaries = ids.map do |id|
|
425
|
+
raw = execute_cmd([*xcresulttool_cmd, '--id', id])
|
426
|
+
json = JSON.parse(raw)
|
427
|
+
Trainer::LegacyXCResult::ActionTestPlanRunSummaries.new(json)
|
428
|
+
end
|
429
|
+
|
430
|
+
# Converts the ActionTestPlanRunSummaries to data for junit generator
|
431
|
+
failures = actions_invocation_record.issues.test_failure_summaries || []
|
432
|
+
summaries_to_data(summaries, failures, output_remove_retry_attempts: output_remove_retry_attempts)
|
433
|
+
end
|
434
|
+
|
435
|
+
private
|
436
|
+
|
437
|
+
def summaries_to_data(summaries, failures, output_remove_retry_attempts: false)
|
438
|
+
# Gets flat list of all ActionTestableSummary
|
439
|
+
all_summaries = summaries.map(&:summaries).flatten
|
440
|
+
testable_summaries = all_summaries.map(&:testable_summaries).flatten
|
441
|
+
|
442
|
+
summaries_to_names = test_summaries_to_configuration_names(all_summaries)
|
443
|
+
|
444
|
+
# Maps ActionTestableSummary to rows for junit generator
|
445
|
+
rows = testable_summaries.map do |testable_summary|
|
446
|
+
all_tests = testable_summary.all_tests.flatten
|
447
|
+
|
448
|
+
# Used by store number of passes and failures by identifier
|
449
|
+
# This is used when Xcode 13 (and up) retries tests
|
450
|
+
# The identifier is duplicated until test succeeds or max count is reached
|
451
|
+
tests_by_identifier = {}
|
452
|
+
|
453
|
+
test_rows = all_tests.map do |test|
|
454
|
+
identifier = "#{test.parent.name}.#{test.name}"
|
455
|
+
test_row = {
|
456
|
+
identifier: identifier,
|
457
|
+
name: test.name,
|
458
|
+
duration: test.duration,
|
459
|
+
status: test.test_status,
|
460
|
+
test_group: test.parent.name,
|
461
|
+
|
462
|
+
# These don't map to anything but keeping empty strings
|
463
|
+
guid: ""
|
464
|
+
}
|
465
|
+
|
466
|
+
info = tests_by_identifier[identifier] || {}
|
467
|
+
info[:failure_count] ||= 0
|
468
|
+
info[:skip_count] ||= 0
|
469
|
+
info[:success_count] ||= 0
|
470
|
+
|
471
|
+
retry_count = info[:retry_count]
|
472
|
+
if retry_count.nil?
|
473
|
+
retry_count = 0
|
474
|
+
else
|
475
|
+
retry_count += 1
|
476
|
+
end
|
477
|
+
info[:retry_count] = retry_count
|
478
|
+
|
479
|
+
# Set failure message if failure found
|
480
|
+
failure = test.find_failure(failures)
|
481
|
+
if failure
|
482
|
+
test_row[:failures] = [{
|
483
|
+
file_name: "",
|
484
|
+
line_number: 0,
|
485
|
+
message: "",
|
486
|
+
performance_failure: {},
|
487
|
+
failure_message: failure.failure_message
|
488
|
+
}]
|
489
|
+
|
490
|
+
info[:failure_count] += 1
|
491
|
+
elsif test.test_status == "Skipped"
|
492
|
+
test_row[:skipped] = true
|
493
|
+
info[:skip_count] += 1
|
494
|
+
else
|
495
|
+
info[:success_count] = 1
|
496
|
+
end
|
497
|
+
|
498
|
+
tests_by_identifier[identifier] = info
|
499
|
+
|
500
|
+
test_row
|
501
|
+
end
|
502
|
+
|
503
|
+
# Remove retry attempts from the count and test rows
|
504
|
+
if output_remove_retry_attempts
|
505
|
+
test_rows = test_rows.reject do |test_row|
|
506
|
+
remove = false
|
507
|
+
|
508
|
+
identifier = test_row[:identifier]
|
509
|
+
info = tests_by_identifier[identifier]
|
510
|
+
|
511
|
+
# Remove if this row is a retry and is a failure
|
512
|
+
if info[:retry_count] > 0
|
513
|
+
remove = !(test_row[:failures] || []).empty?
|
514
|
+
end
|
515
|
+
|
516
|
+
# Remove all failure and retry count if test did eventually pass
|
517
|
+
if remove
|
518
|
+
info[:failure_count] -= 1
|
519
|
+
info[:retry_count] -= 1
|
520
|
+
tests_by_identifier[identifier] = info
|
521
|
+
end
|
522
|
+
|
523
|
+
remove
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
527
|
+
row = {
|
528
|
+
project_path: testable_summary.project_relative_path,
|
529
|
+
target_name: testable_summary.target_name,
|
530
|
+
test_name: testable_summary.name,
|
531
|
+
configuration_name: summaries_to_names[testable_summary],
|
532
|
+
duration: all_tests.map(&:duration).inject(:+),
|
533
|
+
tests: test_rows
|
534
|
+
}
|
535
|
+
|
536
|
+
row[:number_of_tests] = row[:tests].count
|
537
|
+
row[:number_of_failures] = row[:tests].find_all { |a| (a[:failures] || []).count > 0 }.count
|
538
|
+
|
539
|
+
# Used for seeing if any tests continued to fail after all of the Xcode 13 (and up) retries have finished
|
540
|
+
unique_tests = tests_by_identifier.values || []
|
541
|
+
row[:number_of_tests_excluding_retries] = unique_tests.count
|
542
|
+
row[:number_of_skipped] = unique_tests.map { |a| a[:skip_count] }.inject(:+)
|
543
|
+
row[:number_of_failures_excluding_retries] = unique_tests.find_all { |a| (a[:success_count] + a[:skip_count]) == 0 }.count
|
544
|
+
row[:number_of_retries] = unique_tests.map { |a| a[:retry_count] }.inject(:+)
|
545
|
+
|
546
|
+
row
|
547
|
+
end
|
548
|
+
|
549
|
+
rows
|
550
|
+
end
|
551
|
+
|
552
|
+
def test_summaries_to_configuration_names(test_summaries)
|
553
|
+
summary_to_name = {}
|
554
|
+
test_summaries.each do |summary|
|
555
|
+
summary.testable_summaries.each do |testable_summary|
|
556
|
+
summary_to_name[testable_summary] = summary.name
|
557
|
+
end
|
558
|
+
end
|
559
|
+
summary_to_name
|
560
|
+
end
|
561
|
+
|
562
|
+
def generate_cmd_parse_xcresult(path)
|
563
|
+
xcresulttool_cmd = [
|
564
|
+
'xcrun',
|
565
|
+
'xcresulttool',
|
566
|
+
'get',
|
567
|
+
'--format',
|
568
|
+
'json',
|
569
|
+
'--path',
|
570
|
+
path
|
571
|
+
]
|
572
|
+
|
573
|
+
xcresulttool_cmd << '--legacy' if Trainer::XCResult::Helper.supports_xcode16_xcresulttool?
|
574
|
+
|
575
|
+
xcresulttool_cmd
|
576
|
+
end
|
577
|
+
|
578
|
+
def execute_cmd(cmd)
|
579
|
+
output, status = Open3.capture2e(*cmd)
|
580
|
+
raise "Failed to execute '#{cmd}': #{output}" unless status.success?
|
581
|
+
return output
|
582
|
+
end
|
583
|
+
end
|
584
|
+
end
|
585
|
+
end
|
586
|
+
end
|
@@ -50,6 +50,11 @@ module Trainer
|
|
50
50
|
description: "Produces class name and test name identical to xcpretty naming in junit file",
|
51
51
|
is_string: false,
|
52
52
|
default_value: false),
|
53
|
+
FastlaneCore::ConfigItem.new(key: :force_legacy_xcresulttool,
|
54
|
+
env_name: "TRAINER_FORCE_LEGACY_XCRESULTTOOL",
|
55
|
+
description: "Force the use of the '--legacy' flag for xcresulttool instead of using the new commands",
|
56
|
+
type: Boolean,
|
57
|
+
default_value: false),
|
53
58
|
FastlaneCore::ConfigItem.new(key: :silent,
|
54
59
|
env_name: "TRAINER_SILENT",
|
55
60
|
description: "Silences all output",
|