minitest-heat 0.0.8 → 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,11 +3,13 @@
3
3
  module Minitest
4
4
  module Heat
5
5
  class Output
6
- class Issue
7
- attr_accessor :issue
6
+ # Formats issues to output based on the issue type
7
+ class Issue # rubocop:disable Metrics/ClassLength
8
+ attr_accessor :issue, :locations
8
9
 
9
10
  def initialize(issue)
10
11
  @issue = issue
12
+ @locations = issue.locations
11
13
  end
12
14
 
13
15
  def tokens
@@ -27,7 +29,6 @@ module Minitest
27
29
  [
28
30
  headline_tokens,
29
31
  test_location_tokens,
30
- location_tokens,
31
32
  summary_tokens,
32
33
  *backtrace_tokens,
33
34
  newline_tokens
@@ -78,93 +79,96 @@ module Minitest
78
79
  end
79
80
 
80
81
  def headline_tokens
81
- [[issue.type, issue.label], spacer_token, [:default, issue.test_name]]
82
+ [label_token(issue), spacer_token, [:default, test_name(issue)]]
83
+ end
84
+
85
+ def test_name(issue)
86
+ test_prefix = 'test_'
87
+ identifier = issue.test_identifier
88
+
89
+ if identifier.start_with?(test_prefix)
90
+ identifier.delete_prefix(test_prefix).gsub('_', ' ').capitalize
91
+ else
92
+ identifier
93
+ end
94
+ end
95
+
96
+ def label_token(issue)
97
+ [issue.type, issue_label(issue.type)]
98
+ end
99
+
100
+ def issue_label(issue_type)
101
+ case issue_type
102
+ when :error then 'Error'
103
+ when :broken then 'Broken Test'
104
+ when :failure then 'Failure'
105
+ when :skipped then 'Skipped'
106
+ when :slow then 'Passed but Slow'
107
+ when :painful then 'Passed but Very Slow'
108
+ when :passed then 'Success'
109
+ else 'Unknown'
110
+ end
82
111
  end
83
112
 
84
113
  def test_name_and_class_tokens
85
- [[:default, issue.test_class], *test_location_tokens ]
114
+ [[:default, issue.test_class], *test_location_tokens]
86
115
  end
87
116
 
88
117
  def backtrace_tokens
89
- backtrace = ::Minitest::Heat::Output::Backtrace.new(issue.location)
90
-
91
- backtrace.tokens
118
+ @backtrace_tokens ||= ::Minitest::Heat::Output::Backtrace.new(locations).tokens
92
119
  end
93
120
 
94
121
  def test_location_tokens
95
- [[:default, test_file_short_location], [:muted, ':'], [:default, issue.test_definition_line], arrow_token, [:default, issue.test_failure_line], [:muted, test_line_source]]
122
+ [
123
+ [:default, locations.test_definition.relative_filename],
124
+ [:muted, ':'],
125
+ [:default, locations.test_definition.line_number],
126
+ arrow_token,
127
+ [:default, locations.test_failure.line_number],
128
+ [:muted, "\n #{locations.test_failure.source_code.line.strip}"]
129
+ ]
96
130
  end
97
131
 
98
132
  def location_tokens
99
- [[:default, most_relevant_short_location], [:muted, ':'], [:default, issue.location.most_relevant_failure_line], [:muted, most_relevant_line_source]]
133
+ [
134
+ [:default, locations.most_relevant.relative_filename],
135
+ [:muted, ':'],
136
+ [:default, locations.most_relevant.line_number],
137
+ [:muted, "\n #{locations.most_relevant.source_code.line.strip}"]
138
+ ]
100
139
  end
101
140
 
102
141
  def source_tokens
103
- filename = issue.location.project_file
104
- line_number = issue.location.project_failure_line
105
-
106
- # source_code = ::Minitest::Heat::Output::SourceCode.new(filename, line_number, max_line_count: 1)
107
- # source_code.tokens
108
-
142
+ filename = locations.project.filename
143
+ line_number = locations.project.line_number
109
144
  source = Minitest::Heat::Source.new(filename, line_number: line_number)
145
+
110
146
  [[:muted, " #{Output::SYMBOLS[:arrow]} `#{source.line.strip}`"]]
111
147
  end
112
148
 
113
149
  def summary_tokens
114
- [[:italicized, issue.summary.delete_suffix("---------------")]]
150
+ [[:italicized, issue.summary.delete_suffix('---------------').strip]]
115
151
  end
116
152
 
117
153
  def slowness_summary_tokens
118
- [[:bold, issue.slowness], spacer_token, [:default, issue.short_location]]
119
- end
120
-
121
- def newline_tokens
122
- []
123
- end
124
-
125
- def most_relevant_short_location
126
- issue.location.most_relevant_file.to_s.delete_prefix("#{Dir.pwd}/")
127
- end
128
-
129
- def test_file_short_location
130
- issue.location.test_file.to_s.delete_prefix("#{Dir.pwd}/")
154
+ [
155
+ [:bold, slowness(issue)],
156
+ spacer_token,
157
+ [:default, locations.test_definition.relative_path],
158
+ [:default, locations.test_definition.filename],
159
+ [:muted, ':'],
160
+ [:default, locations.test_definition.line_number]
161
+ ]
131
162
  end
132
163
 
133
- def most_relevant_line_source
134
- filename = issue.location.project_file
135
- line_number = issue.location.project_failure_line
136
-
137
- source = Minitest::Heat::Source.new(filename, line_number: line_number)
138
- "\n #{source.line.strip}"
164
+ def slowness(issue)
165
+ "#{issue.execution_time.round(2)}s"
139
166
  end
140
167
 
141
- def test_line_source
142
- filename = issue.location.test_file
143
- line_number = issue.location.test_failure_line
144
-
145
- source = Minitest::Heat::Source.new(filename, line_number: line_number)
146
- "\n #{source.line.strip}"
168
+ def newline_tokens
169
+ []
147
170
  end
148
171
 
149
-
150
- # def failure_summary_tokens
151
- # return unless issue_summary_lines.any?
152
-
153
- # # Sometimes, the exception message is multiple lines, so this adjusts the lines to
154
- # # visually group them together a bit
155
- # if issue_summary_lines.one?
156
- # [[[:italicized, issue_summary_lines.first]]]
157
- # else
158
- # issue_summary_lines.map do |line|
159
- # [Output::TOKENS[:muted_lead], [:italicized, line]]
160
- # end
161
- # end
162
- # end
163
-
164
- # def issue_summary_lines
165
- # @issue_summary_lines ||= issue.summary.split("\n")
166
- # end
167
-
168
172
  def spacer_token
169
173
  Output::TOKENS[:spacer]
170
174
  end
@@ -172,7 +176,6 @@ module Minitest
172
176
  def arrow_token
173
177
  Output::TOKENS[:muted_arrow]
174
178
  end
175
-
176
179
  end
177
180
  end
178
181
  end
@@ -3,6 +3,7 @@
3
3
  module Minitest
4
4
  module Heat
5
5
  class Output
6
+ # Generates the tokens to output the resulting heat map
6
7
  class Map
7
8
  attr_accessor :results
8
9
 
@@ -12,16 +13,26 @@ module Minitest
12
13
  end
13
14
 
14
15
  def tokens
15
- map.file_hits.each do |hit|
16
- file_tokens = pathname(hit)
17
- line_number_tokens = line_numbers(hit)
16
+ results.heat_map.file_hits.each do |hit|
17
+ # If there's legitimate failures or errors, skips and slows aren't relevant
18
+ next unless relevant_issue_types?(hit)
18
19
 
19
- next if line_number_tokens.empty?
20
+ @tokens << [[:muted, ""]]
21
+ @tokens << file_summary_tokens(hit)
20
22
 
21
- @tokens << [
22
- *file_tokens,
23
- *line_number_tokens
24
- ]
23
+ repeats = repeated_line_numbers(hit)
24
+ next unless repeats.any?
25
+
26
+ repeats.each do |line_number|
27
+ @tokens << [[:muted, " Issues on Line #{line_number} initially triggered from these locations:"]]
28
+
29
+ traces = hit.lines[line_number.to_s]
30
+ sorted_traces = traces.sort_by { |trace| trace.locations.last.line_number }
31
+
32
+ sorted_traces.each do |trace|
33
+ @tokens << origination_location_token(trace)
34
+ end
35
+ end
25
36
  end
26
37
 
27
38
  @tokens
@@ -29,13 +40,33 @@ module Minitest
29
40
 
30
41
  private
31
42
 
32
- def map
33
- results.heat_map
43
+ def file_summary_tokens(hit)
44
+ pathname_tokens = pathname(hit)
45
+ line_number_list_tokens = sorted_line_number_list(hit)
46
+
47
+ [*pathname_tokens, *line_number_list_tokens]
48
+ end
49
+
50
+ def origination_location_token(trace)
51
+ # The earliest project line from the backtrace—this is probabyl wholly incorrect in terms
52
+ # of what would be the most helpful line to display, but it's a start.
53
+ location = trace.locations.last
54
+
55
+ [
56
+ [:muted, " #{Output::SYMBOLS[:arrow]} "],
57
+ [:default, location.relative_filename],
58
+ [:muted, ':'],
59
+ [:default, location.line_number],
60
+ [:muted, " in #{location.container}"],
61
+ [:muted, " #{Output::SYMBOLS[:arrow]} `#{location.source_code.line.strip}`"],
62
+ ]
34
63
  end
35
64
 
36
65
  def relevant_issue_types
66
+ # These are always relevant.
37
67
  issue_types = %i[error broken failure]
38
68
 
69
+ # These are only relevant if there aren't more serious isues.
39
70
  issue_types << :skipped unless results.problems?
40
71
  issue_types << :painful unless results.problems? || results.skips.any?
41
72
  issue_types << :slow unless results.problems? || results.skips.any?
@@ -43,32 +74,75 @@ module Minitest
43
74
  issue_types
44
75
  end
45
76
 
46
- def pathname(file)
47
- directory = "#{file.pathname.dirname.to_s.delete_prefix(Dir.pwd)}/"
48
- filename = file.pathname.basename.to_s
77
+ def relevant_issue_types?(hit)
78
+ intersection_issue_types = relevant_issue_types & hit.issues.keys
79
+
80
+ intersection_issue_types.any?
81
+ end
82
+
83
+ def repeated_line_numbers(hit)
84
+ repeated_line_numbers = []
85
+
86
+ hit.lines.each_pair do |line_number, traces|
87
+ # If there aren't multiple traces for a line number, it's not a repeat, right?
88
+ next unless traces.size > 1
89
+
90
+ repeated_line_numbers << Integer(line_number)
91
+ end
92
+
93
+ repeated_line_numbers.sort
94
+ end
95
+
96
+ def repeated_line_numbers?(hit)
97
+ repeated_line_numbers(hit).any?
98
+ end
99
+
100
+ def pathname(hit)
101
+ directory = hit.pathname.dirname.to_s.delete_prefix("#{Dir.pwd}/")
102
+ filename = hit.pathname.basename.to_s
49
103
 
50
104
  [
51
- [:default, directory],
105
+ [:default, "#{directory}/"],
52
106
  [:bold, filename],
53
107
  [:default, ' · ']
54
108
  ]
55
109
  end
56
110
 
57
- def hit_line_numbers(file, issue_type)
58
- numbers = []
59
- line_numbers_for_issue_type = file.issues.fetch(issue_type) { [] }
60
- line_numbers_for_issue_type.sort.map do |line_number|
61
- numbers << [issue_type, "#{line_number} "]
111
+ def line_number_tokens_for_hit(hit)
112
+ line_number_tokens = []
113
+
114
+ relevant_issue_types.each do |issue_type|
115
+ # Retrieve any line numbers for the issue type
116
+ line_numbers_for_issue_type = hit.issues.fetch(issue_type) { [] }
117
+
118
+ # Build the list of tokens representing styled line numbers
119
+ line_numbers_for_issue_type.each do |line_number|
120
+ line_number_tokens << line_number_token(issue_type, line_number)
121
+ end
62
122
  end
63
- numbers
123
+
124
+ line_number_tokens.compact
64
125
  end
65
126
 
66
- def line_numbers(file)
67
- line_number_tokens = []
68
- relevant_issue_types.each do |issue_type|
69
- line_number_tokens += hit_line_numbers(file, issue_type)
127
+ def line_number_token(style, line_number)
128
+ [style, "#{line_number} "]
129
+ end
130
+
131
+ # Generates the line number tokens styled based on their error type
132
+ #
133
+ # @param [Hit] hit the instance of the hit file details to build the heat map entry
134
+ #
135
+ # @return [Array] the arrays representing the line number tokens to display next to a file
136
+ # name in the heat map
137
+ def sorted_line_number_list(hit)
138
+ # Sort the collected group of line number hits so they're in order
139
+ line_number_tokens_for_hit(hit).sort do |a, b|
140
+ # Ensure the line numbers are integers for sorting (otherwise '100' comes before '12')
141
+ first_line_number = Integer(a[1].strip)
142
+ second_line_number = Integer(b[1].strip)
143
+
144
+ first_line_number <=> second_line_number
70
145
  end
71
- line_number_tokens.compact.sort_by { |number_token| number_token[1] }
72
146
  end
73
147
  end
74
148
  end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Minitest
4
4
  module Heat
5
- # Friendly API for printing nicely-formatted output to the console
6
5
  class Output
6
+ # Friendly API for printing consistent markers for the various issue types
7
7
  class Marker
8
8
  SYMBOLS = {
9
9
  success: '·',
@@ -12,7 +12,8 @@ module Minitest
12
12
  broken: 'B',
13
13
  error: 'E',
14
14
  skipped: 'S',
15
- failure: 'F'
15
+ failure: 'F',
16
+ reporter: '✖'
16
17
  }.freeze
17
18
 
18
19
  STYLES = {
@@ -22,7 +23,8 @@ module Minitest
22
23
  broken: :error,
23
24
  error: :error,
24
25
  skipped: :skipped,
25
- failure: :failure
26
+ failure: :failure,
27
+ reporter: :error
26
28
  }.freeze
27
29
 
28
30
  attr_accessor :issue_type
@@ -3,6 +3,7 @@
3
3
  module Minitest
4
4
  module Heat
5
5
  class Output
6
+ # Generates the output tokens to display the results summary
6
7
  class Results
7
8
  extend Forwardable
8
9
 
@@ -90,7 +91,7 @@ module Minitest
90
91
  end
91
92
 
92
93
  def test_count_token
93
- [:default, "#{pluralize(timer.test_count, 'test')}"]
94
+ [:default, pluralize(timer.test_count, 'test').to_s]
94
95
  end
95
96
 
96
97
  def tests_performance_token
@@ -98,7 +99,7 @@ module Minitest
98
99
  end
99
100
 
100
101
  def assertions_count_token
101
- [:default, "#{pluralize(timer.assertion_count, 'assertion')}"]
102
+ [:default, pluralize(timer.assertion_count, 'assertion').to_s]
102
103
  end
103
104
 
104
105
  def assertions_performance_token
@@ -3,7 +3,7 @@
3
3
  module Minitest
4
4
  module Heat
5
5
  class Output
6
- # Builds the collection of tokens representing a specific set of source code lines
6
+ # Generates the tokens representing a specific set of source code lines
7
7
  class SourceCode
8
8
  DEFAULT_LINE_COUNT = 3
9
9
  DEFAULT_INDENTATION_SPACES = 2
@@ -2,8 +2,9 @@
2
2
 
3
3
  module Minitest
4
4
  module Heat
5
- # Friendly API for printing nicely-formatted output to the console
6
5
  class Output
6
+ # Provides a convenient interface for creating console-friendly output while ensuring
7
+ # consistency in the applied styles.
7
8
  class Token
8
9
  class InvalidStyle < ArgumentError; end
9
10
 
@@ -11,18 +11,18 @@ require_relative 'output/token'
11
11
  module Minitest
12
12
  module Heat
13
13
  # Friendly API for printing nicely-formatted output to the console
14
- class Output
14
+ class Output # rubocop:disable Metrics/ClassLength
15
15
  SYMBOLS = {
16
16
  middot: '·',
17
17
  arrow: '➜',
18
- lead: '|',
18
+ lead: '|'
19
19
  }.freeze
20
20
 
21
21
  TOKENS = {
22
- spacer: [:muted, " #{SYMBOLS[:middot]} "],
22
+ spacer: [:muted, " #{SYMBOLS[:middot]} "],
23
23
  muted_arrow: [:muted, " #{SYMBOLS[:arrow]} "],
24
- muted_lead: [:muted, "#{SYMBOLS[:lead]} "],
25
- }
24
+ muted_lead: [:muted, "#{SYMBOLS[:lead]} "]
25
+ }.freeze
26
26
 
