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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +28 -27
  3. data/.rubocop.yml +77 -77
  4. data/.ruby-gemset +1 -1
  5. data/.travis.yml +14 -14
  6. data/.yardopts +5 -5
  7. data/CHANGELOG.md +98 -93
  8. data/Gemfile +2 -2
  9. data/LICENSE +20 -20
  10. data/README.md +135 -135
  11. data/Rakefile +70 -70
  12. data/appveyor.yml +22 -22
  13. data/lib/minitest/extensible_backtrace_filter.rb +67 -67
  14. data/lib/minitest/minitest_reporter_plugin.rb +76 -76
  15. data/lib/minitest/old_activesupport_fix.rb +24 -24
  16. data/lib/minitest/relative_position.rb +26 -26
  17. data/lib/minitest/reporters.rb +91 -91
  18. data/lib/minitest/reporters/ansi.rb +30 -30
  19. data/lib/minitest/reporters/base_reporter.rb +136 -136
  20. data/lib/minitest/reporters/default_reporter.rb +228 -228
  21. data/lib/minitest/reporters/html_reporter.rb +224 -224
  22. data/lib/minitest/reporters/junit_reporter.rb +168 -164
  23. data/lib/minitest/reporters/mean_time_reporter.rb +388 -388
  24. data/lib/minitest/reporters/progress_reporter.rb +102 -96
  25. data/lib/minitest/reporters/ruby_mate_reporter.rb +54 -54
  26. data/lib/minitest/reporters/rubymine_reporter.rb +116 -116
  27. data/lib/minitest/reporters/spec_reporter.rb +61 -61
  28. data/lib/minitest/reporters/version.rb +5 -5
  29. data/lib/minitest/templates/index.html.erb +82 -82
  30. data/minitest-reporters.gemspec +31 -32
  31. data/test/fixtures/junit_filename_bug_example_test.rb +41 -41
  32. data/test/fixtures/mean_time_test.rb +36 -36
  33. data/test/fixtures/progress_detailed_skip_test.rb +8 -8
  34. data/test/fixtures/progress_test.rb +8 -8
  35. data/test/fixtures/sample_test.rb +15 -15
  36. data/test/fixtures/spec_test.rb +18 -18
  37. data/test/gallery/bad_test.rb +25 -25
  38. data/test/gallery/good_test.rb +14 -14
  39. data/test/integration/reporters/junit_reporter_test.rb +12 -12
  40. data/test/integration/reporters/mean_time_reporter_test.rb +7 -7
  41. data/test/integration/reporters/progress_reporter_test.rb +40 -40
  42. data/test/test_helper.rb +22 -22
  43. data/test/unit/minitest/extensible_backtrace_filter_test.rb +42 -42
  44. data/test/unit/minitest/junit_reporter_test.rb +46 -23
  45. data/test/unit/minitest/mean_time_reporter_unit_test.rb +149 -149
  46. data/test/unit/minitest/minitest_reporter_plugin_test.rb +14 -14
  47. data/test/unit/minitest/reporters_test.rb +65 -65
  48. data/test/unit/minitest/spec_reporter_test.rb +62 -62
  49. 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
