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,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
@@ -2,18 +2,17 @@ require 'plist'
2
2
 
3
3
  require 'fastlane_core/print_table'
4
4
 
5
- require_relative 'junit_generator'
6
5
  require_relative 'xcresult'
6
+ require_relative 'xcresult/helper'
7
+ require_relative 'junit_generator'
8
+ require_relative 'legacy_xcresult'
9
+ require_relative 'plist_test_summary_parser'
7
10
  require_relative 'module'
8
11
 
9
12
  module Trainer
10
13
  class TestParser
11
14
  attr_accessor :data
12
15
 
13
- attr_accessor :file_content
14
-
15
- attr_accessor :raw_json
16
-
17
16
  attr_accessor :number_of_tests
18
17
  attr_accessor :number_of_failures
19
18
  attr_accessor :number_of_tests_excluding_retries
@@ -102,15 +101,16 @@ module Trainer
102
101
  UI.user_error!("File not found at path '#{path}'") unless File.exist?(path)
103
102
 
104
103
  if File.directory?(path) && path.end_with?(".xcresult")
105
- parse_xcresult(path, output_remove_retry_attempts: config[:output_remove_retry_attempts])
104
+ parser = XCResult::Helper.supports_xcode16_xcresulttool? && !config[:force_legacy_xcresulttool] ? XCResult::Parser : LegacyXCResult::Parser
105
+ self.data = parser.parse_xcresult(path: path, output_remove_retry_attempts: config[:output_remove_retry_attempts])
106
106
  else
107
- self.file_content = File.read(path)
108
- self.raw_json = Plist.parse_xml(self.file_content)
107
+ file_content = File.read(path)
108
+ raw_json = Plist.parse_xml(file_content)
109
109
 
110
- return if self.raw_json["FormatVersion"].to_s.length.zero? # maybe that's a useless plist file
110
+ return if raw_json["FormatVersion"].to_s.length.zero? # maybe that's a useless plist file
111
111
 
112
- ensure_file_valid!
113
- parse_content(config[:xcpretty_naming])
112
+ PlistTestSummaryParser.ensure_file_valid!(raw_json)
113
+ self.data = PlistTestSummaryParser.parse_content(raw_json, config[:xcpretty_naming])
114
114
  end
115
115
 
116
116
  self.number_of_tests = 0
@@ -131,293 +131,12 @@ module Trainer
131
131
 
132
132
  # Returns the JUnit report as String
133
133
  def to_junit
134
- JunitGenerator.new(self.data).generate
134
+ self.data.kind_of?(Trainer::XCResult::TestPlan) ? self.data.to_xml : JunitGenerator.new(self.data).generate
135
135
  end
136
136
 
137
137
  # @return [Bool] were all tests successful? Is false if at least one test failed
138
138
  def tests_successful?
139
139
  self.data.collect { |a| a[:number_of_failures_excluding_retries] }.all?(&:zero?)
140
140
  end
