minitest-heat 0.0.7 → 0.0.11

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.
@@ -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,100 @@ module Minitest
78
79
  end
79
80
 
80
81
  def headline_tokens
81
- [[issue.type, issue.label], spacer_token, [:default, issue.test_name]]
82
+ [[issue.type, label(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(issue) # rubocop:disable Metrics
97
+ if issue.error? && issue.in_test?
98
+ # When the exception came out of the test itself, that's a different kind of exception
99
+ # that really only indicates there's a problem with the code in the test. It's kind of
100
+ # between an error and a test.
101
+ 'Broken Test'
102
+ elsif issue.error?
103
+ 'Error'
104
+ elsif issue.skipped?
105
+ 'Skipped'
106
+ elsif issue.painful?
107
+ 'Passed but Very Slow'
108
+ elsif issue.slow?
109
+ 'Passed but Slow'
110
+ elsif !issue.passed?
111
+ 'Failure'
112
+ else
113
+ 'Success'
114
+ end
82
115
  end
83
116
 
84
117
  def test_name_and_class_tokens
85
- [[:default, issue.test_class], *test_location_tokens ]
118
+ [[:default, issue.test_class], *test_location_tokens]
86
119
  end
87
120
 
88
121
  def backtrace_tokens
89
- backtrace = ::Minitest::Heat::Output::Backtrace.new(issue.location)
90
-
91
- backtrace.tokens
122
+ @backtrace_tokens ||= ::Minitest::Heat::Output::Backtrace.new(locations).tokens
92
123
  end
93
124
 
94
125
  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]]
126
+ [
127
+ [:default, locations.test_definition.relative_filename],
128
+ [:muted, ':'],
129
+ [:default, locations.test_definition.line_number],
130
+ arrow_token,
131
+ [:default, locations.test_failure.line_number],
132
+ [:muted, "\n #{locations.test_failure.source_code.line.strip}"]
133
+ ]
96
134
  end
97
135
 
98
136
  def location_tokens
99
- [[:default, most_relevant_short_location], [:muted, ':'], [:default, issue.location.most_relevant_failure_line], [:muted, most_relevant_line_source]]
137
+ [
138
+ [:default, locations.most_relevant.relative_filename],
139
+ [:muted, ':'],
140
+ [:default, locations.most_relevant.line_number],
141
+ [:muted, "\n #{locations.most_relevant.source_code.line.strip}"]
142
+ ]
100
143
  end
101
144
 
102
145
  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
-
146
+ filename = locations.project.filename
147
+ line_number = locations.project.line_number
109
148
  source = Minitest::Heat::Source.new(filename, line_number: line_number)
149
+
110
150
  [[:muted, " #{Output::SYMBOLS[:arrow]} `#{source.line.strip}`"]]
111
151
  end
112
152
 
113
153
  def summary_tokens
114
- [[:italicized, issue.summary.delete_suffix("---------------")]]
154
+ [[:italicized, issue.summary.delete_suffix('---------------').strip]]
115
155
  end
116
156
 
117
157
  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}/")
158
+ [
159
+ [:bold, slowness(issue)],
160
+ spacer_token,
161
+ [:default, locations.test_definition.relative_path],
162
+ [:default, locations.test_definition.filename],
163
+ [:muted, ':'],
164
+ [:default, locations.test_definition.line_number]
165
+ ]
131
166
  end
132
167
 
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}"
168
+ def slowness(issue)
169
+ "#{issue.execution_time.round(2)}s"
139
170
  end
140
171
 
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}"
172
+ def newline_tokens
173
+ []
147
174
  end
148
175
 
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
176
  def spacer_token
169
177
  Output::TOKENS[:spacer]
170
178
  end
@@ -172,7 +180,6 @@ module Minitest
172
180
  def arrow_token
173
181
  Output::TOKENS[:muted_arrow]
174
182
  end
175
-
176
183
  end
177
184
  end
178
185
  end
@@ -3,24 +3,36 @@
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
- # extend Forwardable
8
+ attr_accessor :results
8
9
 
