fastlane 2.200.0 → 2.201.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 +100 -93
- data/fastlane/lib/fastlane/actions/trainer.rb +49 -0
- data/fastlane/lib/fastlane/helper/xcodebuild_formatter_helper.rb +9 -0
- data/fastlane/lib/fastlane/tools.rb +2 -1
- data/fastlane/lib/fastlane/version.rb +1 -1
- data/fastlane/swift/Deliverfile.swift +1 -1
- data/fastlane/swift/DeliverfileProtocol.swift +1 -1
- data/fastlane/swift/Fastlane.swift +134 -43
- data/fastlane/swift/Gymfile.swift +1 -1
- data/fastlane/swift/GymfileProtocol.swift +10 -6
- data/fastlane/swift/Matchfile.swift +1 -1
- data/fastlane/swift/MatchfileProtocol.swift +1 -1
- data/fastlane/swift/Precheckfile.swift +1 -1
- data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
- data/fastlane/swift/Scanfile.swift +1 -1
- data/fastlane/swift/ScanfileProtocol.swift +18 -6
- data/fastlane/swift/Screengrabfile.swift +1 -1
- data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
- data/fastlane/swift/Snapshotfile.swift +1 -1
- data/fastlane/swift/SnapshotfileProtocol.swift +9 -5
- data/fastlane/swift/formatting/Brewfile.lock.json +13 -13
- data/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb +7 -0
- data/gym/lib/gym/generators/build_command_generator.rb +67 -21
- data/gym/lib/gym/options.rb +17 -5
- data/scan/lib/scan/options.rb +30 -5
- data/scan/lib/scan/runner.rb +121 -14
- data/scan/lib/scan/test_command_generator.rb +55 -5
- data/snapshot/lib/snapshot/options.rb +23 -7
- data/snapshot/lib/snapshot/test_command_generator.rb +37 -2
- data/trainer/lib/assets/junit.xml.erb +20 -0
- data/trainer/lib/trainer/commands_generator.rb +51 -0
- data/trainer/lib/trainer/junit_generator.rb +31 -0
- data/trainer/lib/trainer/module.rb +10 -0
- data/trainer/lib/trainer/options.rb +55 -0
- data/trainer/lib/trainer/test_parser.rb +376 -0
- data/trainer/lib/trainer/xcresult.rb +403 -0
- data/trainer/lib/trainer.rb +7 -0
- metadata +29 -18
@@ -0,0 +1,376 @@
|
|
1
|
+
require 'plist'
|
2
|
+
|
3
|
+
require 'fastlane_core/print_table'
|
4
|
+
|
5
|
+
require_relative 'junit_generator'
|
6
|
+
require_relative 'xcresult'
|
7
|
+
require_relative 'module'
|
8
|
+
|
9
|
+
module Trainer
|
10
|
+
class TestParser
|
11
|
+
attr_accessor :data
|
12
|
+
|
13
|
+
attr_accessor :file_content
|
14
|
+
|
15
|
+
attr_accessor :raw_json
|
16
|
+
|
17
|
+
attr_accessor :number_of_tests
|
18
|
+
attr_accessor :number_of_failures
|
19
|
+
attr_accessor :number_of_tests_excluding_retries
|
20
|
+
attr_accessor :number_of_failures_excluding_retries
|
21
|
+
attr_accessor :number_of_retries
|
22
|
+
|
23
|
+
# Returns a hash with the path being the key, and the value
|
24
|
+
# defining if the tests were successful
|
25
|
+
def self.auto_convert(config)
|
26
|
+
unless config[:silent]
|
27
|
+
FastlaneCore::PrintTable.print_values(config: config,
|
28
|
+
title: "Summary for trainer #{Fastlane::VERSION}")
|
29
|
+
end
|
30
|
+
|
31
|
+
containing_dir = config[:path]
|
32
|
+
# Xcode < 10
|
33
|
+
files = Dir["#{containing_dir}/**/Logs/Test/*TestSummaries.plist"]
|
34
|
+
files += Dir["#{containing_dir}/Test/*TestSummaries.plist"]
|
35
|
+
files += Dir["#{containing_dir}/*TestSummaries.plist"]
|
36
|
+
# Xcode 10
|
37
|
+
files += Dir["#{containing_dir}/**/Logs/Test/*.xcresult/TestSummaries.plist"]
|
38
|
+
files += Dir["#{containing_dir}/Test/*.xcresult/TestSummaries.plist"]
|
39
|
+
files += Dir["#{containing_dir}/*.xcresult/TestSummaries.plist"]
|
40
|
+
files += Dir[containing_dir] if containing_dir.end_with?(".plist") # if it's the exact path to a plist file
|
41
|
+
# Xcode 11
|
42
|
+
files += Dir["#{containing_dir}/**/Logs/Test/*.xcresult"]
|
43
|
+
files += Dir["#{containing_dir}/Test/*.xcresult"]
|
44
|
+
files += Dir["#{containing_dir}/*.xcresult"]
|
45
|
+
files << containing_dir if File.extname(containing_dir) == ".xcresult"
|
46
|
+
|
47
|
+
if files.empty?
|
48
|
+
UI.user_error!("No test result files found in directory '#{containing_dir}', make sure the file name ends with 'TestSummaries.plist' or '.xcresult'")
|
49
|
+
end
|
50
|
+
|
51
|
+
return_hash = {}
|
52
|
+
files.each do |path|
|
53
|
+
extension = config[:extension]
|
54
|
+
output_filename = config[:output_filename]
|
55
|
+
|
56
|
+
should_write_file = !extension.nil? || !output_filename.nil?
|
57
|
+
|
58
|
+
if should_write_file
|
59
|
+
if config[:output_directory]
|
60
|
+
FileUtils.mkdir_p(config[:output_directory])
|
61
|
+
# Remove .xcresult or .plist extension
|
62
|
+
# Use custom file name ONLY if one file otherwise issues
|
63
|
+
if files.size == 1 && output_filename
|
64
|
+
filename = output_filename
|
65
|
+
elsif path.end_with?(".xcresult")
|
66
|
+
filename ||= File.basename(path).gsub(".xcresult", extension)
|
67
|
+
else
|
68
|
+
filename ||= File.basename(path).gsub(".plist", extension)
|
69
|
+
end
|
70
|
+
to_path = File.join(config[:output_directory], filename)
|
71
|
+
else
|
72
|
+
# Remove .xcresult or .plist extension
|
73
|
+
if path.end_with?(".xcresult")
|
74
|
+
to_path = path.gsub(".xcresult", extension)
|
75
|
+
else
|
76
|
+
to_path = path.gsub(".plist", extension)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
tp = Trainer::TestParser.new(path, config)
|
82
|
+
File.write(to_path, tp.to_junit) if should_write_file
|
83
|
+
UI.success("Successfully generated '#{to_path}'") if should_write_file && !config[:silent]
|
84
|
+
|
85
|
+
return_hash[path] = {
|
86
|
+
to_path: to_path,
|
87
|
+
successful: tp.tests_successful?,
|
88
|
+
number_of_tests: tp.number_of_tests,
|
89
|
+
number_of_failures: tp.number_of_failures,
|
90
|
+
number_of_tests_excluding_retries: tp.number_of_tests_excluding_retries,
|
91
|
+
number_of_failures_excluding_retries: tp.number_of_failures_excluding_retries,
|
92
|
+
number_of_retries: tp.number_of_retries
|
93
|
+
}
|
94
|
+
end
|
95
|
+
return_hash
|
96
|
+
end
|
97
|
+
|
98
|
+
def initialize(path, config = {})
|
99
|
+
path = File.expand_path(path)
|
100
|
+
UI.user_error!("File not found at path '#{path}'") unless File.exist?(path)
|
101
|
+
|
102
|
+
if File.directory?(path) && path.end_with?(".xcresult")
|
103
|
+
parse_xcresult(path, output_remove_retry_attempts: config[:output_remove_retry_attempts])
|
104
|
+
else
|
105
|
+
self.file_content = File.read(path)
|
106
|
+
self.raw_json = Plist.parse_xml(self.file_content)
|
107
|
+
|
108
|
+
return if self.raw_json["FormatVersion"].to_s.length.zero? # maybe that's a useless plist file
|
109
|
+
|
110
|
+
ensure_file_valid!
|
111
|
+
parse_content(config[:xcpretty_naming])
|
112
|
+
end
|
113
|
+
|
114
|
+
self.number_of_tests = 0
|
115
|
+
self.number_of_failures = 0
|
116
|
+
self.number_of_tests_excluding_retries = 0
|
117
|
+
self.number_of_failures_excluding_retries = 0
|
118
|
+
self.number_of_retries = 0
|
119
|
+
self.data.each do |thing|
|
120
|
+
self.number_of_tests += thing[:number_of_tests].to_i
|
121
|
+
self.number_of_failures += thing[:number_of_failures].to_i
|
122
|
+
self.number_of_tests_excluding_retries += thing[:number_of_tests_excluding_retries].to_i
|
123
|
+
self.number_of_failures_excluding_retries += thing[:number_of_failures_excluding_retries].to_i
|
124
|
+
self.number_of_retries += thing[:number_of_retries].to_i
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns the JUnit report as String
|
129
|
+
def to_junit
|
130
|
+
JunitGenerator.new(self.data).generate
|
131
|
+
end
|
132
|
+
|
133
|
+
# @return [Bool] were all tests successful? Is false if at least one test failed
|
134
|
+
def tests_successful?
|
135
|
+
self.data.collect { |a| a[:number_of_failures] }.all?(&:zero?)
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def ensure_file_valid!
|
141
|
+
format_version = self.raw_json["FormatVersion"]
|
142
|
+
supported_versions = ["1.1", "1.2"]
|
143
|
+
UI.user_error!("Format version '#{format_version}' is not supported, must be #{supported_versions.join(', ')}") unless supported_versions.include?(format_version)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Converts the raw plist test structure into something that's easier to enumerate
|
147
|
+
def unfold_tests(data)
|
148
|
+
# `data` looks like this
|
149
|
+
# => [{"Subtests"=>
|
150
|
+
# [{"Subtests"=>
|
151
|
+
# [{"Subtests"=>
|
152
|
+
# [{"Duration"=>0.4,
|
153
|
+
# "TestIdentifier"=>"Unit/testExample()",
|
154
|
+
# "TestName"=>"testExample()",
|
155
|
+
# "TestObjectClass"=>"IDESchemeActionTestSummary",
|
156
|
+
# "TestStatus"=>"Success",
|
157
|
+
# "TestSummaryGUID"=>"4A24BFED-03E6-4FBE-BC5E-2D80023C06B4"},
|
158
|
+
# {"FailureSummaries"=>
|
159
|
+
# [{"FileName"=>"/Users/krausefx/Developer/themoji/Unit/Unit.swift",
|
160
|
+
# "LineNumber"=>34,
|
161
|
+
# "Message"=>"XCTAssertTrue failed - ",
|
162
|
+
# "PerformanceFailure"=>false}],
|
163
|
+
# "TestIdentifier"=>"Unit/testExample2()",
|
164
|
+
|
165
|
+
tests = []
|
166
|
+
data.each do |current_hash|
|
167
|
+
if current_hash["Subtests"]
|
168
|
+
tests += unfold_tests(current_hash["Subtests"])
|
169
|
+
end
|
170
|
+
if current_hash["TestStatus"]
|
171
|
+
tests << current_hash
|
172
|
+
end
|
173
|
+
end
|
174
|
+
return tests
|
175
|
+
end
|
176
|
+
|
177
|
+
# Returns the test group and test name from the passed summary and test
|
178
|
+
# Pass xcpretty_naming = true to get the test naming aligned with xcpretty
|
179
|
+
def test_group_and_name(testable_summary, test, xcpretty_naming)
|
180
|
+
if xcpretty_naming
|
181
|
+
group = testable_summary["TargetName"] + "." + test["TestIdentifier"].split("/")[0..-2].join(".")
|
182
|
+
name = test["TestName"][0..-3]
|
183
|
+
else
|
184
|
+
group = test["TestIdentifier"].split("/")[0..-2].join(".")
|
185
|
+
name = test["TestName"]
|
186
|
+
end
|
187
|
+
return group, name
|
188
|
+
end
|
189
|
+
|
190
|
+
def execute_cmd(cmd)
|
191
|
+
output = `#{cmd}`
|
192
|
+
raise "Failed to execute - #{cmd}" unless $?.success?
|
193
|
+
return output
|
194
|
+
end
|
195
|
+
|
196
|
+
def parse_xcresult(path, output_remove_retry_attempts: false)
|
197
|
+
require 'shellwords'
|
198
|
+
path = Shellwords.escape(path)
|
199
|
+
|
200
|
+
# Executes xcresulttool to get JSON format of the result bundle object
|
201
|
+
result_bundle_object_raw = execute_cmd("xcrun xcresulttool get --format json --path #{path}")
|
202
|
+
result_bundle_object = JSON.parse(result_bundle_object_raw)
|
203
|
+
|
204
|
+
# Parses JSON into ActionsInvocationRecord to find a list of all ids for ActionTestPlanRunSummaries
|
205
|
+
actions_invocation_record = Trainer::XCResult::ActionsInvocationRecord.new(result_bundle_object)
|
206
|
+
test_refs = actions_invocation_record.actions.map do |action|
|
207
|
+
action.action_result.tests_ref
|
208
|
+
end.compact
|
209
|
+
ids = test_refs.map(&:id)
|
210
|
+
|
211
|
+
# Maps ids into ActionTestPlanRunSummaries by executing xcresulttool to get JSON
|
212
|
+
# containing specific information for each test summary,
|
213
|
+
summaries = ids.map do |id|
|
214
|
+
raw = execute_cmd("xcrun xcresulttool get --format json --path #{path} --id #{id}")
|
215
|
+
json = JSON.parse(raw)
|
216
|
+
Trainer::XCResult::ActionTestPlanRunSummaries.new(json)
|
217
|
+
end
|
218
|
+
|
219
|
+
# Converts the ActionTestPlanRunSummaries to data for junit generator
|
220
|
+
failures = actions_invocation_record.issues.test_failure_summaries || []
|
221
|
+
summaries_to_data(summaries, failures, output_remove_retry_attempts: output_remove_retry_attempts)
|
222
|
+
end
|
223
|
+
|
224
|
+
def summaries_to_data(summaries, failures, output_remove_retry_attempts: false)
|
225
|
+
# Gets flat list of all ActionTestableSummary
|
226
|
+
all_summaries = summaries.map(&:summaries).flatten
|
227
|
+
testable_summaries = all_summaries.map(&:testable_summaries).flatten
|
228
|
+
|
229
|
+
# Maps ActionTestableSummary to rows for junit generator
|
230
|
+
rows = testable_summaries.map do |testable_summary|
|
231
|
+
all_tests = testable_summary.all_tests.flatten
|
232
|
+
|
233
|
+
# Used by store number of passes and failures by identifier
|
234
|
+
# This is used when Xcode 13 (and up) retries tests
|
235
|
+
# The identifier is duplicated until test succeeds or max count is reachd
|
236
|
+
tests_by_identifier = {}
|
237
|
+
|
238
|
+
test_rows = all_tests.map do |test|
|
239
|
+
identifier = "#{test.parent.name}.#{test.name}"
|
240
|
+
test_row = {
|
241
|
+
identifier: identifier,
|
242
|
+
name: test.name,
|
243
|
+
duration: test.duration,
|
244
|
+
status: test.test_status,
|
245
|
+
test_group: test.parent.name,
|
246
|
+
|
247
|
+
# These don't map to anything but keeping empty strings
|
248
|
+
guid: ""
|
249
|
+
}
|
250
|
+
|
251
|
+
info = tests_by_identifier[identifier] || {}
|
252
|
+
info[:failure_count] ||= 0
|
253
|
+
info[:success_count] ||= 0
|
254
|
+
|
255
|
+
retry_count = info[:retry_count]
|
256
|
+
if retry_count.nil?
|
257
|
+
retry_count = 0
|
258
|
+
else
|
259
|
+
retry_count += 1
|
260
|
+
end
|
261
|
+
info[:retry_count] = retry_count
|
262
|
+
|
263
|
+
# Set failure message if failure found
|
264
|
+
failure = test.find_failure(failures)
|
265
|
+
if failure
|
266
|
+
test_row[:failures] = [{
|
267
|
+
file_name: "",
|
268
|
+
line_number: 0,
|
269
|
+
message: "",
|
270
|
+
performance_failure: {},
|
271
|
+
failure_message: failure.failure_message
|
272
|
+
}]
|
273
|
+
|
274
|
+
info[:failure_count] += 1
|
275
|
+
else
|
276
|
+
info[:success_count] = 1
|
277
|
+
end
|
278
|
+
|
279
|
+
tests_by_identifier[identifier] = info
|
280
|
+
|
281
|
+
test_row
|
282
|
+
end
|
283
|
+
|
284
|
+
# Remove retry attempts from the count and test rows
|
285
|
+
if output_remove_retry_attempts
|
286
|
+
test_rows = test_rows.reject do |test_row|
|
287
|
+
remove = false
|
288
|
+
|
289
|
+
identifier = test_row[:identifier]
|
290
|
+
info = tests_by_identifier[identifier]
|
291
|
+
|
292
|
+
# Remove if this row is a retry and is a failure
|
293
|
+
if info[:retry_count] > 0
|
294
|
+
remove = !(test_row[:failures] || []).empty?
|
295
|
+
end
|
296
|
+
|
297
|
+
# Remove all failure and retry count if test did eventually pass
|
298
|
+
if remove
|
299
|
+
info[:failure_count] -= 1
|
300
|
+
info[:retry_count] -= 1
|
301
|
+
tests_by_identifier[identifier] = info
|
302
|
+
end
|
303
|
+
|
304
|
+
remove
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
row = {
|
309
|
+
project_path: testable_summary.project_relative_path,
|
310
|
+
target_name: testable_summary.target_name,
|
311
|
+
test_name: testable_summary.name,
|
312
|
+
duration: all_tests.map(&:duration).inject(:+),
|
313
|
+
tests: test_rows
|
314
|
+
}
|
315
|
+
|
316
|
+
row[:number_of_tests] = row[:tests].count
|
317
|
+
row[:number_of_failures] = row[:tests].find_all { |a| (a[:failures] || []).count > 0 }.count
|
318
|
+
|
319
|
+
# Used for seeing if any tests continued to fail after all of the Xcode 13 (and up) retries have finished
|
320
|
+
unique_tests = tests_by_identifier.values || []
|
321
|
+
row[:number_of_tests_excluding_retries] = unique_tests.count
|
322
|
+
row[:number_of_failures_excluding_retries] = unique_tests.find_all { |a| a[:success_count] == 0 }.count
|
323
|
+
row[:number_of_retries] = unique_tests.map { |a| a[:retry_count] }.inject(:+)
|
324
|
+
|
325
|
+
row
|
326
|
+
end
|
327
|
+
|
328
|
+
self.data = rows
|
329
|
+
end
|
330
|
+
|
331
|
+
# Convert the Hashes and Arrays in something more useful
|
332
|
+
def parse_content(xcpretty_naming)
|
333
|
+
self.data = self.raw_json["TestableSummaries"].collect do |testable_summary|
|
334
|
+
summary_row = {
|
335
|
+
project_path: testable_summary["ProjectPath"],
|
336
|
+
target_name: testable_summary["TargetName"],
|
337
|
+
test_name: testable_summary["TestName"],
|
338
|
+
duration: testable_summary["Tests"].map { |current_test| current_test["Duration"] }.inject(:+),
|
339
|
+
tests: unfold_tests(testable_summary["Tests"]).collect do |current_test|
|
340
|
+
test_group, test_name = test_group_and_name(testable_summary, current_test, xcpretty_naming)
|
341
|
+
current_row = {
|
342
|
+
identifier: current_test["TestIdentifier"],
|
343
|
+
test_group: test_group,
|
344
|
+
name: test_name,
|
345
|
+
object_class: current_test["TestObjectClass"],
|
346
|
+
status: current_test["TestStatus"],
|
347
|
+
guid: current_test["TestSummaryGUID"],
|
348
|
+
duration: current_test["Duration"]
|
349
|
+
}
|
350
|
+
if current_test["FailureSummaries"]
|
351
|
+
current_row[:failures] = current_test["FailureSummaries"].collect do |current_failure|
|
352
|
+
{
|
353
|
+
file_name: current_failure['FileName'],
|
354
|
+
line_number: current_failure['LineNumber'],
|
355
|
+
message: current_failure['Message'],
|
356
|
+
performance_failure: current_failure['PerformanceFailure'],
|
357
|
+
failure_message: "#{current_failure['Message']} (#{current_failure['FileName']}:#{current_failure['LineNumber']})"
|
358
|
+
}
|
359
|
+
end
|
360
|
+
end
|
361
|
+
current_row
|
362
|
+
end
|
363
|
+
}
|
364
|
+
summary_row[:number_of_tests] = summary_row[:tests].count
|
365
|
+
summary_row[:number_of_failures] = summary_row[:tests].find_all { |a| (a[:failures] || []).count > 0 }.count
|
366
|
+
|
367
|
+
# Makes sure that plist support matches data output of xcresult
|
368
|
+
summary_row[:number_of_tests_excluding_retries] = summary_row[:number_of_tests]
|
369
|
+
summary_row[:number_of_failures_excluding_retries] = summary_row[:number_of_failures]
|
370
|
+
summary_row[:number_of_retries] = 0
|
371
|
+
|
372
|
+
summary_row
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|