141
-
142
- private
143
-
144
- def ensure_file_valid!
145
- format_version = self.raw_json["FormatVersion"]
146
- supported_versions = ["1.1", "1.2"]
147
- UI.user_error!("Format version '#{format_version}' is not supported, must be #{supported_versions.join(', ')}") unless supported_versions.include?(format_version)
148
- end
149
-
150
- # Converts the raw plist test structure into something that's easier to enumerate
151
- def unfold_tests(data)
152
- # `data` looks like this
153
- # => [{"Subtests"=>
154
- # [{"Subtests"=>
155
- # [{"Subtests"=>
156
- # [{"Duration"=>0.4,
157
- # "TestIdentifier"=>"Unit/testExample()",
158
- # "TestName"=>"testExample()",
159
- # "TestObjectClass"=>"IDESchemeActionTestSummary",
160
- # "TestStatus"=>"Success",
161
- # "TestSummaryGUID"=>"4A24BFED-03E6-4FBE-BC5E-2D80023C06B4"},
162
- # {"FailureSummaries"=>
163
- # [{"FileName"=>"/Users/krausefx/Developer/themoji/Unit/Unit.swift",
164
- # "LineNumber"=>34,
165
- # "Message"=>"XCTAssertTrue failed - ",
166
- # "PerformanceFailure"=>false}],
167
- # "TestIdentifier"=>"Unit/testExample2()",
168
-
169
- tests = []
170
- data.each do |current_hash|
171
- if current_hash["Subtests"]
172
- tests += unfold_tests(current_hash["Subtests"])
173
- end
174
- if current_hash["TestStatus"]
175
- tests << current_hash
176
- end
177
- end
178
- return tests
179
- end
180
-
181
- # Returns the test group and test name from the passed summary and test
182
- # Pass xcpretty_naming = true to get the test naming aligned with xcpretty
183
- def test_group_and_name(testable_summary, test, xcpretty_naming)
184
- if xcpretty_naming
185
- group = testable_summary["TargetName"] + "." + test["TestIdentifier"].split("/")[0..-2].join(".")
186
- name = test["TestName"][0..-3]
187
- else
188
- group = test["TestIdentifier"].split("/")[0..-2].join(".")
189
- name = test["TestName"]
190
- end
191
- return group, name
192
- end
193
-
194
- def execute_cmd(cmd)
195
- output = `#{cmd}`
196
- raise "Failed to execute - #{cmd}" unless $?.success?
197
- return output
198
- end
199
-
200
- # Hotfix: From Xcode 16 beta 3 'xcresulttool get --format json' has been deprecated;
201
- # '--legacy' flag required to keep on using the command
202
- def generate_cmd_parse_xcresult(path)
203
- xcresulttool_cmd = %W(
204
- xcrun
205
- xcresulttool
206
- get
207
- --format
208
- json
209
- --path
210
- #{path}
211
- )
212
-
213
- # e.g. DEVELOPER_DIR=/Applications/Xcode_16_beta_3.app
214
- # xcresulttool version 23021, format version 3.53 (current)
215
- match = `xcrun xcresulttool version`.match(/xcresulttool version (?<version>[\d.]+)/)
216
- version = match[:version]
217
- xcresulttool_cmd << '--legacy' if Gem::Version.new(version) >= Gem::Version.new(23_021)
218
-
219
- xcresulttool_cmd.join(' ')
220
- end
221
-
222
- def parse_xcresult(path, output_remove_retry_attempts: false)
223
- require 'shellwords'
224
- path = Shellwords.escape(path)
225
-
226
- # Executes xcresulttool to get JSON format of the result bundle object
227
- # Hotfix: From Xcode 16 beta 3 'xcresulttool get --format json' has been deprecated; '--legacy' flag required to keep on using the command
228
- xcresulttool_cmd = generate_cmd_parse_xcresult(path)
229
-
230
- result_bundle_object_raw = execute_cmd(xcresulttool_cmd)
231
- result_bundle_object = JSON.parse(result_bundle_object_raw)
232
-
233
- # Parses JSON into ActionsInvocationRecord to find a list of all ids for ActionTestPlanRunSummaries
234
- actions_invocation_record = Trainer::XCResult::ActionsInvocationRecord.new(result_bundle_object)
235
- test_refs = actions_invocation_record.actions.map do |action|
236
- action.action_result.tests_ref
237
- end.compact
238
- ids = test_refs.map(&:id)
239
-
240
- # Maps ids into ActionTestPlanRunSummaries by executing xcresulttool to get JSON
241
- # containing specific information for each test summary,
242
- summaries = ids.map do |id|
243
- raw = execute_cmd("#{xcresulttool_cmd} --id #{id}")
244
- json = JSON.parse(raw)
245
- Trainer::XCResult::ActionTestPlanRunSummaries.new(json)
246
- end
247
-
248
- # Converts the ActionTestPlanRunSummaries to data for junit generator
249
- failures = actions_invocation_record.issues.test_failure_summaries || []
250
- summaries_to_data(summaries, failures, output_remove_retry_attempts: output_remove_retry_attempts)
251
- end
252
-
253
- def summaries_to_data(summaries, failures, output_remove_retry_attempts: false)
254
- # Gets flat list of all ActionTestableSummary
255
- all_summaries = summaries.map(&:summaries).flatten
256
- testable_summaries = all_summaries.map(&:testable_summaries).flatten
257
-
258
- summaries_to_names = test_summaries_to_configuration_names(all_summaries)
259
-
260
- # Maps ActionTestableSummary to rows for junit generator
261
- rows = testable_summaries.map do |testable_summary|
262
- all_tests = testable_summary.all_tests.flatten
263
-
264
- # Used by store number of passes and failures by identifier
265
- # This is used when Xcode 13 (and up) retries tests
266
- # The identifier is duplicated until test succeeds or max count is reached
267
- tests_by_identifier = {}
268
-
269
- test_rows = all_tests.map do |test|
270
- identifier = "#{test.parent.name}.#{test.name}"
271
- test_row = {
272
- identifier: identifier,
273
- name: test.name,
274
- duration: test.duration,
275
- status: test.test_status,
276
- test_group: test.parent.name,
277
-
278
- # These don't map to anything but keeping empty strings
279
- guid: ""
280
- }
281
-
282
- info = tests_by_identifier[identifier] || {}
283
- info[:failure_count] ||= 0
284
- info[:skip_count] ||= 0
285
- info[:success_count] ||= 0
286
-
287
- retry_count = info[:retry_count]
288
- if retry_count.nil?
289
- retry_count = 0
290
- else
291
- retry_count += 1
292
- end
293
- info[:retry_count] = retry_count
294
-
295
- # Set failure message if failure found
296
- failure = test.find_failure(failures)
297
- if failure
298
- test_row[:failures] = [{
299
- file_name: "",
300
- line_number: 0,
301
- message: "",
302
- performance_failure: {},
303
- failure_message: failure.failure_message
304
- }]
305
-
306
- info[:failure_count] += 1
307
- elsif test.test_status == "Skipped"
308
- test_row[:skipped] = true
309
- info[:skip_count] += 1
310
- else
311
- info[:success_count] = 1
312
- end
313
-
314
- tests_by_identifier[identifier] = info
315
-
316
- test_row
317
- end
318
-
319
- # Remove retry attempts from the count and test rows
320
- if output_remove_retry_attempts
321
- test_rows = test_rows.reject do |test_row|
322
- remove = false
323
-
324
- identifier = test_row[:identifier]
325
- info = tests_by_identifier[identifier]
326
-
327
- # Remove if this row is a retry and is a failure
328
- if info[:retry_count] > 0
329
- remove = !(test_row[:failures] || []).empty?
330
- end
331
-
332
- # Remove all failure and retry count if test did eventually pass
333
- if remove
334
- info[:failure_count] -= 1
335
- info[:retry_count] -= 1
336
- tests_by_identifier[identifier] = info
337
- end
338
-
339
- remove
340
- end
341
- end
342
-
343
- row = {
344
- project_path: testable_summary.project_relative_path,
345
- target_name: testable_summary.target_name,
346
- test_name: testable_summary.name,
347
- configuration_name: summaries_to_names[testable_summary],
348
- duration: all_tests.map(&:duration).inject(:+),
349
- tests: test_rows
350
- }
351
-
352
- row[:number_of_tests] = row[:tests].count
353
- row[:number_of_failures] = row[:tests].find_all { |a| (a[:failures] || []).count > 0 }.count
354
-
355
- # Used for seeing if any tests continued to fail after all of the Xcode 13 (and up) retries have finished
356
- unique_tests = tests_by_identifier.values || []
357
- row[:number_of_tests_excluding_retries] = unique_tests.count
358
- row[:number_of_skipped] = unique_tests.map { |a| a[:skip_count] }.inject(:+)
359
- row[:number_of_failures_excluding_retries] = unique_tests.find_all { |a| (a[:success_count] + a[:skip_count]) == 0 }.count
360
- row[:number_of_retries] = unique_tests.map { |a| a[:retry_count] }.inject(:+)
361
-
362
- row
363
- end
364
-
365
- self.data = rows
366
- end
367
-
368
- def test_summaries_to_configuration_names(test_summaries)
369
- summary_to_name = {}
370
- test_summaries.each do |summary|
371
- summary.testable_summaries.each do |testable_summary|
372
- summary_to_name[testable_summary] = summary.name
373
- end
374
- end
375
- summary_to_name
376
- end
377
-
378
- # Convert the Hashes and Arrays in something more useful
379
- def parse_content(xcpretty_naming)
380
- self.data = self.raw_json["TestableSummaries"].collect do |testable_summary|
381
- summary_row = {
382
- project_path: testable_summary["ProjectPath"],
383
- target_name: testable_summary["TargetName"],
384
- test_name: testable_summary["TestName"],
385
- duration: testable_summary["Tests"].map { |current_test| current_test["Duration"] }.inject(:+),
386
- tests: unfold_tests(testable_summary["Tests"]).collect do |current_test|
387
- test_group, test_name = test_group_and_name(testable_summary, current_test, xcpretty_naming)
388
- current_row = {
389
- identifier: current_test["TestIdentifier"],
390
- test_group: test_group,
391
- name: test_name,
392
- object_class: current_test["TestObjectClass"],
393
- status: current_test["TestStatus"],
394
- guid: current_test["TestSummaryGUID"],
395
- duration: current_test["Duration"]
396
- }
397
- if current_test["FailureSummaries"]
398
- current_row[:failures] = current_test["FailureSummaries"].collect do |current_failure|
399
- {
400
- file_name: current_failure['FileName'],
401
- line_number: current_failure['LineNumber'],
402
- message: current_failure['Message'],
403
- performance_failure: current_failure['PerformanceFailure'],
404
- failure_message: "#{current_failure['Message']} (#{current_failure['FileName']}:#{current_failure['LineNumber']})"
405
- }
406
- end
407
- end
408
- current_row
409
- end
410
- }
411
- summary_row[:number_of_tests] = summary_row[:tests].count
412
- summary_row[:number_of_failures] = summary_row[:tests].find_all { |a| (a[:failures] || []).count > 0 }.count
413
-
414
- # Makes sure that plist support matches data output of xcresult
415
- summary_row[:number_of_tests_excluding_retries] = summary_row[:number_of_tests]
416
- summary_row[:number_of_failures_excluding_retries] = summary_row[:number_of_failures]
417
- summary_row[:number_of_retries] = 0
418
-
419
- summary_row
420
- end
421
- end
422
141
  end