27
27
  attr_reader :stream
28
28
 
@@ -42,8 +42,30 @@ module Minitest
42
42
  end
43
43
  alias newline puts
44
44
 
45
+ def issues_list(results)
46
+ # A couple of blank lines to create some breathing room
47
+ newline
48
+ newline
49
+
50
+ # Issues start with the least critical and go up to the most critical so that the most
51
+ # pressing issues are displayed at the bottom of the report in order to reduce scrolling.
52
+ # This way, as you fix issues, the list gets shorter, and eventually the least critical
53
+ # issues will be displayed without scrolling once more problematic issues are resolved.
54
+ %i[slows painfuls skips failures brokens errors].each do |issue_category|
55
+ next unless show?(issue_category, results)
56
+
57
+ results.send(issue_category).each { |issue| issue_details(issue) }
58
+ end
59
+ rescue StandardError => e
60
+ message = "Sorry, but Minitest Heat couldn't display the details of any failures."
61
+ exception_guidance(message, e)
62
+ end
63
+
45
64
  def issue_details(issue)
46
65
  print_tokens Minitest::Heat::Output::Issue.new(issue).tokens
66
+ rescue StandardError => e
67
+ message = "Sorry, but Minitest Heat couldn't display output for a failure."
68
+ exception_guidance(message, e)
47
69
  end