- def initialize(reports_dir = "test/reports", empty = true, options = {})
14
- super({})
15
- @reports_path = File.absolute_path(reports_dir)
16
- @single_file = options[:single_file]
17
- @base_path = options[:base_path] || Dir.pwd
18
-
19
- if empty
20
- puts "Emptying #{@reports_path}"
21
- FileUtils.mkdir_p(@reports_path)
22
- File.delete(*Dir.glob("#{@reports_path}/TEST-*.xml"))
23
- end
24
- end
25
-
26
- def report
27
- super
28
-
29
- puts "Writing XML reports to #{@reports_path}"
30
- suites = tests.group_by { |test|
31
- test_class(test)
32
- }
33
-
34
- if @single_file
35
- xml = Builder::XmlMarkup.new(:indent => 2)
36
- xml.instruct!
37
- xml.testsuites do
38
- suites.each do |suite, tests|
39
- parse_xml_for(xml, suite, tests)
40
- end
41
- end
42
- File.open(filename_for('minitest'), "w") { |file| file << xml.target! }
43
- else
44
- suites.each do |suite, tests|
45
- xml = Builder::XmlMarkup.new(:indent => 2)
46
- xml.instruct!
47
- xml.testsuites do
48
- parse_xml_for(xml, suite, tests)
49
- end
50
- File.open(filename_for(suite), "w") { |file| file << xml.target! }
51
- end
52
- end
53
- end
54
-
55
- def get_relative_path(result)
56
- file_path = Pathname.new(get_source_location(result).first)
57
- base_path = Pathname.new(@base_path)
58
- file_path.relative_path_from(base_path) if file_path.absolute?
59
- file_path
60
- end
61
-
62
- private
63
-
64
- def get_source_location(result)
65
- if result.respond_to? :klass
66
- result.source_location
67
- else
68
- result.method(result.name).source_location
69
- end
70
- end
71
-
72
- def parse_xml_for(xml, suite, tests)
73
- suite_result = analyze_suite(tests)
74
- file_path = get_relative_path(tests.first)
75
-
76
- xml.testsuite(:name => suite, :filepath => file_path,
77
- :skipped => suite_result[:skip_count], :failures => suite_result[:fail_count],
78
- :errors => suite_result[:error_count], :tests => suite_result[:test_count],
79
- :assertions => suite_result[:assertion_count], :time => suite_result[:time]) do
80
- tests.each do |test|
81
- lineno = get_source_location(test).last
82
- xml.testcase(:name => test.name, :lineno => lineno, :classname => suite, :assertions => test.assertions,
83
- :time => test.time) do
84
- xml << xml_message_for(test) unless test.passed?
85
- end
86
- end
87
- end
88
- end
89
-
90
- def xml_message_for(test)
91
- # This is a trick lifted from ci_reporter
92
- xml = Builder::XmlMarkup.new(:indent => 2, :margin => 2)
93
-
94
- def xml.trunc!(txt)
95
- txt.sub(/\n.*/m, '...')
96
- end
97
-
98
- e = test.failure
99
-
100
- if test.skipped?
101
- xml.skipped(:type => test.name)
102
- elsif test.error?
103
- xml.error(:type => test.name, :message => xml.trunc!(e.message)) do
104
- xml.text!(message_for(test))
105
- end
106
- elsif test.failure
107
- xml.failure(:type => test.name, :message => xml.trunc!(e.message)) do
108
- xml.text!(message_for(test))
109
- end
110
- end
111
- end
112
-
113
- def message_for(test)
114
- suite = test.class
115
- name = test.name
116
- e = test.failure
117
-
118
- if test.passed?
119
- nil
120
- elsif test.skipped?
121
- "Skipped:\n#{name}(#{suite}) [#{location(e)}]:\n#{e.message}\n"
122
- elsif test.failure
123
- "Failure:\n#{name}(#{suite}) [#{location(e)}]:\n#{e.message}\n"
124
- elsif test.error?
125
- "Error:\n#{name}(#{suite}):\n#{e.message}"
126
- end
127
- end
128
-
129
- def location(exception)
130
- last_before_assertion = ''
131
- exception.backtrace.reverse_each do |s|
132
- break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
133
- last_before_assertion = s
134
- end
135
- last_before_assertion.sub(/:in .*$/, '')
136
- end
137
-
138
- def analyze_suite(tests)
139
- result = Hash.new(0)
140
- result[:time] = 0
141
- tests.each do |test|
142
- result[:"#{result(test)}_count"] += 1
143
- result[:assertion_count] += test.assertions
144
- result[:test_count] += 1
145
- result[:time] += test.time
146
- end
147
- result
148
- end
149
-
150
- def filename_for(suite)
151
- file_counter = 0
152
- # restrict max filename length, to be kind to filesystems
153
- suite_name = suite.to_s[0..240].gsub(/[^a-zA-Z0-9]+/, '-')
154
- filename = "TEST-#{suite_name}.xml"
155
- while File.exist?(File.join(@reports_path, filename)) # restrict number of tries, to avoid infinite loops
156
- file_counter += 1
157
- filename = "TEST-#{suite_name}-#{file_counter}.xml"
158
- puts "Too many duplicate files, overwriting earlier report #{filename}" and break if file_counter >= 99
159
- end
160
- File.join(@reports_path, filename)
161
- end
162
- end
163
- end
164
- end
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