minitest-bender 0.0.3 → 1.0.0

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -1
  3. data/Gemfile +1 -1
  4. data/README.md +37 -6
  5. data/lib/minitest-bender/colorizer.rb +61 -0
  6. data/lib/minitest-bender/configuration.rb +174 -0
  7. data/lib/minitest-bender/printers/plain.rb +29 -0
  8. data/lib/minitest-bender/printers/with_progress_bar.rb +115 -0
  9. data/lib/minitest-bender/recorders/grouped_icons.rb +36 -0
  10. data/lib/minitest-bender/recorders/icons.rb +26 -0
  11. data/lib/minitest-bender/recorders/none.rb +17 -0
  12. data/lib/minitest-bender/recorders/progress.rb +25 -0
  13. data/lib/minitest-bender/recorders/progress_groups.rb +48 -0
  14. data/lib/minitest-bender/recorders/progress_groups_and_issues.rb +55 -0
  15. data/lib/minitest-bender/recorders/progress_issues.rb +32 -0
  16. data/lib/minitest-bender/recorders/progress_verbose.rb +34 -0
  17. data/lib/minitest-bender/result_context.rb +39 -0
  18. data/lib/minitest-bender/result_factory.rb +5 -0
  19. data/lib/minitest-bender/results/base.rb +94 -38
  20. data/lib/minitest-bender/results/expectation.rb +10 -8
  21. data/lib/minitest-bender/results/test.rb +21 -8
  22. data/lib/minitest-bender/sections/activity.rb +95 -0
  23. data/lib/minitest-bender/sections/issues.rb +22 -0
  24. data/lib/minitest-bender/sections/sorted_overview.rb +115 -0
  25. data/lib/minitest-bender/sections/suite_status.rb +72 -0
  26. data/lib/minitest-bender/sections/time_ranking.rb +49 -0
  27. data/lib/minitest-bender/states/base.rb +76 -19
  28. data/lib/minitest-bender/states/failing.rb +11 -8
  29. data/lib/minitest-bender/states/passing.rb +19 -8
  30. data/lib/minitest-bender/states/raising.rb +42 -20
  31. data/lib/minitest-bender/states/skipped.rb +12 -9
  32. data/lib/minitest-bender/utils.rb +26 -0
  33. data/lib/minitest-bender/version.rb +1 -1
  34. data/lib/minitest/bender.rb +166 -77
  35. data/lib/minitest/bender_plugin.rb +49 -3
  36. data/lib/minitest_bender.rb +23 -5
  37. data/minitest-bender.gemspec +6 -6
  38. metadata +39 -22
@@ -1,18 +1,21 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
1
4
  module MinitestBender
2
5
  module States
3
6
  class Failing < Base
4
- COLOR = :red_500
5
- LABEL = 'FAILED'.freeze
6
- GROUP_LABEL = 'FAILURES'.freeze
7
+ COLOR = :fail
8
+ LABEL = 'FAILED'
9
+ GROUP_LABEL = 'FAILURES'
10
+ ICON = '✖'
7
11
 
8
12
  def formatted_message(result)
9
- @formatted_message ||= colored(location(result))
13
+ colored(location(result))
10
14
  end
11
15
 
12
- def summary_message(results)
13
- filtered_results = only_with_this_state(results)
14
- return '' if filtered_results.empty?
15
- colored("#{filtered_results.size} failed")
16
+ def summary_message
17
+ return '' if results.empty?
18
+ colored("#{results.size} failed")
16
19
  end
17
20
  end
18
21
  end
@@ -1,22 +1,33 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
1
4
  module MinitestBender
2
5
  module States
3
6
  class Passing < Base
4
- COLOR = :green_500
5
- LABEL = 'PASSED'.freeze
6
- GROUP_LABEL = 'PASSING'.freeze
7
+ COLOR = :pass
8
+ LABEL = 'PASSED'
9
+ GROUP_LABEL = 'PASSING'
10
+ ICON = '✔'
7
11
 
8
12
  def formatted_message(_result)
9
13
  ''
10
14
  end
11
15
 
12
- def print_details(_io, _results)
16
+ def print_details(_io)
13
17
  :no_details
14
18
  end
15
19
 