48
70
 
49
71
  def marker(issue_type)
@@ -53,15 +75,49 @@ module Minitest
53
75
  def compact_summary(results, timer)
54
76
  newline
55
77
  print_tokens ::Minitest::Heat::Output::Results.new(results, timer).tokens
78
+ rescue StandardError => e
79
+ message = "Sorry, but Minitest Heat couldn't display the summary."
80
+ exception_guidance(message, e)
56
81
  end
57
82
 
58
83
  def heat_map(map)
59
84
  newline
60
85
  print_tokens ::Minitest::Heat::Output::Map.new(map).tokens
86
+ newline
87
+ rescue StandardError => e
88
+ message = "Sorry, but Minitest Heat couldn't display the heat map."
89
+ exception_guidance(message, e)
90
+ end
91
+
92
+ def exception_guidance(message, exception)
93
+ newline
94
+ puts "#{message} Disabling Minitest Heat can get you back on track until the problem can be fixed."
95
+ puts 'Please use the following exception details to submit an issue at https://github.com/garrettdimon/minitest-heat/issues'
96
+ puts "#{exception.message}:"
97
+ exception.backtrace.each do |line|
98
+ puts " #{line}"
99
+ end
100
+ newline
61
101
  end
62
102
 
63
103
  private
64
104
 
