minitest-reporters 1.3.6 → 1.3.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +28 -27
- data/.rubocop.yml +77 -77
- data/.ruby-gemset +1 -1
- data/.travis.yml +14 -14
- data/.yardopts +5 -5
- data/CHANGELOG.md +98 -93
- data/Gemfile +2 -2
- data/LICENSE +20 -20
- data/README.md +135 -135
- data/Rakefile +70 -70
- data/appveyor.yml +22 -22
- data/lib/minitest/extensible_backtrace_filter.rb +67 -67
- data/lib/minitest/minitest_reporter_plugin.rb +76 -76
- data/lib/minitest/old_activesupport_fix.rb +24 -24
- data/lib/minitest/relative_position.rb +26 -26
- data/lib/minitest/reporters.rb +91 -91
- data/lib/minitest/reporters/ansi.rb +30 -30
- data/lib/minitest/reporters/base_reporter.rb +136 -136
- data/lib/minitest/reporters/default_reporter.rb +228 -228
- data/lib/minitest/reporters/html_reporter.rb +224 -224
- data/lib/minitest/reporters/junit_reporter.rb +168 -164
- data/lib/minitest/reporters/mean_time_reporter.rb +388 -388
- data/lib/minitest/reporters/progress_reporter.rb +102 -96
- data/lib/minitest/reporters/ruby_mate_reporter.rb +54 -54
- data/lib/minitest/reporters/rubymine_reporter.rb +116 -116
- data/lib/minitest/reporters/spec_reporter.rb +61 -61
- data/lib/minitest/reporters/version.rb +5 -5
- data/lib/minitest/templates/index.html.erb +82 -82
- data/minitest-reporters.gemspec +31 -32
- data/test/fixtures/junit_filename_bug_example_test.rb +41 -41
- data/test/fixtures/mean_time_test.rb +36 -36
- data/test/fixtures/progress_detailed_skip_test.rb +8 -8
- data/test/fixtures/progress_test.rb +8 -8
- data/test/fixtures/sample_test.rb +15 -15
- data/test/fixtures/spec_test.rb +18 -18
- data/test/gallery/bad_test.rb +25 -25
- data/test/gallery/good_test.rb +14 -14
- data/test/integration/reporters/junit_reporter_test.rb +12 -12
- data/test/integration/reporters/mean_time_reporter_test.rb +7 -7
- data/test/integration/reporters/progress_reporter_test.rb +40 -40
- data/test/test_helper.rb +22 -22
- data/test/unit/minitest/extensible_backtrace_filter_test.rb +42 -42
- data/test/unit/minitest/junit_reporter_test.rb +46 -23
- data/test/unit/minitest/mean_time_reporter_unit_test.rb +149 -149
- data/test/unit/minitest/minitest_reporter_plugin_test.rb +14 -14
- data/test/unit/minitest/reporters_test.rb +65 -65
- data/test/unit/minitest/spec_reporter_test.rb +62 -62
- metadata +22 -5
@@ -1,224 +1,224 @@
|
|
1
|
-
require 'builder'
|
2
|
-
require 'fileutils'
|
3
|
-
require 'erb'
|
4
|
-
|
5
|
-
module Minitest
|
6
|
-
module Reporters
|
7
|
-
# A reporter for generating HTML test reports
|
8
|
-
# This is recommended to be used with a CI server, where the report is kept as an artifact and is accessible via
|
9
|
-
# a shared link
|
10
|
-
#
|
11
|
-
# The reporter sorts the results alphabetically and then by results
|
12
|
-
# so that failing and skipped tests are at the top.
|
13
|
-
#
|
14
|
-
# When using Minitest Specs, the number prefix is dropped from the name of the test so that it reads well
|
15
|
-
#
|
16
|
-
# On each test run all files in the reports directory are deleted, this prevents a build up of old reports
|
17
|
-
#
|
18
|
-
# The report is generated using ERB. A custom ERB template can be provided but it is not required
|
19
|
-
# The default ERB template uses JQuery and Bootstrap, both of these are included by referencing the CDN sites
|
20
|
-
class HtmlReporter < BaseReporter
|
21
|
-
# The title of the report
|
22
|
-
attr_reader :title
|
23
|
-
|
24
|
-
# The number of tests that passed
|
25
|
-
def passes
|
26
|
-
count - failures - errors - skips
|
27
|
-
end
|
28
|
-
|
29
|
-
# The percentage of tests that passed, calculated in a way that avoids rounding errors
|
30
|
-
def percent_passes
|
31
|
-
100 - percent_skipps - percent_errors_failures
|
32
|
-
end
|
33
|
-
|
34
|
-
# The percentage of tests that were skipped
|
35
|
-
def percent_skipps
|
36
|
-
(skips / count.to_f * 100).to_i
|
37
|
-
end
|
38
|
-
|
39
|
-
# The percentage of tests that failed
|
40
|
-
def percent_errors_failures
|
41
|
-
((errors + failures) / count.to_f * 100).to_i
|
42
|
-
end
|
43
|
-
|
44
|
-
# Trims off the number prefix on test names when using Minitest Specs
|
45
|
-
def friendly_name(test)
|
46
|
-
groups = test.name.scan(/(test_\d+_)(.*)/i)
|
47
|
-
return test.name if groups.empty?
|
48
|
-
"it #{groups[0][1]}"
|
49
|
-
end
|
50
|
-
|
51
|
-
# The constructor takes a hash, and uses the following keys:
|
52
|
-
# :title - the title that will be used in the report, defaults to 'Test Results'
|
53
|
-
# :reports_dir - the directory the reports should be written to, defaults to 'test/html_reports'
|
54
|
-
# :erb_template - the path to a custom ERB template, defaults to the supplied ERB template
|
55
|
-
# :mode - Useful for debugging, :terse suppresses errors and is the default, :verbose lets errors bubble up
|
56
|
-
# :output_filename - the report's filename, defaults to 'index.html'
|
57
|
-
def initialize(args = {})
|
58
|
-
super({})
|
59
|
-
|
60
|
-
defaults = {
|
61
|
-
:title => 'Test Results',
|
62
|
-
:erb_template => "#{File.dirname(__FILE__)}/../templates/index.html.erb",
|
63
|
-
:reports_dir => 'test/html_reports',
|
64
|
-
:mode => :safe,
|
65
|
-
:output_filename => 'index.html',
|
66
|
-
}
|
67
|
-
|
68
|
-
settings = defaults.merge(args)
|
69
|
-
|
70
|
-
@mode = settings[:mode]
|
71
|
-
@title = settings[:title]
|
72
|
-
@erb_template = settings[:erb_template]
|
73
|
-
@output_filename = settings[:output_filename]
|
74
|
-
reports_dir = settings[:reports_dir]
|
75
|
-
|
76
|
-
@reports_path = File.absolute_path(reports_dir)
|
77
|
-
end
|
78
|
-
|
79
|
-
def start
|
80
|
-
super
|
81
|
-
|
82
|
-
puts "Emptying #{@reports_path}"
|
83
|
-
FileUtils.mkdir_p(@reports_path)
|
84
|
-
File.delete(html_file) if File.exist?(html_file)
|
85
|
-
end
|
86
|
-
|
87
|
-
# Called by the framework to generate the report
|
88
|
-
def report
|
89
|
-
super
|
90
|
-
|
91
|
-
begin
|
92
|
-
puts "Writing HTML reports to #{@reports_path}"
|
93
|
-
erb_str = File.read(@erb_template)
|
94
|
-
renderer = ERB.new(erb_str)
|
95
|
-
|
96
|
-
tests_by_suites = tests.group_by { |test| test_class(test) } # taken from the JUnit reporter
|
97
|
-
|
98
|
-
suites = tests_by_suites.map do |suite, tests|
|
99
|
-
suite_summary = summarize_suite(suite, tests)
|
100
|
-
suite_summary[:tests] = tests.sort { |a, b| compare_tests(a, b) }
|
101
|
-
suite_summary
|
102
|
-
end
|
103
|
-
|
104
|
-
suites.sort! { |a, b| compare_suites(a, b) }
|
105
|
-
|
106
|
-
result = renderer.result(binding)
|
107
|
-
File.open(html_file, 'w') do |f|
|
108
|
-
f.write(result)
|
109
|
-
end
|
110
|
-
|
111
|
-
# rubocop:disable Lint/RescueException
|
112
|
-
rescue Exception => e
|
113
|
-
puts 'There was an error writing the HTML report'
|
114
|
-
puts 'This may have been caused by cancelling the test run'
|
115
|
-
puts 'Use mode => :verbose in the HTML reporters constructor to see more detail' if @mode == :terse
|
116
|
-
puts 'Use mode => :terse in the HTML reporters constructor to see less detail' if @mode != :terse
|
117
|
-
raise e if @mode != :terse
|
118
|
-
end
|
119
|
-
# rubocop:enable Lint/RescueException
|
120
|
-
end
|
121
|
-
|
122
|
-
private
|
123
|
-
|
124
|
-
def html_file
|
125
|
-
"#{@reports_path}/#{@output_filename}"
|
126
|
-
end
|
127
|
-
|
128
|
-
def compare_suites_by_name(suite_a, suite_b)
|
129
|
-
suite_a[:name] <=> suite_b[:name]
|
130
|
-
end
|
131
|
-
|
132
|
-
def compare_tests_by_name(test_a, test_b)
|
133
|
-
friendly_name(test_a) <=> friendly_name(test_b)
|
134
|
-
end
|
135
|
-
|
136
|
-
# Test suites are first ordered by evaluating the results of the tests, then by test suite name
|
137
|
-
# Test suites which have failing tests are given highest order
|
138
|
-
# Tests suites which have skipped tests are given second highest priority
|
139
|
-
def compare_suites(suite_a, suite_b)
|
140
|
-
return compare_suites_by_name(suite_a, suite_b) if suite_a[:has_errors_or_failures] && suite_b[:has_errors_or_failures]
|
141
|
-
return -1 if suite_a[:has_errors_or_failures] && !suite_b[:has_errors_or_failures]
|
142
|
-
return 1 if !suite_a[:has_errors_or_failures] && suite_b[:has_errors_or_failures]
|
143
|
-
|
144
|
-
return compare_suites_by_name(suite_a, suite_b) if suite_a[:has_skipps] && suite_b[:has_skipps]
|
145
|
-
return -1 if suite_a[:has_skipps] && !suite_b[:has_skipps]
|
146
|
-
return 1 if !suite_a[:has_skipps] && suite_b[:has_skipps]
|
147
|
-
|
148
|
-
compare_suites_by_name(suite_a, suite_b)
|
149
|
-
end
|
150
|
-
|
151
|
-
# Tests are first ordered by evaluating the results of the tests, then by tests names
|
152
|
-
# Tess which fail are given highest order
|
153
|
-
# Tests which are skipped are given second highest priority
|
154
|
-
def compare_tests(test_a, test_b)
|
155
|
-
return compare_tests_by_name(test_a, test_b) if test_fail_or_error?(test_a) && test_fail_or_error?(test_b)
|
156
|
-
|
157
|
-
return -1 if test_fail_or_error?(test_a) && !test_fail_or_error?(test_b)
|
158
|
-
return 1 if !test_fail_or_error?(test_a) && test_fail_or_error?(test_b)
|
159
|
-
|
160
|
-
return compare_tests_by_name(test_a, test_b) if test_a.skipped? && test_b.skipped?
|
161
|
-
return -1 if test_a.skipped? && !test_b.skipped?
|
162
|
-
return 1 if !test_a.skipped? && test_b.skipped?
|
163
|
-
|
164
|
-
compare_tests_by_name(test_a, test_b)
|
165
|
-
end
|
166
|
-
|
167
|
-
def test_fail_or_error?(test)
|
168
|
-
test.error? || test.failure
|
169
|
-
end
|
170
|
-
|
171
|
-
# based on analyze_suite from the JUnit reporter
|
172
|
-
def summarize_suite(suite, tests)
|
173
|
-
summary = Hash.new(0)
|
174
|
-
summary[:name] = suite.to_s
|
175
|
-
tests.each do |test|
|
176
|
-
summary[:"#{result(test)}_count"] += 1
|
177
|
-
summary[:assertion_count] += test.assertions
|
178
|
-
summary[:test_count] += 1
|
179
|
-
summary[:time] += test.time
|
180
|
-
end
|
181
|
-
summary[:has_errors_or_failures] = (summary[:fail_count] + summary[:error_count]) > 0
|
182
|
-
summary[:has_skipps] = summary[:skip_count] > 0
|
183
|
-
summary
|
184
|
-
end
|
185
|
-
|
186
|
-
# based on message_for(test) from the JUnit reporter
|
187
|
-
def message_for(test)
|
188
|
-
suite = test.class
|
189
|
-
name = test.name
|
190
|
-
e = test.failure
|
191
|
-
|
192
|
-
if test.passed?
|
193
|
-
nil
|
194
|
-
elsif test.skipped?
|
195
|
-
"Skipped:\n#{name}(#{suite}) [#{location(e)}]:\n#{e.message}\n"
|
196
|
-
elsif test.failure
|
197
|
-
"Failure:\n#{name}(#{suite}) [#{location(e)}]:\n#{e.message}\n"
|
198
|
-
elsif test.error?
|
199
|
-
"Error:\n#{name}(#{suite}):\n#{e.message}"
|
200
|
-
end
|
201
|
-
end
|
202
|
-
|
203
|
-
# taken from the JUnit reporter
|
204
|
-
def location(exception)
|
205
|
-
last_before_assertion = ''
|
206
|
-
exception.backtrace.reverse_each do |s|
|
207
|
-
break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
|
208
|
-
last_before_assertion = s
|
209
|
-
end
|
210
|
-
last_before_assertion.sub(/:in .*$/, '')
|
211
|
-
end
|
212
|
-
|
213
|
-
def total_time_to_hms
|
214
|
-
return ('%.2fs' % total_time) if total_time < 1
|
215
|
-
|
216
|
-
hours = (total_time / (60 * 60)).round
|
217
|
-
minutes = ((total_time / 60) % 60).round.to_s.rjust(2, '0')
|
218
|
-
seconds = (total_time % 60).round.to_s.rjust(2, '0')
|
219
|
-
|
220
|
-
"#{hours}h#{minutes}m#{seconds}s"
|
221
|
-
end
|
222
|
-
end
|
223
|
-
end
|
224
|
-
end
|
1
|
+
require 'builder'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
module Minitest
|
6
|
+
module Reporters
|
7
|
+
# A reporter for generating HTML test reports
|
8
|
+
# This is recommended to be used with a CI server, where the report is kept as an artifact and is accessible via
|
9
|
+
# a shared link
|
10
|
+
#
|
11
|
+
# The reporter sorts the results alphabetically and then by results
|
12
|
+
# so that failing and skipped tests are at the top.
|
13
|
+
#
|
14
|
+
# When using Minitest Specs, the number prefix is dropped from the name of the test so that it reads well
|
15
|
+
#
|
16
|
+
# On each test run all files in the reports directory are deleted, this prevents a build up of old reports
|
17
|
+
#
|
18
|
+
# The report is generated using ERB. A custom ERB template can be provided but it is not required
|
19
|
+
# The default ERB template uses JQuery and Bootstrap, both of these are included by referencing the CDN sites
|
20
|
+
class HtmlReporter < BaseReporter
|
21
|
+
# The title of the report
|
22
|
+
attr_reader :title
|
23
|
+
|
24
|
+
# The number of tests that passed
|
25
|
+
def passes
|
26
|
+
count - failures - errors - skips
|
27
|
+
end
|
28
|
+
|
29
|
+
# The percentage of tests that passed, calculated in a way that avoids rounding errors
|
30
|
+
def percent_passes
|
31
|
+
100 - percent_skipps - percent_errors_failures
|
32
|
+
end
|
33
|
+
|
34
|
+
# The percentage of tests that were skipped
|
35
|
+
def percent_skipps
|
36
|
+
(skips / count.to_f * 100).to_i
|
37
|
+
end
|
38
|
+
|
39
|
+
# The percentage of tests that failed
|
40
|
+
def percent_errors_failures
|
41
|
+
((errors + failures) / count.to_f * 100).to_i
|
42
|
+
end
|
43
|
+
|
44
|
+
# Trims off the number prefix on test names when using Minitest Specs
|
45
|
+
def friendly_name(test)
|
46
|
+
groups = test.name.scan(/(test_\d+_)(.*)/i)
|
47
|
+
return test.name if groups.empty?
|
48
|
+
"it #{groups[0][1]}"
|
49
|
+
end
|
50
|
+
|
51
|
+
# The constructor takes a hash, and uses the following keys:
|
52
|
+
# :title - the title that will be used in the report, defaults to 'Test Results'
|
53
|
+
# :reports_dir - the directory the reports should be written to, defaults to 'test/html_reports'
|
54
|
+
# :erb_template - the path to a custom ERB template, defaults to the supplied ERB template
|
55
|
+
# :mode - Useful for debugging, :terse suppresses errors and is the default, :verbose lets errors bubble up
|
56
|
+
# :output_filename - the report's filename, defaults to 'index.html'
|
57
|
+
def initialize(args = {})
|
58
|
+
super({})
|
59
|
+
|
60
|
+
defaults = {
|
61
|
+
:title => 'Test Results',
|
62
|
+
:erb_template => "#{File.dirname(__FILE__)}/../templates/index.html.erb",
|
63
|
+
:reports_dir => 'test/html_reports',
|
64
|
+
:mode => :safe,
|
65
|
+
:output_filename => 'index.html',
|
66
|
+
}
|
67
|
+
|
68
|
+
settings = defaults.merge(args)
|
69
|
+
|
70
|
+
@mode = settings[:mode]
|
71
|
+
@title = settings[:title]
|
72
|
+
@erb_template = settings[:erb_template]
|
73
|
+
@output_filename = settings[:output_filename]
|
74
|
+
reports_dir = settings[:reports_dir]
|
75
|
+
|
76
|
+
@reports_path = File.absolute_path(reports_dir)
|
77
|
+
end
|
78
|
+
|
79
|
+
def start
|
80
|
+
super
|
81
|
+
|
82
|
+
puts "Emptying #{@reports_path}"
|
83
|
+
FileUtils.mkdir_p(@reports_path)
|
84
|
+
File.delete(html_file) if File.exist?(html_file)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Called by the framework to generate the report
|
88
|
+
def report
|
89
|
+
super
|
90
|
+
|
91
|
+
begin
|
92
|
+
puts "Writing HTML reports to #{@reports_path}"
|
93
|
+
erb_str = File.read(@erb_template)
|
94
|
+
renderer = ERB.new(erb_str)
|
95
|
+
|
96
|
+
tests_by_suites = tests.group_by { |test| test_class(test) } # taken from the JUnit reporter
|
97
|
+
|
98
|
+
suites = tests_by_suites.map do |suite, tests|
|
99
|
+
suite_summary = summarize_suite(suite, tests)
|
100
|
+
suite_summary[:tests] = tests.sort { |a, b| compare_tests(a, b) }
|
101
|
+
suite_summary
|
102
|
+
end
|
103
|
+
|
104
|
+
suites.sort! { |a, b| compare_suites(a, b) }
|
105
|
+
|
106
|
+
result = renderer.result(binding)
|
107
|
+
File.open(html_file, 'w') do |f|
|
108
|
+
f.write(result)
|
109
|
+
end
|
110
|
+
|
111
|
+
# rubocop:disable Lint/RescueException
|
112
|
+
rescue Exception => e
|
113
|
+
puts 'There was an error writing the HTML report'
|
114
|
+
puts 'This may have been caused by cancelling the test run'
|
115
|
+
puts 'Use mode => :verbose in the HTML reporters constructor to see more detail' if @mode == :terse
|
116
|
+
puts 'Use mode => :terse in the HTML reporters constructor to see less detail' if @mode != :terse
|
117
|
+
raise e if @mode != :terse
|
118
|
+
end
|
119
|
+
# rubocop:enable Lint/RescueException
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def html_file
|
125
|
+
"#{@reports_path}/#{@output_filename}"
|
126
|
+
end
|
127
|
+
|
128
|
+
def compare_suites_by_name(suite_a, suite_b)
|
129
|
+
suite_a[:name] <=> suite_b[:name]
|
130
|
+
end
|
131
|
+
|
132
|
+
def compare_tests_by_name(test_a, test_b)
|
133
|
+
friendly_name(test_a) <=> friendly_name(test_b)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Test suites are first ordered by evaluating the results of the tests, then by test suite name
|
137
|
+
# Test suites which have failing tests are given highest order
|
138
|
+
# Tests suites which have skipped tests are given second highest priority
|
139
|
+
def compare_suites(suite_a, suite_b)
|
140
|
+
return compare_suites_by_name(suite_a, suite_b) if suite_a[:has_errors_or_failures] && suite_b[:has_errors_or_failures]
|
141
|
+
return -1 if suite_a[:has_errors_or_failures] && !suite_b[:has_errors_or_failures]
|
142
|
+
return 1 if !suite_a[:has_errors_or_failures] && suite_b[:has_errors_or_failures]
|
143
|
+
|
144
|
+
return compare_suites_by_name(suite_a, suite_b) if suite_a[:has_skipps] && suite_b[:has_skipps]
|
145
|
+
return -1 if suite_a[:has_skipps] && !suite_b[:has_skipps]
|
146
|
+
return 1 if !suite_a[:has_skipps] && suite_b[:has_skipps]
|
147
|
+
|
148
|
+
compare_suites_by_name(suite_a, suite_b)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Tests are first ordered by evaluating the results of the tests, then by tests names
|
152
|
+
# Tess which fail are given highest order
|
153
|
+
# Tests which are skipped are given second highest priority
|
154
|
+
def compare_tests(test_a, test_b)
|
155
|
+
return compare_tests_by_name(test_a, test_b) if test_fail_or_error?(test_a) && test_fail_or_error?(test_b)
|
156
|
+
|
157
|
+
return -1 if test_fail_or_error?(test_a) && !test_fail_or_error?(test_b)
|
158
|
+
return 1 if !test_fail_or_error?(test_a) && test_fail_or_error?(test_b)
|
159
|
+
|
160
|
+
return compare_tests_by_name(test_a, test_b) if test_a.skipped? && test_b.skipped?
|
161
|
+
return -1 if test_a.skipped? && !test_b.skipped?
|
162
|
+
return 1 if !test_a.skipped? && test_b.skipped?
|
163
|
+
|
164
|
+
compare_tests_by_name(test_a, test_b)
|
165
|
+
end
|
166
|
+
|
167
|
+
def test_fail_or_error?(test)
|
168
|
+
test.error? || test.failure
|
169
|
+
end
|
170
|
+
|
171
|
+
# based on analyze_suite from the JUnit reporter
|
172
|
+
def summarize_suite(suite, tests)
|
173
|
+
summary = Hash.new(0)
|
174
|
+
summary[:name] = suite.to_s
|
175
|
+
tests.each do |test|
|
176
|
+
summary[:"#{result(test)}_count"] += 1
|
177
|
+
summary[:assertion_count] += test.assertions
|
178
|
+
summary[:test_count] += 1
|
179
|
+
summary[:time] += test.time
|
180
|
+
end
|
181
|
+
summary[:has_errors_or_failures] = (summary[:fail_count] + summary[:error_count]) > 0
|
182
|
+
summary[:has_skipps] = summary[:skip_count] > 0
|
183
|
+
summary
|
184
|
+
end
|
185
|
+
|
186
|
+
# based on message_for(test) from the JUnit reporter
|
187
|
+
def message_for(test)
|
188
|
+
suite = test.class
|
189
|
+
name = test.name
|
190
|
+
e = test.failure
|
191
|
+
|
192
|
+
if test.passed?
|
193
|
+
nil
|
194
|
+
elsif test.skipped?
|
195
|
+
"Skipped:\n#{name}(#{suite}) [#{location(e)}]:\n#{e.message}\n"
|
196
|
+
elsif test.failure
|
197
|
+
"Failure:\n#{name}(#{suite}) [#{location(e)}]:\n#{e.message}\n"
|
198
|
+
elsif test.error?
|
199
|
+
"Error:\n#{name}(#{suite}):\n#{e.message}"
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# taken from the JUnit reporter
|
204
|
+
def location(exception)
|
205
|
+
last_before_assertion = ''
|
206
|
+
exception.backtrace.reverse_each do |s|
|
207
|
+
break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
|
208
|
+
last_before_assertion = s
|
209
|
+
end
|
210
|
+
last_before_assertion.sub(/:in .*$/, '')
|
211
|
+
end
|
212
|
+
|
213
|
+
def total_time_to_hms
|
214
|
+
return ('%.2fs' % total_time) if total_time < 1
|
215
|
+
|
216
|
+
hours = (total_time / (60 * 60)).round
|
217
|
+
minutes = ((total_time / 60) % 60).round.to_s.rjust(2, '0')
|
218
|
+
seconds = (total_time % 60).round.to_s.rjust(2, '0')
|
219
|
+
|
220
|
+
"#{hours}h#{minutes}m#{seconds}s"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -1,164 +1,168 @@
|
|
1
|
-
require 'builder'
|
2
|
-
require 'fileutils'
|
3
|
-
require 'pathname'
|
4
|
-
module Minitest
|
5
|
-
module Reporters
|
6
|
-
# A reporter for writing JUnit test reports
|
7
|
-
# Intended for easy integration with CI servers - tested on JetBrains TeamCity
|
8
|
-
#
|
9
|
-
# Inspired by ci_reporter (see https://github.com/nicksieger/ci_reporter)
|
10
|
-
# Also inspired by Marc Seeger's attempt at producing a JUnitReporter (see https://github.com/rb2k/minitest-reporters/commit/e13d95b5f884453a9c77f62bc5cba3fa1df30ef5)
|
11
|
-
# Also inspired by minitest-ci (see https://github.com/bhenderson/minitest-ci)
|
12
|
-
class JUnitReporter < BaseReporter
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
elsif test.
|
107
|
-
xml.
|
108
|
-
xml.text!(message_for(test))
|
109
|
-
end
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
elsif test.
|
125
|
-
"
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
1
|
+
require 'builder'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'pathname'
|
4
|
+
module Minitest
|
5
|
+
module Reporters
|
6
|
+
# A reporter for writing JUnit test reports
|
7
|
+
# Intended for easy integration with CI servers - tested on JetBrains TeamCity
|
8
|
+
#
|
9
|
+
# Inspired by ci_reporter (see https://github.com/nicksieger/ci_reporter)
|
10
|
+
# Also inspired by Marc Seeger's attempt at producing a JUnitReporter (see https://github.com/rb2k/minitest-reporters/commit/e13d95b5f884453a9c77f62bc5cba3fa1df30ef5)
|
11
|
+
# Also inspired by minitest-ci (see https://github.com/bhenderson/minitest-ci)
|
12
|
+
class JUnitReporter < BaseReporter
|
13
|
+
DEFAULT_REPORTS_DIR = "test/reports".freeze
|
14
|
+
|
15
|
+
attr_reader :reports_path
|
16
|
+
|
17
|
+
def initialize(reports_dir = DEFAULT_REPORTS_DIR, empty = true, options = {})
|
18
|
+
super({})
|
19
|
+
@reports_path = File.absolute_path(ENV.fetch("MINITEST_REPORTERS_REPORTS_DIR", reports_dir))
|
20
|
+
@single_file = options[:single_file]
|
21
|
+
@base_path = options[:base_path] || Dir.pwd
|
22
|
+
|
23
|
+
if empty
|
24
|
+
puts "Emptying #{@reports_path}"
|
25
|
+
FileUtils.mkdir_p(@reports_path)
|
26
|
+
File.delete(*Dir.glob("#{@reports_path}/TEST-*.xml"))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def report
|
31
|
+
super
|
32
|
+
|
33
|
+
puts "Writing XML reports to #{@reports_path}"
|
34
|
+
suites = tests.group_by { |test|
|
35
|
+
test_class(test)
|
36
|
+
}
|
37
|
+
|
38
|
+
if @single_file
|
39
|
+
xml = Builder::XmlMarkup.new(:indent => 2)
|
40
|
+
xml.instruct!
|
41
|
+
xml.testsuites do
|
42
|
+
suites.each do |suite, tests|
|
43
|
+
parse_xml_for(xml, suite, tests)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
File.open(filename_for('minitest'), "w") { |file| file << xml.target! }
|
47
|
+
else
|
48
|
+
suites.each do |suite, tests|
|
49
|
+
xml = Builder::XmlMarkup.new(:indent => 2)
|
50
|
+
xml.instruct!
|
51
|
+
xml.testsuites do
|
52
|
+
parse_xml_for(xml, suite, tests)
|
53
|
+
end
|
54
|
+
File.open(filename_for(suite), "w") { |file| file << xml.target! }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def get_relative_path(result)
|
60
|
+
file_path = Pathname.new(get_source_location(result).first)
|
61
|
+
base_path = Pathname.new(@base_path)
|
62
|
+
file_path.relative_path_from(base_path) if file_path.absolute?
|
63
|
+
file_path
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def get_source_location(result)
|
69
|
+
if result.respond_to? :klass
|
70
|
+
result.source_location
|
71
|
+
else
|
72
|
+
result.method(result.name).source_location
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def parse_xml_for(xml, suite, tests)
|
77
|
+
suite_result = analyze_suite(tests)
|
78
|
+
file_path = get_relative_path(tests.first)
|
79
|
+
|
80
|
+
xml.testsuite(:name => suite, :filepath => file_path,
|
81
|
+
:skipped => suite_result[:skip_count], :failures => suite_result[:fail_count],
|
82
|
+
:errors => suite_result[:error_count], :tests => suite_result[:test_count],
|
83
|
+
:assertions => suite_result[:assertion_count], :time => suite_result[:time]) do
|
84
|
+
tests.each do |test|
|
85
|
+
lineno = get_source_location(test).last
|
86
|
+
xml.testcase(:name => test.name, :lineno => lineno, :classname => suite, :assertions => test.assertions,
|
87
|
+
:time => test.time) do
|
88
|
+
xml << xml_message_for(test) unless test.passed?
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def xml_message_for(test)
|
95
|
+
# This is a trick lifted from ci_reporter
|
96
|
+
xml = Builder::XmlMarkup.new(:indent => 2, :margin => 2)
|
97
|
+
|
98
|
+
def xml.trunc!(txt)
|
99
|
+
txt.sub(/\n.*/m, '...')
|
100
|
+
end
|
101
|
+
|
102
|
+
e = test.failure
|
103
|
+
|
104
|
+
if test.skipped?
|
105
|
+
xml.skipped(:type => test.name)
|
106
|
+
elsif test.error?
|
107
|
+
xml.error(:type => test.name, :message => xml.trunc!(e.message)) do
|
108
|
+
xml.text!(message_for(test))
|
109
|
+
end
|
110
|
+
elsif test.failure
|
111
|
+
xml.failure(:type => test.name, :message => xml.trunc!(e.message)) do
|
112
|
+
xml.text!(message_for(test))
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def message_for(test)
|
118
|
+
suite = test.class
|
119
|
+
name = test.name
|
120
|
+
e = test.failure
|
121
|
+
|
122
|
+
if test.passed?
|
123
|
+
nil
|
124
|
+
elsif test.skipped?
|
125
|
+
"Skipped:\n#{name}(#{suite}) [#{location(e)}]:\n#{e.message}\n"
|
126
|
+
elsif test.failure
|
127
|
+
"Failure:\n#{name}(#{suite}) [#{location(e)}]:\n#{e.message}\n"
|
128
|
+
elsif test.error?
|
129
|
+
"Error:\n#{name}(#{suite}):\n#{e.message}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def location(exception)
|
134
|
+
last_before_assertion = ''
|
135
|
+
exception.backtrace.reverse_each do |s|
|
136
|
+
break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
|
137
|
+
last_before_assertion = s
|
138
|
+
end
|
139
|
+
last_before_assertion.sub(/:in .*$/, '')
|
140
|
+
end
|
141
|
+
|
142
|
+
def analyze_suite(tests)
|
143
|
+
result = Hash.new(0)
|
144
|
+
result[:time] = 0
|
145
|
+
tests.each do |test|
|
146
|
+
result[:"#{result(test)}_count"] += 1
|
147
|
+
result[:assertion_count] += test.assertions
|
148
|
+
result[:test_count] += 1
|
149
|
+
result[:time] += test.time
|
150
|
+
end
|
151
|
+
result
|
152
|
+
end
|
153
|
+
|
154
|
+
def filename_for(suite)
|
155
|
+
file_counter = 0
|
156
|
+
# restrict max filename length, to be kind to filesystems
|
157
|
+
suite_name = suite.to_s[0..240].gsub(/[^a-zA-Z0-9]+/, '-')
|
158
|
+
filename = "TEST-#{suite_name}.xml"
|
159
|
+
while File.exist?(File.join(@reports_path, filename)) # restrict number of tries, to avoid infinite loops
|
160
|
+
file_counter += 1
|
161
|
+
filename = "TEST-#{suite_name}-#{file_counter}.xml"
|
162
|
+
puts "Too many duplicate files, overwriting earlier report #{filename}" and break if file_counter >= 99
|
163
|
+
end
|
164
|
+
File.join(@reports_path, filename)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|