16
- def summary_message(results)
17
- filtered_results = only_with_this_state(results)
18
- return '' if filtered_results.empty?
19
- colored("#{filtered_results.size} passed")
20
+ def detail_lines(_result)
21
+ []
22
+ end
23
+
24
+ def detail_lines_without_header(_result)
25
+ []
26
+ end
27
+
28
+ def summary_message
29
+ return '' if results.empty?
30
+ colored("#{results.size} passed")
20
31
  end
21
32
  end
22
33
  end
@@ -1,46 +1,68 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
1
4
  module MinitestBender
2
5
  module States
3
6
  class Raising < Base
4
- COLOR = :amber_300
5
- LABEL = 'RAISED'.freeze
6
- GROUP_LABEL = 'ERRORS'.freeze
7
+ COLOR = :error
8
+ LABEL = 'RAISED'
9
+ GROUP_LABEL = 'ERRORS'
10
+ ICON = '💥'
7
11
 
8
12
  def formatted_message(result)
9
- @formatted_message ||= colored(detailed_error_message(result))
13
+ colored(error_message(result))
10
14
  end
11
15
 
12
- def summary_message(results)
13
- filtered_results = only_with_this_state(results)
14
- return '' if filtered_results.empty?
15
- colored("#{filtered_results.size} raised an error")
16
+ def summary_message
17
+ return '' if results.empty?
18
+ colored("#{results.size} raised an error")
16
19
  end
17
20
 
18
21
  def test_location(result)