105
+ def no_problems?(results)
106
+ !results.problems?
107
+ end
108
+
109
+ def no_problems_or_skips?(results)
110
+ !results.problems? && results.skips.none?
111
+ end
112
+
113
+ def show?(issue_category, results)
114
+ case issue_category
115
+ when :skips then no_problems?(results)
116
+ when :painfuls, :slows then no_problems_or_skips?(results)
117
+ else true
118
+ end
119
+ end
120
+
65
121
  def style_enabled?
66
122
  stream.tty?
67
123
  end
@@ -82,7 +138,11 @@ module Minitest
82
138
  def print_tokens(lines_of_tokens)
83
139
  lines_of_tokens.each do |tokens|
84
140
  tokens.each do |token|
85
- print Token.new(*token).to_s(token_format)
141
+ begin
142
+ print Token.new(*token).to_s(token_format)
143
+ rescue
144
+ puts token.inspect
145
+ end
86
146
  end
87
147
  newline
88
148
  end
@@ -11,9 +11,29 @@ module Minitest
11
11
  @heat_map = Heat::Map.new
12
12
  end
13
13
 
14
+ # Logs an issue to the results for later reporting
15
+ # @param issue [Issue] the issue generated from a given test result
16
+ #
17
+ # @return [type] [description]
14
18
  def record(issue)
