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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +100 -93
  3. data/fastlane/lib/fastlane/actions/trainer.rb +49 -0
  4. data/fastlane/lib/fastlane/helper/xcodebuild_formatter_helper.rb +9 -0
  5. data/fastlane/lib/fastlane/tools.rb +2 -1
  6. data/fastlane/lib/fastlane/version.rb +1 -1
  7. data/fastlane/swift/Deliverfile.swift +1 -1
  8. data/fastlane/swift/DeliverfileProtocol.swift +1 -1
  9. data/fastlane/swift/Fastlane.swift +134 -43
  10. data/fastlane/swift/Gymfile.swift +1 -1
  11. data/fastlane/swift/GymfileProtocol.swift +10 -6
  12. data/fastlane/swift/Matchfile.swift +1 -1
  13. data/fastlane/swift/MatchfileProtocol.swift +1 -1
  14. data/fastlane/swift/Precheckfile.swift +1 -1
  15. data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
  16. data/fastlane/swift/Scanfile.swift +1 -1
  17. data/fastlane/swift/ScanfileProtocol.swift +18 -6
  18. data/fastlane/swift/Screengrabfile.swift +1 -1
  19. data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
  20. data/fastlane/swift/Snapshotfile.swift +1 -1
  21. data/fastlane/swift/SnapshotfileProtocol.swift +9 -5
  22. data/fastlane/swift/formatting/Brewfile.lock.json +13 -13
  23. data/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb +7 -0
  24. data/gym/lib/gym/generators/build_command_generator.rb +67 -21
  25. data/gym/lib/gym/options.rb +17 -5
  26. data/scan/lib/scan/options.rb +30 -5
  27. data/scan/lib/scan/runner.rb +121 -14
  28. data/scan/lib/scan/test_command_generator.rb +55 -5
  29. data/snapshot/lib/snapshot/options.rb +23 -7
  30. data/snapshot/lib/snapshot/test_command_generator.rb +37 -2
  31. data/trainer/lib/assets/junit.xml.erb +20 -0
  32. data/trainer/lib/trainer/commands_generator.rb +51 -0
  33. data/trainer/lib/trainer/junit_generator.rb +31 -0
  34. data/trainer/lib/trainer/module.rb +10 -0
  35. data/trainer/lib/trainer/options.rb +55 -0
  36. data/trainer/lib/trainer/test_parser.rb +376 -0
  37. data/trainer/lib/trainer/xcresult.rb +403 -0
  38. data/trainer/lib/trainer.rb +7 -0
  39. 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