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
@@ -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
|
-
|
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
|
-
|
108
|
-
|
107
|
+
file_content = File.read(path)
|
108
|
+
raw_json = Plist.parse_xml(file_content)
|
109
109
|
|
110
|
-
return if
|
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
|