19
+ # Record everything—even if it's a success
15
20
  @issues.push(issue)
16
- @heat_map.add(*issue.to_hit) if issue.hit?
21
+
22
+ # If it's not a genuine problem, we're done here...
23
+ return unless issue.hit?
24
+
25
+ # ...otherwise update the heat map
26
+ update_heat_map(issue)
27
+ end
28
+
29
+ def update_heat_map(issue)
30
+ # For heat map purposes, only the project backtrace lines are interesting
31
+ pathname, line_number = issue.locations.project.to_a
32
+
33
+ # Backtrace is only relevant for exception-generating issues, not slows, skips, or failures
34
+ backtrace = issue.error? ? issue.locations.backtrace.project_locations : []
35
+
36
+ @heat_map.add(pathname, line_number, issue.type, backtrace: backtrace)
17
37
  end
18
38
 
19
39
  def problems?
@@ -37,11 +57,11 @@ module Minitest
37
57
  end
38
58
 
39
59
  def painfuls
40
- @painfuls ||= select_issues(:painful).sort_by(&:time).reverse
60
+ @painfuls ||= select_issues(:painful).sort_by(&:execution_time).reverse
41
61
  end
42
62
 
43
63
  def slows
44
- @slows ||= select_issues(:slow).sort_by(&:time).reverse
64
+ @slows ||= select_issues(:slow).sort_by(&:execution_time).reverse
45
65
  end