423
142
  end
@@ -0,0 +1,53 @@
1
+ require 'rexml/document'
2
+ require 'rubygems'
3
+
4
+ module Trainer
5
+ module XCResult
6
+ # Helper class for XML and node operations
7
+ class Helper
8
+ # Creates an XML element with the given name and attributes
9
+ #
10
+ # @param name [String] The name of the XML element
11
+ # @param attributes [Hash] A hash of attributes to add to the element
12
+ # @return [REXML::Element] The created XML element
13
+ def self.create_xml_element(name, **attributes)
14
+ # Sanitize invalid XML characters (control chars except tab/CR/LF) to avoid errors when generating XML
15
+ sanitizer = proc { |text| text.to_s.gsub(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/) { |c| format("\\u%04x", c.ord) } }
16
+ element = REXML::Element.new(sanitizer.call(name))
17
+ attributes.compact.each do |key, value|
18
+ safe_value = sanitizer.call(value.to_s)
19
+ element.attributes[key.to_s] = safe_value
20
+ end
21
+ element
22
+ end
23
+
24
+ # Find children of a node by specified node types
25
+ #
26
+ # @param node [Hash, nil] The JSON node to search within
27
+ # @param node_types [Array<String>] The node types to filter by
28
+ # @return [Array<Hash>] Array of child nodes matching the specified types
29
+ def self.find_json_children(node, *node_types)
30
+ return [] if node.nil? || node['children'].nil?
31
+
32
+ node['children'].select { |child| node_types.include?(child['nodeType']) }
33
+ end
34
+
35
+ # Check if the current xcresulttool supports new commands introduced in Xcode 16+
36
+ #
37
+ # Since Xcode 16b3, xcresulttool has marked `get <object> --format json` as deprecated/legacy,
38
+ # and replaced it with `xcrun xcresulttool get test-results tests` instead.
39
+ #
40
+ # @return [Boolean] Whether the xcresulttool supports Xcode 16+ commands
41
+ def self.supports_xcode16_xcresulttool?
42
+ # e.g. DEVELOPER_DIR=/Applications/Xcode_16_beta_3.app
43
+ # xcresulttool version 23021, format version 3.53 (current)
44
+ match = `xcrun xcresulttool version`.match(/xcresulttool version (?<version>[\d.]+)/)
45
+ version = match[:version]
46
+
47
+ Gem::Version.new(version) >= Gem::Version.new(23_021)
48
+ rescue
49
+ false
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,39 @@
1
+ require_relative 'test_case_attributes'
2
+
3
+ module Trainer
4
+ module XCResult
5
+ # Represents retries of a test case, including the original run
6
+ # e.g. a test case that ran 3 times in total will be represented by 3 Repetition instances in the `xcresulttool` JSON output,
7
+ # one for the original run and one for the 2 retries.
8
+ class Repetition
9
+ include TestCaseAttributes
10
+
11
+ attr_reader :name
12
+ attr_reader :duration
13
+ attr_reader :result
14
+ attr_reader :failure_messages
15
+ attr_reader :source_references
16
+ attr_reader :attachments
17
+
18
+ def initialize(name:, duration:, result:, failure_messages: [], source_references: [], attachments: [])
19
+ @name = name
20
+ @duration = duration
21
+ @result = result
22
+ @failure_messages = failure_messages
23
+ @source_references = source_references
24
+ @attachments = attachments
25
+ end
26
+
27
+ def self.from_json(node:)
28
+ new(
29
+ name: node['name'],
30
+ duration: parse_duration(node['duration']),
31
+ result: node['result'],
32
+ failure_messages: extract_failure_messages(node),
33
+ source_references: extract_source_references(node),
34
+ attachments: extract_attachments(node)
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end