minitest-reporters 1.3.0 → 1.3.1.beta1

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