46
66
 
47
67
  private
@@ -35,7 +35,7 @@ module Minitest
35
35
  #
36
36
  # @return [Array<String>] the range of lines of code around
37
37
  def lines
38
- return [line] if max_line_count == 1
38
+ return [line].compact if max_line_count == 1
39
39
 
40
40
  file_lines[(line_numbers.first - 1)..(line_numbers.last - 1)]
41
41
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Minitest
4
4
  module Heat
5
- VERSION = '0.0.8'
5
+ VERSION = '0.0.12'
6
6
  end
7
7
  end
data/lib/minitest/heat.rb CHANGED
@@ -3,8 +3,8 @@
3
3
  require_relative 'heat/backtrace'
4
4
  require_relative 'heat/hit'
5
5
  require_relative 'heat/issue'
6
- require_relative 'heat/line'
7
6
  require_relative 'heat/location'
7
+ require_relative 'heat/locations'
8
8
  require_relative 'heat/map'
9
9
  require_relative 'heat/output'
10
10
  require_relative 'heat/results'
@@ -13,7 +13,8 @@ require_relative 'heat/timer'
13
13
  require_relative 'heat/version'
14
14
 
15
15
  module Minitest
16
- # Custom minitest reporter just for Reviewer. Focuses on printing directly actionable guidance.
16
+ # Custom Minitest reporter focused on generating output designed around efficiently identifying
17
+ # issues and potential solutions
17
18
  # - Colorize the Output
18
19
  # - What files had the most errors?
19
20
  # - Show the most impacted areas first.
@@ -2,25 +2,17 @@
2
2
 
3
3
  require_relative 'heat_reporter'
4
4
 
5
- module Minitest
6
- def self.plugin_heat_options(opts, _options)
7
- opts.on '--show-fast', 'Show failures as they happen instead of waiting for the entire suite.' do
8
- # Heat.show_fast!
9
- end
10
-
11
- # TODO: options.
12
- # 1. Fail Fast
13
- # 2. Don't worry about skips.
14
- # 3. Skip coverage.
15
- end
16
-
5
+ module Minitest # rubocop:disable Style/Documentation
17
6
  def self.plugin_heat_init(options)
18
- io = options[:io]
7
+ io = options.fetch(:io, $stdout)
19
8
 
20
- # Clean out the existing reporters.
21
- reporter.reporters = []
9
+ reporter.reporters.reject! do |reporter|
10
+ # Minitest Heat acts as a unified Progress *and* Summary reporter. Using other reporters of
11
+ # those types in conjunction with it creates some overly-verbose output
12
+ reporter.is_a?(ProgressReporter) || reporter.is_a?(SummaryReporter)
13
+ end
22
14
 
23
- # Use Reviewer as the sole reporter.
24
- reporter << HeatReporter.new(io, options)
15
+ # Hook up Reviewer
16
+ self.reporter.reporters << HeatReporter.new(io, options)
25
17
  end
26
18
  end