fastlane 2.200.0 → 2.201.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- 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/gym/lib/gym/generators/build_command_generator.rb +67 -21
- data/gym/lib/gym/options.rb +17 -5
- data/scan/lib/scan/options.rb +25 -5
- data/scan/lib/scan/runner.rb +57 -14
- data/scan/lib/scan/test_command_generator.rb +54 -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 +335 -0
- data/trainer/lib/trainer/xcresult.rb +403 -0
- data/trainer/lib/trainer.rb +7 -0
- metadata +13 -3
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'commander'
|
2
|
+
|
3
|
+
require 'fastlane_core/configuration/configuration'
|
4
|
+
require 'fastlane_core/ui/help_formatter'
|
5
|
+
|
6
|
+
require_relative 'options'
|
7
|
+
require_relative 'test_parser'
|
8
|
+
|
9
|
+
require_relative 'module'
|
10
|
+
|
11
|
+
HighLine.track_eof = false
|
12
|
+
|
13
|
+
module Trainer
|
14
|
+
class CommandsGenerator
|
15
|
+
include Commander::Methods
|
16
|
+
|
17
|
+
def self.start
|
18
|
+
self.new.run
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
program :version, Fastlane::VERSION
|
23
|
+
program :description, Trainer::DESCRIPTION
|
24
|
+
program :help, 'Author', 'Felix Krause <trainer@krausefx.com>'
|
25
|
+
program :help, 'Website', 'https://fastlane.tools'
|
26
|
+
program :help, 'GitHub', 'https://github.com/KrauseFx/trainer'
|
27
|
+
program :help_formatter, :compact
|
28
|
+
|
29
|
+
global_option('--verbose', 'Shows a more verbose output') { $verbose = true }
|
30
|
+
|
31
|
+
always_trace!
|
32
|
+
|
33
|
+
FastlaneCore::CommanderGenerator.new.generate(Trainer::Options.available_options)
|
34
|
+
|
35
|
+
command :run do |c|
|
36
|
+
c.syntax = 'trainer'
|
37
|
+
c.description = Trainer::DESCRIPTION
|
38
|
+
|
39
|
+
c.action do |args, options|
|
40
|
+
options = FastlaneCore::Configuration.create(Trainer::Options.available_options, options.__hash__)
|
41
|
+
FastlaneCore::PrintTable.print_values(config: options, title: "Summary for trainer #{Fastlane::VERSION}") if $verbose
|
42
|
+
Trainer::TestParser.auto_convert(options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
default_command(:run)
|
47
|
+
|
48
|
+
run!
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative 'module'
|
2
|
+
|
3
|
+
module Trainer
|
4
|
+
class JunitGenerator
|
5
|
+
attr_accessor :results
|
6
|
+
|
7
|
+
def initialize(results)
|
8
|
+
self.results = results
|
9
|
+
end
|
10
|
+
|
11
|
+
def generate
|
12
|
+
# JUnit file documentation: http://llg.cubic.org/docs/junit/
|
13
|
+
# And http://nelsonwells.net/2012/09/how-jenkins-ci-parses-and-displays-junit-output/
|
14
|
+
# And http://windyroad.com.au/dl/Open%20Source/JUnit.xsd
|
15
|
+
|
16
|
+
lib_path = Trainer::ROOT
|
17
|
+
xml_path = File.join(lib_path, "lib/assets/junit.xml.erb")
|
18
|
+
xml = ERB.new(File.read(xml_path), nil, '<>').result(binding) # http://www.rrn.dk/rubys-erb-templating-system
|
19
|
+
|
20
|
+
xml = xml.gsub('system_', 'system-').delete("\e") # Jenkins can not parse 'ESC' symbol
|
21
|
+
|
22
|
+
# We have to manuall clear empty lines
|
23
|
+
# They may contain white spaces
|
24
|
+
clean_xml = []
|
25
|
+
xml.each_line do |row|
|
26
|
+
clean_xml << row.delete("\n") if row.strip.to_s.length > 0
|
27
|
+
end
|
28
|
+
return clean_xml.join("\n")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -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,335 @@
|
|
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
|
+
if config[:output_directory]
|
54
|
+
FileUtils.mkdir_p(config[:output_directory])
|
55
|
+
# Remove .xcresult or .plist extension
|
56
|
+
if path.end_with?(".xcresult")
|
57
|
+
filename = File.basename(path).gsub(".xcresult", config[:extension])
|
58
|
+
else
|
59
|
+
filename = File.basename(path).gsub(".plist", config[:extension])
|
60
|
+
end
|
61
|
+
to_path = File.join(config[:output_directory], filename)
|
62
|
+
else
|
63
|
+
# Remove .xcresult or .plist extension
|
64
|
+
if path.end_with?(".xcresult")
|
65
|
+
to_path = path.gsub(".xcresult", config[:extension])
|
66
|
+
else
|
67
|
+
to_path = path.gsub(".plist", config[:extension])
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
tp = Trainer::TestParser.new(path, config)
|
72
|
+
File.write(to_path, tp.to_junit)
|
73
|
+
UI.success("Successfully generated '#{to_path}'") unless config[:silent]
|
74
|
+
|
75
|
+
return_hash[to_path] = {
|
76
|
+
successful: tp.tests_successful?,
|
77
|
+
number_of_tests: tp.number_of_tests,
|
78
|
+
number_of_failures: tp.number_of_failures,
|
79
|
+
number_of_tests_excluding_retries: tp.number_of_tests_excluding_retries,
|
80
|
+
number_of_failures_excluding_retries: tp.number_of_failures_excluding_retries,
|
81
|
+
number_of_retries: tp.number_of_retries
|
82
|
+
}
|
83
|
+
end
|
84
|
+
return_hash
|
85
|
+
end
|
86
|
+
|
87
|
+
def initialize(path, config = {})
|
88
|
+
path = File.expand_path(path)
|
89
|
+
UI.user_error!("File not found at path '#{path}'") unless File.exist?(path)
|
90
|
+
|
91
|
+
if File.directory?(path) && path.end_with?(".xcresult")
|
92
|
+
parse_xcresult(path)
|
93
|
+
else
|
94
|
+
self.file_content = File.read(path)
|
95
|
+
self.raw_json = Plist.parse_xml(self.file_content)
|
96
|
+
|
97
|
+
return if self.raw_json["FormatVersion"].to_s.length.zero? # maybe that's a useless plist file
|
98
|
+
|
99
|
+
ensure_file_valid!
|
100
|
+
parse_content(config[:xcpretty_naming])
|
101
|
+
end
|
102
|
+
|
103
|
+
self.number_of_tests = 0
|
104
|
+
self.number_of_failures = 0
|
105
|
+
self.number_of_tests_excluding_retries = 0
|
106
|
+
self.number_of_failures_excluding_retries = 0
|
107
|
+
self.number_of_retries = 0
|
108
|
+
self.data.each do |thing|
|
109
|
+
self.number_of_tests += thing[:number_of_tests].to_i
|
110
|
+
self.number_of_failures += thing[:number_of_failures].to_i
|
111
|
+
self.number_of_tests_excluding_retries += thing[:number_of_tests_excluding_retries].to_i
|
112
|
+
self.number_of_failures_excluding_retries += thing[:number_of_failures_excluding_retries].to_i
|
113
|
+
self.number_of_retries += thing[:number_of_retries].to_i
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns the JUnit report as String
|
118
|
+
def to_junit
|
119
|
+
JunitGenerator.new(self.data).generate
|
120
|
+
end
|
121
|
+
|
122
|
+
# @return [Bool] were all tests successful? Is false if at least one test failed
|
123
|
+
def tests_successful?
|
124
|
+
self.data.collect { |a| a[:number_of_failures] }.all?(&:zero?)
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def ensure_file_valid!
|
130
|
+
format_version = self.raw_json["FormatVersion"]
|
131
|
+
supported_versions = ["1.1", "1.2"]
|
132
|
+
UI.user_error!("Format version '#{format_version}' is not supported, must be #{supported_versions.join(', ')}") unless supported_versions.include?(format_version)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Converts the raw plist test structure into something that's easier to enumerate
|
136
|
+
def unfold_tests(data)
|
137
|
+
# `data` looks like this
|
138
|
+
# => [{"Subtests"=>
|
139
|
+
# [{"Subtests"=>
|
140
|
+
# [{"Subtests"=>
|
141
|
+
# [{"Duration"=>0.4,
|
142
|
+
# "TestIdentifier"=>"Unit/testExample()",
|
143
|
+
# "TestName"=>"testExample()",
|
144
|
+
# "TestObjectClass"=>"IDESchemeActionTestSummary",
|
145
|
+
# "TestStatus"=>"Success",
|
146
|
+
# "TestSummaryGUID"=>"4A24BFED-03E6-4FBE-BC5E-2D80023C06B4"},
|
147
|
+
# {"FailureSummaries"=>
|
148
|
+
# [{"FileName"=>"/Users/krausefx/Developer/themoji/Unit/Unit.swift",
|
149
|
+
# "LineNumber"=>34,
|
150
|
+
# "Message"=>"XCTAssertTrue failed - ",
|
151
|
+
# "PerformanceFailure"=>false}],
|
152
|
+
# "TestIdentifier"=>"Unit/testExample2()",
|
153
|
+
|
154
|
+
tests = []
|
155
|
+
data.each do |current_hash|
|
156
|
+
if current_hash["Subtests"]
|
157
|
+
tests += unfold_tests(current_hash["Subtests"])
|
158
|
+
end
|
159
|
+
if current_hash["TestStatus"]
|
160
|
+
tests << current_hash
|
161
|
+
end
|
162
|
+
end
|
163
|
+
return tests
|
164
|
+
end
|
165
|
+
|
166
|
+
# Returns the test group and test name from the passed summary and test
|
167
|
+
# Pass xcpretty_naming = true to get the test naming aligned with xcpretty
|
168
|
+
def test_group_and_name(testable_summary, test, xcpretty_naming)
|
169
|
+
if xcpretty_naming
|
170
|
+
group = testable_summary["TargetName"] + "." + test["TestIdentifier"].split("/")[0..-2].join(".")
|
171
|
+
name = test["TestName"][0..-3]
|
172
|
+
else
|
173
|
+
group = test["TestIdentifier"].split("/")[0..-2].join(".")
|
174
|
+
name = test["TestName"]
|
175
|
+
end
|
176
|
+
return group, name
|
177
|
+
end
|
178
|
+
|
179
|
+
def execute_cmd(cmd)
|
180
|
+
output = `#{cmd}`
|
181
|
+
raise "Failed to execute - #{cmd}" unless $?.success?
|
182
|
+
return output
|
183
|
+
end
|
184
|
+
|
185
|
+
def parse_xcresult(path)
|
186
|
+
require 'shellwords'
|
187
|
+
path = Shellwords.escape(path)
|
188
|
+
|
189
|
+
# Executes xcresulttool to get JSON format of the result bundle object
|
190
|
+
result_bundle_object_raw = execute_cmd("xcrun xcresulttool get --format json --path #{path}")
|
191
|
+
result_bundle_object = JSON.parse(result_bundle_object_raw)
|
192
|
+
|
193
|
+
# Parses JSON into ActionsInvocationRecord to find a list of all ids for ActionTestPlanRunSummaries
|
194
|
+
actions_invocation_record = Trainer::XCResult::ActionsInvocationRecord.new(result_bundle_object)
|
195
|
+
test_refs = actions_invocation_record.actions.map do |action|
|
196
|
+
action.action_result.tests_ref
|
197
|
+
end.compact
|
198
|
+
ids = test_refs.map(&:id)
|
199
|
+
|
200
|
+
# Maps ids into ActionTestPlanRunSummaries by executing xcresulttool to get JSON
|
201
|
+
# containing specific information for each test summary,
|
202
|
+
summaries = ids.map do |id|
|
203
|
+
raw = execute_cmd("xcrun xcresulttool get --format json --path #{path} --id #{id}")
|
204
|
+
json = JSON.parse(raw)
|
205
|
+
Trainer::XCResult::ActionTestPlanRunSummaries.new(json)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Converts the ActionTestPlanRunSummaries to data for junit generator
|
209
|
+
failures = actions_invocation_record.issues.test_failure_summaries || []
|
210
|
+
summaries_to_data(summaries, failures)
|
211
|
+
end
|
212
|
+
|
213
|
+
def summaries_to_data(summaries, failures)
|
214
|
+
# Gets flat list of all ActionTestableSummary
|
215
|
+
all_summaries = summaries.map(&:summaries).flatten
|
216
|
+
testable_summaries = all_summaries.map(&:testable_summaries).flatten
|
217
|
+
|
218
|
+
# Maps ActionTestableSummary to rows for junit generator
|
219
|
+
rows = testable_summaries.map do |testable_summary|
|
220
|
+
all_tests = testable_summary.all_tests.flatten
|
221
|
+
|
222
|
+
# Used by store number of passes and failures by identifier
|
223
|
+
# This is used when Xcode 13 (and up) retries tests
|
224
|
+
# The identifier is duplicated until test succeeds or max count is reachd
|
225
|
+
tests_by_identifier = {}
|
226
|
+
|
227
|
+
test_rows = all_tests.map do |test|
|
228
|
+
identifier = "#{test.parent.name}.#{test.name}"
|
229
|
+
test_row = {
|
230
|
+
identifier: identifier,
|
231
|
+
name: test.name,
|
232
|
+
duration: test.duration,
|
233
|
+
status: test.test_status,
|
234
|
+
test_group: test.parent.name,
|
235
|
+
|
236
|
+
# These don't map to anything but keeping empty strings
|
237
|
+
guid: ""
|
238
|
+
}
|
239
|
+
|
240
|
+
info = tests_by_identifier[identifier] || {}
|
241
|
+
info[:failure_count] ||= 0
|
242
|
+
info[:success_count] ||= 0
|
243
|
+
|
244
|
+
retry_count = info[:retry_count]
|
245
|
+
if retry_count.nil?
|
246
|
+
retry_count = 0
|
247
|
+
else
|
248
|
+
retry_count += 1
|
249
|
+
end
|
250
|
+
info[:retry_count] = retry_count
|
251
|
+
|
252
|
+
# Set failure message if failure found
|
253
|
+
failure = test.find_failure(failures)
|
254
|
+
if failure
|
255
|
+
test_row[:failures] = [{
|
256
|
+
file_name: "",
|
257
|
+
line_number: 0,
|
258
|
+
message: "",
|
259
|
+
performance_failure: {},
|
260
|
+
failure_message: failure.failure_message
|
261
|
+
}]
|
262
|
+
|
263
|
+
info[:failure_count] += 1
|
264
|
+
else
|
265
|
+
info[:success_count] = 1
|
266
|
+
end
|
267
|
+
|
268
|
+
tests_by_identifier[identifier] = info
|
269
|
+
|
270
|
+
test_row
|
271
|
+
end
|
272
|
+
|
273
|
+
row = {
|
274
|
+
project_path: testable_summary.project_relative_path,
|
275
|
+
target_name: testable_summary.target_name,
|
276
|
+
test_name: testable_summary.name,
|
277
|
+
duration: all_tests.map(&:duration).inject(:+),
|
278
|
+
tests: test_rows
|
279
|
+
}
|
280
|
+
|
281
|
+
row[:number_of_tests] = row[:tests].count
|
282
|
+
row[:number_of_failures] = row[:tests].find_all { |a| (a[:failures] || []).count > 0 }.count
|
283
|
+
|
284
|
+
# Used for seeing if any tests continued to fail after all of the Xcode 13 (and up) retries have finished
|
285
|
+
unique_tests = tests_by_identifier.values || []
|
286
|
+
row[:number_of_tests_excluding_retries] = unique_tests.count
|
287
|
+
row[:number_of_failures_excluding_retries] = unique_tests.find_all { |a| a[:success_count] == 0 }.count
|
288
|
+
row[:number_of_retries] = unique_tests.map { |a| a[:retry_count] }.inject(:+)
|
289
|
+
|
290
|
+
row
|
291
|
+
end
|
292
|
+
|
293
|
+
self.data = rows
|
294
|
+
end
|
295
|
+
|
296
|
+
# Convert the Hashes and Arrays in something more useful
|
297
|
+
def parse_content(xcpretty_naming)
|
298
|
+
self.data = self.raw_json["TestableSummaries"].collect do |testable_summary|
|
299
|
+
summary_row = {
|
300
|
+
project_path: testable_summary["ProjectPath"],
|
301
|
+
target_name: testable_summary["TargetName"],
|
302
|
+
test_name: testable_summary["TestName"],
|
303
|
+
duration: testable_summary["Tests"].map { |current_test| current_test["Duration"] }.inject(:+),
|
304
|
+
tests: unfold_tests(testable_summary["Tests"]).collect do |current_test|
|
305
|
+
test_group, test_name = test_group_and_name(testable_summary, current_test, xcpretty_naming)
|
306
|
+
current_row = {
|
307
|
+
identifier: current_test["TestIdentifier"],
|
308
|
+
test_group: test_group,
|
309
|
+
name: test_name,
|
310
|
+
object_class: current_test["TestObjectClass"],
|
311
|
+
status: current_test["TestStatus"],
|
312
|
+
guid: current_test["TestSummaryGUID"],
|
313
|
+
duration: current_test["Duration"]
|
314
|
+
}
|
315
|
+
if current_test["FailureSummaries"]
|
316
|
+
current_row[:failures] = current_test["FailureSummaries"].collect do |current_failure|
|
317
|
+
{
|
318
|
+
file_name: current_failure['FileName'],
|
319
|
+
line_number: current_failure['LineNumber'],
|
320
|
+
message: current_failure['Message'],
|
321
|
+
performance_failure: current_failure['PerformanceFailure'],
|
322
|
+
failure_message: "#{current_failure['Message']} (#{current_failure['FileName']}:#{current_failure['LineNumber']})"
|
323
|
+
}
|
324
|
+
end
|
325
|
+
end
|
326
|
+
current_row
|
327
|
+
end
|
328
|
+
}
|
329
|
+
summary_row[:number_of_tests] = summary_row[:tests].count
|
330
|
+
summary_row[:number_of_failures] = summary_row[:tests].find_all { |a| (a[:failures] || []).count > 0 }.count
|
331
|
+
summary_row
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|