fastlane 2.199.0 → 2.201.0.rc3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +92 -92
  4. data/deliver/lib/deliver/app_screenshot.rb +7 -0
  5. data/fastlane/lib/fastlane/.version.rb.swp +0 -0
  6. data/fastlane/lib/fastlane/actions/download_dsyms.rb +1 -1
  7. data/fastlane/lib/fastlane/actions/get_version_number.rb +1 -0
  8. data/fastlane/lib/fastlane/actions/trainer.rb +49 -0
  9. data/fastlane/lib/fastlane/helper/xcodebuild_formatter_helper.rb +9 -0
  10. data/fastlane/lib/fastlane/tools.rb +2 -1
  11. data/fastlane/lib/fastlane/version.rb +1 -1
  12. data/fastlane/swift/Actions.swift +1 -1
  13. data/fastlane/swift/Appfile.swift +1 -1
  14. data/fastlane/swift/ArgumentProcessor.swift +1 -1
  15. data/fastlane/swift/ControlCommand.swift +1 -1
  16. data/fastlane/swift/Deliverfile.swift +2 -2
  17. data/fastlane/swift/DeliverfileProtocol.swift +2 -2
  18. data/fastlane/swift/Fastlane.swift +11 -3
  19. data/fastlane/swift/Gymfile.swift +2 -2
  20. data/fastlane/swift/GymfileProtocol.swift +2 -2
  21. data/fastlane/swift/LaneFileProtocol.swift +1 -1
  22. data/fastlane/swift/MainProcess.swift +1 -1
  23. data/fastlane/swift/Matchfile.swift +2 -2
  24. data/fastlane/swift/MatchfileProtocol.swift +2 -2
  25. data/fastlane/swift/OptionalConfigValue.swift +1 -1
  26. data/fastlane/swift/Plugins.swift +1 -1
  27. data/fastlane/swift/Precheckfile.swift +2 -2
  28. data/fastlane/swift/PrecheckfileProtocol.swift +2 -2
  29. data/fastlane/swift/RubyCommand.swift +1 -1
  30. data/fastlane/swift/RubyCommandable.swift +1 -1
  31. data/fastlane/swift/Runner.swift +1 -1
  32. data/fastlane/swift/RunnerArgument.swift +1 -1
  33. data/fastlane/swift/Scanfile.swift +2 -2
  34. data/fastlane/swift/ScanfileProtocol.swift +2 -2
  35. data/fastlane/swift/Screengrabfile.swift +2 -2
  36. data/fastlane/swift/ScreengrabfileProtocol.swift +2 -2
  37. data/fastlane/swift/Snapshotfile.swift +2 -2
  38. data/fastlane/swift/SnapshotfileProtocol.swift +2 -2
  39. data/fastlane/swift/SocketClient.swift +1 -1
  40. data/fastlane/swift/SocketClientDelegateProtocol.swift +1 -1
  41. data/fastlane/swift/SocketResponse.swift +1 -1
  42. data/fastlane/swift/formatting/Brewfile.lock.json +3 -3
  43. data/fastlane/swift/main.swift +1 -1
  44. data/fastlane_core/lib/fastlane_core/ipa_file_analyser.rb +10 -5
  45. data/fastlane_core/lib/fastlane_core/itunes_transporter.rb +4 -1
  46. data/gym/lib/gym/generators/build_command_generator.rb +68 -22
  47. data/gym/lib/gym/options.rb +17 -5
  48. data/scan/lib/scan/.detect_values.rb.swp +0 -0
  49. data/scan/lib/scan/.options.rb.swp +0 -0
  50. data/scan/lib/scan/.runner.rb.swp +0 -0
  51. data/scan/lib/scan/options.rb +25 -5
  52. data/scan/lib/scan/runner.rb +115 -14
  53. data/scan/lib/scan/test_command_generator.rb +55 -5
  54. data/snapshot/lib/snapshot/.collector.rb.swp +0 -0
  55. data/snapshot/lib/snapshot/options.rb +23 -7
  56. data/snapshot/lib/snapshot/test_command_generator.rb +37 -2
  57. data/spaceship/lib/spaceship/connect_api/models/app.rb +43 -0
  58. data/spaceship/lib/spaceship/connect_api/models/app_info.rb +1 -0
  59. data/spaceship/lib/spaceship/connect_api/models/app_screenshot_set.rb +2 -0
  60. data/spaceship/lib/spaceship/connect_api/models/review_submission.rb +73 -0
  61. data/spaceship/lib/spaceship/connect_api/models/review_submission_item.rb +40 -0
  62. data/spaceship/lib/spaceship/connect_api/tunes/tunes.rb +83 -0
  63. data/spaceship/lib/spaceship/connect_api.rb +2 -0
  64. data/supply/lib/supply/options.rb +8 -0
  65. data/supply/lib/supply/uploader.rb +6 -2
  66. data/trainer/lib/assets/.junit.xml.erb.swp +0 -0
  67. data/trainer/lib/assets/junit.xml.erb +20 -0
  68. data/trainer/lib/trainer/.junit_generator.rb.swp +0 -0
  69. data/trainer/lib/trainer/.test_parser.rb.swp +0 -0
  70. data/trainer/lib/trainer/commands_generator.rb +51 -0
  71. data/trainer/lib/trainer/junit_generator.rb +31 -0
  72. data/trainer/lib/trainer/module.rb +10 -0
  73. data/trainer/lib/trainer/options.rb +55 -0
  74. data/trainer/lib/trainer/test_parser.rb +352 -0
  75. data/trainer/lib/trainer/xcresult.rb +403 -0
  76. data/trainer/lib/trainer.rb +7 -0
  77. metadata +42 -22
