fastlane 2.200.0 → 2.201.0.rc1
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/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
|