fastlane 2.226.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 +96 -96
- 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/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/setup_ci.rb +14 -4
- data/fastlane/lib/fastlane/actions/testfairy.rb +5 -2
- data/fastlane/lib/fastlane/actions/unlock_keychain.rb +6 -1
- 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 +20 -10
- 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 +19 -12
- 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 +29 -21
@@ -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",
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'plist'
|
2
|
+
|
3
|
+
module Trainer
|
4
|
+
module PlistTestSummaryParser
|
5
|
+
class << self
|
6
|
+
def parse_content(raw_json, xcpretty_naming)
|
7
|
+
data = raw_json["TestableSummaries"].collect do |testable_summary|
|
8
|
+
summary_row = {
|
9
|
+
project_path: testable_summary["ProjectPath"],
|
10
|
+
target_name: testable_summary["TargetName"],
|
11
|
+
test_name: testable_summary["TestName"],
|
12
|
+
duration: testable_summary["Tests"].map { |current_test| current_test["Duration"] }.inject(:+),
|
13
|
+
tests: unfold_tests(testable_summary["Tests"]).collect do |current_test|
|
14
|
+
test_group, test_name = test_group_and_name(testable_summary, current_test, xcpretty_naming)
|
15
|
+
current_row = {
|
16
|
+
identifier: current_test["TestIdentifier"],
|
17
|
+
test_group: test_group,
|
18
|
+
name: test_name,
|
19
|
+
object_class: current_test["TestObjectClass"],
|
20
|
+
status: current_test["TestStatus"],
|
21
|
+
guid: current_test["TestSummaryGUID"],
|
22
|
+
duration: current_test["Duration"]
|
23
|
+
}
|
24
|
+
if current_test["FailureSummaries"]
|
25
|
+
current_row[:failures] = current_test["FailureSummaries"].collect do |current_failure|
|
26
|
+
{
|
27
|
+
file_name: current_failure['FileName'],
|
28
|
+
line_number: current_failure['LineNumber'],
|
29
|
+
message: current_failure['Message'],
|
30
|
+
performance_failure: current_failure['PerformanceFailure'],
|
31
|
+
failure_message: "#{current_failure['Message']} (#{current_failure['FileName']}:#{current_failure['LineNumber']})"
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
current_row
|
36
|
+
end
|
37
|
+
}
|
38
|
+
summary_row[:number_of_tests] = summary_row[:tests].count
|
39
|
+
summary_row[:number_of_failures] = summary_row[:tests].find_all { |a| (a[:failures] || []).count > 0 }.count
|
40
|
+
|
41
|
+
# Makes sure that plist support matches data output of xcresult
|
42
|
+
summary_row[:number_of_tests_excluding_retries] = summary_row[:number_of_tests]
|
43
|
+
summary_row[:number_of_failures_excluding_retries] = summary_row[:number_of_failures]
|
44
|
+
summary_row[:number_of_retries] = 0
|
45
|
+
|
46
|
+
summary_row
|
47
|
+
end
|
48
|
+
data
|
49
|
+
end
|
50
|
+
|
51
|
+
def ensure_file_valid!(raw_json)
|
52
|
+
format_version = raw_json["FormatVersion"]
|
53
|
+
supported_versions = ["1.1", "1.2"]
|
54
|
+
raise "Format version '#{format_version}' is not supported, must be #{supported_versions.join(', ')}" unless supported_versions.include?(format_version)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def unfold_tests(data)
|
60
|
+
tests = []
|
61
|
+
data.each do |current_hash|
|
62
|
+
if current_hash["Subtests"]
|
63
|
+
tests += unfold_tests(current_hash["Subtests"])
|
64
|
+
end
|
65
|
+
if current_hash["TestStatus"]
|
66
|
+
tests << current_hash
|
67
|
+
end
|
68
|
+
end
|
69
|
+
return tests
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_group_and_name(testable_summary, test, xcpretty_naming)
|
73
|
+
if xcpretty_naming
|
74
|
+
group = testable_summary["TargetName"] + "." + test["TestIdentifier"].split("/")[0..-2].join(".")
|
75
|
+
name = test["TestName"][0..-3]
|
76
|
+
else
|
77
|
+
group = test["TestIdentifier"].split("/")[0..-2].join(".")
|
78
|
+
name = test["TestName"]
|
79
|
+
end
|
80
|
+
return group, name
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|