9
- attr_accessor :map
10
-
11
- # def_delegators :@results, :errors, :brokens, :failures, :slows, :skips, :problems?, :slows?
12
-
13
- def initialize(map)
14
- @map = map
10
+ def initialize(results)
11
+ @results = results
15
12
  @tokens = []
16
13
  end
17
14
 
18
15
  def tokens
19
- map.file_hits.each do |file|
20
- @tokens << [
21
- *pathname(file),
22
- *line_numbers(file)
23
- ]
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)
19
+
20
+ @tokens << [[:muted, ""]]
21
+ @tokens << file_summary_tokens(hit)
22
+
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
24
36
  end
25
37
 
26
38
  @tokens
@@ -28,38 +40,109 @@ module Minitest
28
40
 
29
41
  private
30
42
 
31
- def pathname(file)
32
- directory = "#{file.pathname.dirname.to_s.delete_prefix(Dir.pwd)}/"
33
- filename = file.pathname.basename.to_s
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
+ ]
63
+ end
64
+
65
+ def relevant_issue_types
66
+ # These are always relevant.
67
+ issue_types = %i[error broken failure]
68
+
69
+ # These are only relevant if there aren't more serious isues.
70
+ issue_types << :skipped unless results.problems?
71
+ issue_types << :painful unless results.problems? || results.skips.any?
72
+ issue_types << :slow unless results.problems? || results.skips.any?
73
+
74
+ issue_types
75
+ end
76
+
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
34
103
 
35
104
  [
36
- [:default, directory],
105
+ [:default, "#{directory}/"],
37
106
  [:bold, filename],
38
107
  [:default, ' · ']
39
108
  ]
40
109
  end
41
110
 
42
- def hit_line_numbers(file, issue_type)
43
- line_numbers_for_issue_type = file.issues.fetch(issue_type) { [] }
111
+ def line_number_tokens_for_hit(hit)
112
+ line_number_tokens = []
44
113
 
45
- return nil if line_numbers_for_issue_type.empty?
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) { [] }
46
117
 
47
- numbers = []
48
- line_numbers_for_issue_type.sort.map do |line_number|
49
- numbers << [issue_type, "#{line_number} "]
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
50
122
  end
51
- numbers
123
+
124
+ line_number_tokens.compact
52
125
  end
53
126
 
54
- def line_numbers(file)
55
- [
56
- *hit_line_numbers(file, :error),
57
- *hit_line_numbers(file, :broken),
58
- *hit_line_numbers(file, :failure),
59
- *hit_line_numbers(file, :skipped),
60
- *hit_line_numbers(file, :painful),
61
- *hit_line_numbers(file, :slow)
62
- ].compact.sort_by { |number_token| number_token[1] }
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
145
+ end
63
146
  end
64
147
  end
65
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
@@ -4,14 +4,36 @@ module Minitest
4
4
  module Heat
5
5
  # A collection of test failures
6
6
  class Results
7
- attr_reader :issues
7
+ attr_reader :issues, :heat_map
8
8
 
9
9
  def initialize
10
10
  @issues = []
11
+ @heat_map = Heat::Map.new
11
12
  end
12
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]
13
18
  def record(issue)
14
- @issues << issue
19
+ # Record everything—even if it's a success
20
+ @issues.push(issue)
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)
15
37
  end
16
38
 
17
39
  def problems?
@@ -35,11 +57,11 @@ module Minitest
35
57
  end
36
58
 
37
59
  def painfuls
38
- @painfuls ||= select_issues(:painful).sort_by(&:time).reverse
60
+ @painfuls ||= select_issues(:painful).sort_by(&:execution_time).reverse
39
61
  end
40
62
 
41
63
  def slows
42
- @slows ||= select_issues(:slow).sort_by(&:time).reverse
64
+ @slows ||= select_issues(:slow).sort_by(&:execution_time).reverse
43
65
  end
44
66
 
45
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.7'
5
+ VERSION = '0.0.11'
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.