fastlane 2.200.0 → 2.201.0

Sign up to get free protection for your applications and to get access to all the features.
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