19
- backtrace_line = backtrace(result).select { |line| line =~ /\/test\/|\/spec\// }.last
20
- Utils.relative_path(backtrace_line).split(':').first
22
+ Utils.relative_path(result.file_path)
21
23
  end
22
24
 
23
25
  private
24
26
 
25
- def do_print_details(io, result, padding)
26
- io.puts "#{padding}#{colored(error_message(result))}"
27
+ def inner_detail_lines(result, padding)
28
+ lines = []
29
+ message = colored(error_message(result))
30
+ lines << "#{padding}#{message.gsub("\n", "\n#{padding}")}"
27
31
  backtrace(result).each do |line|
28
- io.puts "#{padding}#{Colorin.brown_400(line)}"
32
+ adjusted_line = Utils.with_home_shorthand(line)
33
+ lines << "#{padding}#{Colorizer.colorize(adjusted_line, :backtrace)}"
29
34
  end
35
+ lines
30
36
  end
31
37
 
32
38
  def error_message(result)
33
- exception = result.failures[0].exception
34
- "#{exception.class}: #{exception.message}"
39
+ error = result.failures[0].error
40
+ "#{error.class}: #{error.message}"
35
41
  end
36
42
 
37
- def detailed_error_message(result)
38
- details = Utils.relative_path(backtrace(result)[0])
39
- "#{error_message(result)}\n (#{details})"
43
+ def backtrace(result)
44
+ case backtrace_view
45
+ when :user
46
+ user_backtrace(result)
47
+ when :full
48
+ full_backtrace(result)
49
+ else
50
+ raise "unknown backtrace view: #{backtrace_view}"
51
+ end
40
52
  end
41
53
 
42
- def backtrace(result)
43
- result.failures[0].backtrace
54
+ def backtrace_view
55
+ Minitest::Bender.configuration.backtrace_view
56
+ end
57
+
58
+ def user_backtrace(result)
59
+ full_backtrace(result).take_while do |line|
60
+ line !~ %r{minitest/test\.rb}
61
+ end
62
+ end
63
+
64
+ def full_backtrace(result)
65
+ result.failures[0].backtrace || []
44
66
  end
45
67
  end
46
68
  end
@@ -1,20 +1,23 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
1
4
  module MinitestBender
2
5
  module States
3
6
  class Skipped < Base
4
- COLOR = :cyan_300
5
- LABEL = 'SKIPPED'.freeze
6
- GROUP_LABEL = 'SKIPS'.freeze
7
+ COLOR = :skip
8
+ LABEL = 'SKIPPED'
9
+ GROUP_LABEL = 'SKIPS'
10
+ ICON = '○'
7
11
 
8
12
  def formatted_message(result)
9
- @formatted_message ||= colored(result.failures[0].message)
13
+ colored(result.failures[0].message)
10
14
  end
11
15
 
12
- def summary_message(results)
13
- filtered_results = only_with_this_state(results)
14
- return '' if filtered_results.empty?
15
- skipped_count = filtered_results.size
16
+ def summary_message
17
+ return '' if results.empty?
18
+ skipped_count = results.size
16
19
  auxiliary_verb = skipped_count == 1 ? 'was' : 'were'
17
- colored("#{filtered_results.size} #{auxiliary_verb} skipped")
20
+ colored("#{skipped_count} #{auxiliary_verb} skipped")
18
21
  end
19
22
  end
20
23
  end
@@ -1,7 +1,33 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MinitestBender
2
4
  module Utils
3
5
  def self.relative_path(full_path)
4
6
  full_path.gsub("#{Dir.pwd}/", '')
5
7
  end
8
+
9
+ def self.with_home_shorthand(full_path)
10
+ if ENV['HOME'].to_s.start_with?('/home/')
11
+ full_path.sub(ENV['HOME'], '~')
12
+ else
13
+ full_path
14
+ end
15
+ end
16
+
17
+ def self.english_join(strings)
18
+ strings.reject(&:empty?).join(', ').gsub(/(.*), /, '\1 and ')
19
+ end
20
+
21
+ def self.with_symbolized_keys(hash)
22
+ hash.each_with_object({}) do |(k, v), h|
23
+ h[k.to_sym] = v
24
+ end
25
+ end
26
+
27
+ def self.without_nil_values(hash)
28
+ hash.each_with_object({}) do |(k, v), h|
29
+ h[k] = v unless v.nil?
30
+ end
31
+ end
6
32
  end
7
33
  end
@@ -1,3 +1,3 @@
1
1
  module MinitestBender
2
- VERSION = '0.0.3'.freeze
2
+ VERSION = '1.0.0'.freeze
3
3
  end
@@ -3,78 +3,130 @@ require 'minitest_bender'
3
3
 
4
4
  module Minitest
5
5
  class Bender < AbstractReporter
6
- attr_reader :io, :options, :previous_context, :results, :started_at
6
+ Colorizer = MinitestBender::Colorizer
7
+
8
+ attr_accessor :io, :options
9
+ attr_reader :previous_context, :results, :results_by_context, :started_at
10
+
11
+ class << self
12
+ def enable!(client_config = {})
13
+ @is_enabled = true
14
+ configuration.add_client_config(client_config)
15
+ Colorizer.custom_colors = configuration.custom_colors
16
+ end
17
+
18
+ def enabled?
19
+ @is_enabled ||= false
20
+ end
21
+
22
+ def configuration
23
+ @configuration ||= MinitestBender::Configuration.new
24
+ end
25
+ end
7
26
 
8
27
  def initialize(io, options = {})
9
28
  @io = io
10
29
  @options = options
11
30
  @previous_context = nil
12
31
  @results = []
13
- @slowness_podium_is_relevant = false
32
+ @results_by_context = {}
33
+ @time_ranking_is_relevant = false
14
34
  end
15
35
 
16
36
  def start
17
37
  @started_at = Time.now
18
38
  io.puts
19
- io.puts Colorin.white("Minitest started at #{started_at}")
20
- io.puts Colorin.white("Options: #{options_args}")
39
+ io.puts Colorizer.colorize("Minitest started at #{started_at}", :normal)
40
+ io.puts Colorizer.colorize("Options: #{options_args}", :normal)
21
41
  io.puts
42
+ io.flush
22
43
  end
23
44
 
24
45
  def record(minitest_result)
46
+ flush_stdio
47
+
25
48
  result = MinitestBender.result_factory.create(minitest_result)
26
49
  results << result
27
50
 
28
51
  current_context = result.context
29
52
 
30
53
  if current_context != previous_context
31
- io.puts
32
- io.puts(result.header)
54
+ recorder.print_context_with_results(previous_context, results_by_context[previous_context]) unless previous_context.nil?
55
+ recorder.print_context(current_context)
33
56
  @previous_context = current_context
34
57
  end
35
58
 
36
- @slowness_podium_is_relevant = true if result.time > 0.01
59
+ (results_by_context[current_context] ||= []) << result
60
+
61
+ @time_ranking_is_relevant = true if result.time > 0.01
62
+
63
+ if run_count == total_tests_count
64
+ recorder.print_context_with_results(current_context, results_by_context[current_context])
65
+ end
37
66
 
38
- io.puts result.line_to_report
67
+ recorder.print_result(result)
68
+
69
+ io.flush
39
70
  end
40
71
 
41
72
  def passed?
42
- passed_count + skipped_count == test_count
73
+ passed_count + skipped_count == run_count
43
74
  end
44
75
 
45
76
  def report
46
- io.puts
47
- print_divider(:white)
48
-
49
- print_details
50
-
51
- if @slowness_podium_is_relevant && passed?
52
- print_slowness_podium
53
- io.puts
77
+ if results.empty?
78
+ print_no_tests_status
79
+ return
54
80
  end
55
81
 
56
- print_statistics
57
82
  io.puts
83
+ io.puts
84
+ print_divider(:normal)
58
85
 
59
- print_suite_status
86
+ print_sections
60
87
  end
61
88
 
62
89
  private
63
90
 
64
- def options_args
65
- options.fetch(:args, '(none)')
91
+ def configuration
92
+ self.class.configuration
66
93
  end
67
94
 
68
- def passed_without_skips?
69
- passed_count == test_count
95
+ def flush_stdio
96
+ # as we might already have some output from the test itself,
97
+ # make sure we see *all* of it before we report anything
98
+ STDOUT.flush
99
+ STDERR.flush
70
100
  end
71
101
 
72
- def run_all_tests?
73
- !options_args.include?('--name')
102
+ def options_args
103
+ options.fetch(:args, '(none)')
74
104
  end
75
105
 
76
- def test_count
77
- results.size
106
+ def recorder
107
+ @recorder ||= begin
108
+ recorder_sym = configuration.recorder
109
+ case recorder_sym
110
+ when :progress
111
+ MinitestBender::Recorders::Progress.new(io, total_tests_count)
112
+ when :progress_groups
113
+ MinitestBender::Recorders::ProgressGroups.new(io, total_tests_count)
114
+ when :progress_issues
115
+ MinitestBender::Recorders::ProgressIssues.new(io, total_tests_count)
116
+ when :progress_groups_and_issues
117
+ MinitestBender::Recorders::ProgressGroupsAndIssues.new(io, total_tests_count)
118
+ when :progress_verbose
119
+ MinitestBender::Recorders::ProgressVerbose.new(io, total_tests_count)
120
+ when :icons
121
+ MinitestBender::Recorders::Icons.new(io)
122
+ when :grouped_icons
123
+ MinitestBender::Recorders::GroupedIcons.new(io)
124
+ when :none
125
+ MinitestBender::Recorders::None.new
126
+ else
127
+ raise "unknown recorder: #{recorder_sym}"
128
+ end
129
+ end
78
130
  end
79
131
 
80
132
  def passed_count
@@ -85,74 +137,111 @@ module Minitest
85
137
  @skipped_count ||= results.count(&:skipped?)
86
138
  end
87
139
 
88
- def assertion_count
89
- @assertion_count ||= results.reduce(0) { |acum, result| acum + result.assertions }
90
- end
91
-
92
- def print_divider(color)
93
- io.puts(Colorin.public_send(color, ' _______________________').bold)
94
- io.puts
140
+ def run_count
141
+ results.size
95
142
  end
96
143
 
97
- def print_details
98
- states = MinitestBender.states.values
99
- symbols = states.map { |state| state.print_details(io, results) }
100
- io.puts unless symbols.all? { |symbol| symbol == :no_details }
101
- end
144
+ # Minitest should share this with reporters...
145
+ def total_tests_count
146
+ @total_tests_count ||= begin
147
+ filter = options[:filter] || '/./'
148
+ filter = Regexp.new($1) if filter.is_a?(String) && filter =~ %r%/(.*)/%
102
149
 
103
- def print_statistics
104
- total_tests = "#{test_count} tests"
105
- total_tests = total_tests.chop if test_count == 1
106
- formatted_total_tests = Colorin.blue_a700(total_tests)
150
+ exclude = options[:exclude]
151
+ exclude = Regexp.new($1) if exclude.is_a?(String) && exclude =~ %r%/(.*)/%
107
152
 
108
- total_assertions = "#{assertion_count} assertions"
109
- total_assertions = total_assertions.chop if assertion_count == 1
110
- formatted_total_assertions = Colorin.purple_400(total_assertions)
153
+ Minitest::Runnable.runnables.map do |runnable|
154
+ runnable.runnable_methods.count do |m|
155
+ (filter === m || filter === "#{runnable}##{m}") &&
156
+ !(exclude === m || exclude === "#{runnable}##{m}")
157
+ end
158
+ end.inject(:+)
159
+ end
160
+ end
111
161
 
112
- auxiliary_verb = test_count == 1 ? 'was' : 'were'
162
+ def print_no_tests_status
163
+ message = no_tests_message
164
+ padded_message = " #{message}"
165
+ io.puts(Colorizer.colorize(padded_message, :tests))
166
+ print_divider(:tests, message.length)
167
+ end
113
168
 
114
- total_time = (Time.now - started_at).round(3)
115
- formatted_total_time = Colorin.grey_700("#{total_time} seconds")
169
+ def no_tests_message
170
+ 'NO TESTS WERE RUN! (-_-)zzz'.freeze
171
+ end
116
172
 
117
- tests_rate = Colorin.grey_700("#{(test_count / total_time).round(4)} tests/s")
118
- assertions_rate = Colorin.grey_700("#{(assertion_count / total_time).round(4)} assertions/s")
173
+ def print_divider(color, line_length = 23)
174
+ io.puts(Colorizer.colorize(" #{'_' * line_length}", color, :bold))
175
+ io.puts
176
+ end
119
177
 
120
- io.puts " #{formatted_total_tests} with #{formatted_total_assertions} #{auxiliary_verb} run in #{formatted_total_time} (#{tests_rate}, #{assertions_rate})"
178
+ def print_sections
179
+ sections.each(&:print)
180
+ end
181
+
182
+ def sections
183
+ section_names.map do |section_name|
184
+ case section_name
185
+ when :overview
186
+ MinitestBender::Sections::SortedOverview.new(io, results_by_context)
187
+ when :time_ranking
188
+ MinitestBender::Sections::TimeRanking.new(io, time_ranking_size, results)
189
+ when :issues
190
+ MinitestBender::Sections::Issues.new(io)
191
+ when :activity
192
+ MinitestBender::Sections::Activity.new(io, started_at, results)
193
+ when :suite_status
194
+ MinitestBender::Sections::SuiteStatus.new(io, options, results, total_tests_count)
195
+ else
196
+ raise "unknown section: #{section_name}"
197
+ end
198
+ end
121
199
  end
122
200
 
123
- def print_suite_status
124
- all_passed_color = MinitestBender.passing_color
125
- final_divider_color = all_passed_color
201
+ def section_names
202
+ configuration.sections
203
+ end
126
204
 
127
- if passed_without_skips? && run_all_tests?
128
- message = Colorin.public_send(all_passed_color, ' ALL TESTS PASS! (^_^)/')
205
+ def time_ranking_size
206
+ if @time_ranking_is_relevant
207
+ configuration.time_ranking_size
129
208
  else
130
- messages = MinitestBender.states.values.map do |state|
131
- summary_message = state.summary_message(results)
132
- final_divider_color = state.color unless summary_message.empty?
133
- summary_message
134
- end
135
-
136
- message = " #{messages.reject(&:empty?).join(', ').gsub(/(.*), /, '\1 and ')}"
209
+ 0
137
210
  end
138
- io.puts(message)
139
-
140
- print_divider(final_divider_color)
141
211
  end
212
+ end
142
213
 
143
- def print_slowness_podium
144
- results.sort_by! { |r| -r.time }
214
+ ##
215
+ # Compatibility with
216
+ # [minitest-reporters](https://github.com/kern/minitest-reporters)
217
+ #
218
+ # Given:
219
+ #
220
+ # ```
221
+ # require 'minitest/reporters'
222
+ # Minitest::Reporters.use!
223
+ # ```
224
+ #
225
+ # Bender can be selected with:
226
+ #
227
+ # ```
228
+ # MINITEST_REPORTER=BenderReporter rake test
229
+ # ```
230
+
231
+ module Reporters
232
+ class BenderReporter < Minitest::Bender
233
+ def initialize(options = {})
234
+ super(options.fetch(:io, $stdout), options)
235
+ Minitest::Bender.enable!
236
+ end
145
237
 
146
- io.puts(formatted_slowness_podium_label)
147
- io.puts
148
- results.take(3).each_with_index do |result, i|
149
- number = "#{i + 1})".ljust(4)
150
- io.puts " #{number}#{result.line_for_slowness_podium}"
238
+ def add_defaults(defaults)
239
+ @options = defaults.merge(options)
151
240
  end
152
- end
153
241
 
154
- def formatted_slowness_podium_label
155
- " #{Colorin.grey_700('SLOWNESS PODIUM').bold.underline}"
242
+ def before_test(_test_cls); end
243
+
244
+ def after_test(_test_cls); end
156
245
  end
157
246
  end
158
247
  end