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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +96 -96
  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/latest_testflight_build_number.rb +1 -1
  9. data/fastlane/lib/fastlane/actions/notarize.rb +4 -0
  10. data/fastlane/lib/fastlane/actions/onesignal.rb +1 -1
  11. data/fastlane/lib/fastlane/actions/setup_ci.rb +14 -4
  12. data/fastlane/lib/fastlane/actions/testfairy.rb +5 -2
  13. data/fastlane/lib/fastlane/actions/unlock_keychain.rb +6 -1
  14. data/fastlane/lib/fastlane/version.rb +1 -1
  15. data/fastlane/swift/Actions.swift +1 -1
  16. data/fastlane/swift/Appfile.swift +1 -1
  17. data/fastlane/swift/ArgumentProcessor.swift +1 -1
  18. data/fastlane/swift/Atomic.swift +1 -1
  19. data/fastlane/swift/ControlCommand.swift +1 -1
  20. data/fastlane/swift/Deliverfile.swift +2 -2
  21. data/fastlane/swift/DeliverfileProtocol.swift +2 -2
  22. data/fastlane/swift/Fastlane.swift +20 -10
  23. data/fastlane/swift/Gymfile.swift +2 -2
  24. data/fastlane/swift/GymfileProtocol.swift +2 -2
  25. data/fastlane/swift/LaneFileProtocol.swift +1 -1
  26. data/fastlane/swift/MainProcess.swift +1 -1
  27. data/fastlane/swift/Matchfile.swift +2 -2
  28. data/fastlane/swift/MatchfileProtocol.swift +2 -2
  29. data/fastlane/swift/OptionalConfigValue.swift +1 -1
  30. data/fastlane/swift/Plugins.swift +1 -1
  31. data/fastlane/swift/Precheckfile.swift +2 -2
  32. data/fastlane/swift/PrecheckfileProtocol.swift +2 -2
  33. data/fastlane/swift/RubyCommand.swift +1 -1
  34. data/fastlane/swift/RubyCommandable.swift +1 -1
  35. data/fastlane/swift/Runner.swift +1 -1
  36. data/fastlane/swift/RunnerArgument.swift +1 -1
  37. data/fastlane/swift/Scanfile.swift +2 -2
  38. data/fastlane/swift/ScanfileProtocol.swift +2 -2
  39. data/fastlane/swift/Screengrabfile.swift +2 -2
  40. data/fastlane/swift/ScreengrabfileProtocol.swift +2 -2
  41. data/fastlane/swift/Snapshotfile.swift +2 -2
  42. data/fastlane/swift/SnapshotfileProtocol.swift +2 -2
  43. data/fastlane/swift/SocketClient.swift +1 -1
  44. data/fastlane/swift/SocketClientDelegateProtocol.swift +1 -1
  45. data/fastlane/swift/SocketResponse.swift +1 -1
  46. data/fastlane/swift/main.swift +1 -1
  47. data/fastlane_core/lib/fastlane_core/helper.rb +6 -1
  48. data/match/lib/assets/READMETemplate.md +2 -2
  49. data/match/lib/match/generator.rb +2 -2
  50. data/match/lib/match/runner.rb +1 -1
  51. data/precheck/lib/precheck/options.rb +1 -1
  52. data/produce/lib/produce/options.rb +1 -1
  53. data/spaceship/lib/spaceship/connect_api/models/certificate.rb +1 -0
  54. data/supply/lib/supply/uploader.rb +19 -12
  55. data/trainer/lib/trainer/legacy_xcresult.rb +586 -0
  56. data/trainer/lib/trainer/options.rb +5 -0
  57. data/trainer/lib/trainer/plist_test_summary_parser.rb +84 -0
  58. data/trainer/lib/trainer/test_parser.rb +12 -293
  59. data/trainer/lib/trainer/xcresult/helper.rb +53 -0
  60. data/trainer/lib/trainer/xcresult/repetition.rb +39 -0
  61. data/trainer/lib/trainer/xcresult/test_case.rb +221 -0
  62. data/trainer/lib/trainer/xcresult/test_case_attributes.rb +49 -0
  63. data/trainer/lib/trainer/xcresult/test_plan.rb +91 -0
  64. data/trainer/lib/trainer/xcresult/test_suite.rb +134 -0
  65. data/trainer/lib/trainer/xcresult.rb +31 -388
  66. data/trainer/lib/trainer.rb +3 -1
  67. 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