@@ -0,0 +1,10 @@
1
+ require 'fastlane_core/helper'
2
+ require 'fastlane/boolean'
3
+
4
+ module Trainer
5
+ Helper = FastlaneCore::Helper # you gotta love Ruby: Helper.* should use the Helper class contained in FastlaneCore
6
+ UI = FastlaneCore::UI
7
+ Boolean = Fastlane::Boolean
8
+ ROOT = Pathname.new(File.expand_path('../../..', __FILE__))
9
+ DESCRIPTION = "Convert xcodebuild plist and xcresult files to JUnit reports"
10
+ end
@@ -0,0 +1,55 @@
1
+ require 'fastlane_core/configuration/config_item'
2
+
3
+ require_relative 'module'
4
+
5
+ module Trainer
6
+ class Options
7
+ def self.available_options
8
+ @options ||= [
9
+ FastlaneCore::ConfigItem.new(key: :path,
10
+ short_option: "-p",
11
+ env_name: "TRAINER_PATH",
12
+ default_value: ".",
13
+ description: "Path to the directory that should be converted",
14
+ verify_block: proc do |value|
15
+ v = File.expand_path(value.to_s)
16
+ if v.end_with?(".plist")
17
+ UI.user_error!("Can't find file at path #{v}") unless File.exist?(v)
18
+ else
19
+ UI.user_error!("Path '#{v}' is not a directory or can't be found") unless File.directory?(v)
20
+ end
21
+ end),
22
+ FastlaneCore::ConfigItem.new(key: :extension,
23
+ short_option: "-e",
24
+ env_name: "TRAINER_EXTENSION",
25
+ default_value: ".xml",
26
+ description: "The extension for the newly created file. Usually .xml or .junit",
27
+ verify_block: proc do |value|
28
+ UI.user_error!("extension must contain a `.`") unless value.include?(".")
29
+ end),
30
+ FastlaneCore::ConfigItem.new(key: :output_directory,
31
+ short_option: "-o",
32
+ env_name: "TRAINER_OUTPUT_DIRECTORY",
33
+ default_value: nil,
34
+ optional: true,
35
+ description: "Directoy in which the xml files should be written to. Same directory as source by default"),
36
+ FastlaneCore::ConfigItem.new(key: :fail_build,
37
+ env_name: "TRAINER_FAIL_BUILD",
38
+ description: "Should this step stop the build if the tests fail? Set this to false if you're handling this with a test reporter",
39
+ is_string: false,
40
+ default_value: true),
41
+ FastlaneCore::ConfigItem.new(key: :xcpretty_naming,
42
+ short_option: "-x",
43
+ env_name: "TRAINER_XCPRETTY_NAMING",
44
+ description: "Produces class name and test name identical to xcpretty naming in junit file",
45
+ is_string: false,
46
+ default_value: false),
47
+ FastlaneCore::ConfigItem.new(key: :silent,
48
+ env_name: "TRAINER_SILENT",
49
+ description: "Silences all output",
50
+ is_string: false,
51
+ default_value: false)
52
+ ]
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,352 @@
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)
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)
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)
222
+ end
223
+
224
+ def summaries_to_data(summaries, failures)
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
+ row = {
285
+ project_path: testable_summary.project_relative_path,
286
+ target_name: testable_summary.target_name,
287
+ test_name: testable_summary.name,
288
+ duration: all_tests.map(&:duration).inject(:+),
289
+ tests: test_rows
290
+ }
291
+
292
+ row[:number_of_tests] = row[:tests].count
293
+ row[:number_of_failures] = row[:tests].find_all { |a| (a[:failures] || []).count > 0 }.count
294
+
295
+ # Used for seeing if any tests continued to fail after all of the Xcode 13 (and up) retries have finished
296
+ unique_tests = tests_by_identifier.values || []
297
+ row[:number_of_tests_excluding_retries] = unique_tests.count
298
+ row[:number_of_failures_excluding_retries] = unique_tests.find_all { |a| a[:success_count] == 0 }.count
299
+ row[:number_of_retries] = unique_tests.map { |a| a[:retry_count] }.inject(:+)
300
+
301
+ row
302
+ end
303
+
304
+ self.data = rows
305
+ end
306
+
307
+ # Convert the Hashes and Arrays in something more useful
308
+ def parse_content(xcpretty_naming)
309
+ self.data = self.raw_json["TestableSummaries"].collect do |testable_summary|
310
+ summary_row = {
311
+ project_path: testable_summary["ProjectPath"],
312
+ target_name: testable_summary["TargetName"],
313
+ test_name: testable_summary["TestName"],
314
+ duration: testable_summary["Tests"].map { |current_test| current_test["Duration"] }.inject(:+),
315
+ tests: unfold_tests(testable_summary["Tests"]).collect do |current_test|
316
+ test_group, test_name = test_group_and_name(testable_summary, current_test, xcpretty_naming)
317
+ current_row = {
318
+ identifier: current_test["TestIdentifier"],
319
+ test_group: test_group,
320
+ name: test_name,
321
+ object_class: current_test["TestObjectClass"],
322
+ status: current_test["TestStatus"],
323
+ guid: current_test["TestSummaryGUID"],
324
+ duration: current_test["Duration"]
325
+ }
326
+ if current_test["FailureSummaries"]
327
+ current_row[:failures] = current_test["FailureSummaries"].collect do |current_failure|
328
+ {
329
+ file_name: current_failure['FileName'],
330
+ line_number: current_failure['LineNumber'],
331
+ message: current_failure['Message'],
332
+ performance_failure: current_failure['PerformanceFailure'],
333
+ failure_message: "#{current_failure['Message']} (#{current_failure['FileName']}:#{current_failure['LineNumber']})"
334
+ }
335
+ end
336
+ end
337
+ current_row
338
+ end
339
+ }
340
+ summary_row[:number_of_tests] = summary_row[:tests].count
341
+ summary_row[:number_of_failures] = summary_row[:tests].find_all { |a| (a[:failures] || []).count > 0 }.count
342
+
343
+ # Makes sure that plist support matches data output of xcresult
344
+ summary_row[:number_of_tests_excluding_retries] = summary_row[:number_of_tests]
345
+ summary_row[:number_of_failures_excluding_retries] = summary_row[:number_of_failures]
346
+ summary_row[:number_of_retries] = 0
347
+
348
+ summary_row
349
+ end
350
+ end
351
+ end
352
+ end