minitest-heat 0.0.5 → 0.0.9

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.
@@ -14,10 +14,18 @@ module Minitest
14
14
  # - 'most_relevant' represents the most specific file to investigate starting with the source
15
15
  # code and then looking to the test code with final line of the backtrace as a fallback
16
16
  class Location
17
+ TestDefinition = Struct.new(:pathname, :line_number) do
18
+ def initialize(pathname, line_number)
19
+ @pathname = Pathname(pathname)
20
+ @line_number = Integer(line_number)
21
+ super
22
+ end
23
+ end
24
+
17
25
  attr_reader :test_location, :backtrace
18
26
 
19
27
  def initialize(test_location, backtrace = [])
20
- @test_location = test_location
28
+ @test_location = TestDefinition.new(*test_location)
21
29
  @backtrace = Backtrace.new(backtrace)
22
30
  end
23
31
 
@@ -58,28 +66,28 @@ module Minitest
58
66
  #
59
67
  # @return [String] the relative path to the file from the project root
60
68
  def most_relevant_file
61
- Pathname(most_relevant_location[0])
69
+ Pathname(most_relevant_location.pathname)
62
70
  end
63
71
 
64
72
  # The line number of the `most_relevant_file` where the failure originated
65
73
  #
66
74
  # @return [Integer] line number
67
75
  def most_relevant_failure_line
68
- most_relevant_location[1]
76
+ most_relevant_location.line_number
69
77
  end
70
78
 
71
79
  # The final location of the stacktrace regardless of whether it's from within the project
72
80
  #
73
81
  # @return [String] the relative path to the file from the project root
74
82
  def final_file
75
- Pathname(final_location[0])
83
+ Pathname(final_location.pathname)
76
84
  end
77
85
 
78
86
  # The line number of the `final_file` where the failure originated
79
87
  #
80
88
  # @return [Integer] line number
81
89
  def final_failure_line
82
- final_location[1]
90
+ final_location.line_number
83
91
  end
84
92
 
85
93
  # The final location of the stacktrace regardless of whether it's from within the project
@@ -100,7 +108,7 @@ module Minitest
100
108
  #
101
109
  # @return [String, nil] the relative path to the file from the project root
102
110
  def source_code_file
103
- return nil unless backtrace.source_code_lines.any?
111
+ return nil unless backtrace.source_code_entries.any?
104
112
 
105
113
  backtrace.final_source_code_location.pathname
106
114
  end
@@ -109,30 +117,30 @@ module Minitest
109
117
  #
110
118
  # @return [Integer] line number
111
119
  def source_code_failure_line
112
- return nil unless backtrace.source_code_lines.any?
120
+ return nil unless backtrace.source_code_entries.any?
113
121
 
114
- backtrace.final_source_code_location.number
122
+ backtrace.final_source_code_location.line_number
115
123
  end
116
124
 
117
125
  # The final location from the stacktrace that is within the project's test directory
118
126
  #
119
127
  # @return [String, nil] the relative path to the file from the project root
120
128
  def test_file
121
- Pathname(test_location[0])
129
+ Pathname(test_location.pathname)
122
130
  end
123
131
 
124
132
  # The line number of the `test_file` where the test is defined
125
133
  #
126
134
  # @return [Integer] line number
127
135
  def test_definition_line
128
- test_location[1].to_s
136
+ test_location.line_number
129
137
  end
130
138
 
131
139
  # The line number from within the `test_file` test definition where the failure occurred
132
140
  #
133
141
  # @return [Integer] line number
134
142
  def test_failure_line
135
- backtrace.final_test_location&.number || test_definition_line
143
+ backtrace.final_test_location&.line_number || test_definition_line
136
144
  end
137
145
 
138
146
  # The line number from within the `test_file` test definition where the failure occurred
@@ -166,10 +174,8 @@ module Minitest
166
174
  end
167
175
 
168
176
  def backtrace?
169
- backtrace.parsed_lines.any?
177
+ backtrace.parsed_entries.any?
170
178
  end
171
179
  end
172
180
  end
173
181
  end
174
-
175
-
@@ -3,55 +3,28 @@
3
3
  module Minitest
4
4
  module Heat
5
5
  class Map
6
- attr_reader :hits
6
+ MAXIMUM_FILES_TO_SHOW = 5
7
7
 
8
- # So we can sort hot spots by liklihood of being the most important spot to check out before
9
- # trying to fix something. These are ranked based on the possibility they represent ripple
10
- # effects where fixing one problem could potentially fix multiple other failures.
11
- #
12
- # For example, if there's an exception in the file, start there. Broken code can't run. If a
13
- # test is broken (i.e. raising an exception), that's a special sort of failure that would be
14
- # misleading. It doesn't represent a proper failure, but rather a test that doesn't work.
15
- WEIGHTS = {
16
- error: 3, # exceptions from source code have the highest liklihood of a ripple effect
17
- broken: 1, # broken tests won't have ripple effects but can't help if they can't run
18
- failure: 1, # failures are kind of the whole point, and they could have ripple effects
19
- skipped: 0, # skips aren't failures, but they shouldn't go ignored
20
- painful: 0, # slow tests aren't failures, but they shouldn't be ignored
21
- slow: 0,
22
- }
8
+ attr_reader :hits
23
9
 
24
10
  def initialize
25
11
  @hits = {}
26
12
  end
27
13
 
28
14
  def add(filename, line_number, type)
29
- @hits[filename] ||= { weight: 0, total: 0 }
30
- @hits[filename][:total] += 1
31
- @hits[filename][:weight] += WEIGHTS[type]
15
+ @hits[filename] ||= Hit.new(filename)
32
16
 
33
- @hits[filename][type] ||= []
34
- @hits[filename][type] << line_number
17
+ @hits[filename].log(type, line_number)
35
18
  end
36
19
 
37
- def files
38
- hot_files
39
- .sort_by { |filename, weight| weight }
40
- .reverse
41
- .take(5)
20
+ def file_hits
21
+ hot_files.take(MAXIMUM_FILES_TO_SHOW)
42
22
  end
43
23
 
44
24
  private
45
25
 
46
26
  def hot_files
47
- files = {}
48
- @hits.each_pair do |filename, details|
49
- # Can't really be a "hot spot" with just a single issue
50
- # next unless details[:weight] > 1
51
-
52
- files[filename] = details[:weight]
53
- end
54
- files
27
+ hits.values.sort_by(&:weight).reverse
55
28
  end
56
29
  end
57
30
  end
@@ -5,7 +5,7 @@ module Minitest
5
5
  class Output
6
6
  # Builds the collection of tokens for a backtrace when an exception occurs
7
7
  class Backtrace
8
- DEFAULT_LINE_COUNT = 5
8
+ DEFAULT_LINE_COUNT = 10
9
9
  DEFAULT_INDENTATION_SPACES = 2
10
10
 
11
11
  attr_accessor :location, :backtrace
@@ -21,22 +21,18 @@ module Minitest
21
21
  # final backtrace line if it might be relevant/helpful?
22
22
 
23
23
  # Iterate over the selected lines from the backtrace
24
- backtrace_lines.each do |backtrace_line|
24
+ backtrace_entries.each do |backtrace_entry|
25
25
  # Get the source code for the line from the backtrace
26
- source_code = source_code_for(backtrace_line)
27
-
28
26
  parts = [
29
27
  indentation_token,
30
- path_token(backtrace_line),
31
- file_and_line_number_token(backtrace_line),
32
- source_code_line_token(source_code)
28
+ path_token(backtrace_entry),
29
+ *file_and_line_number_tokens(backtrace_entry),
30
+ source_code_line_token(backtrace_entry.source_code)
33
31
  ]
34
32
 
35
- parts << file_freshness(backtrace_line) if most_recently_modified?(backtrace_line)
33
+ parts << file_freshness(backtrace_entry) if most_recently_modified?(backtrace_entry)
36
34
 
37
35
  @tokens << parts
38
-
39
-
40
36
  end
41
37
 
42
38
  @tokens
@@ -55,37 +51,31 @@ module Minitest
55
51
  # ...it could be influenced by a "compact" or "robust" reporter super-style?
56
52
  # ...it's smart about exceptions that were raised outside of the project?
57
53
  # ...it's smart about highlighting lines of code differently based on whether it's source code, test code, or external code?
58
- def backtrace_lines
59
- project_lines
54
+ def backtrace_entries
55
+ all_entries
60
56
  end
61
57
 
62
58
  private
63
59
 
64
- def all_backtrace_lines_from_project?
65
- backtrace_lines.all? { |line| line.path.include?(project_root_dir) }
60
+ def all_backtrace_entries_from_project?
61
+ backtrace_entries.all? { |line| line.path.to_s.include?(project_root_dir) }
66
62
  end
67
63
 
68
64
  def project_root_dir
69
65
  Dir.pwd
70
66
  end
71
67
 
72
- def project_lines
73
- backtrace.project_lines.take(line_count)
74
- end
75
-
76
- def all_lines
77
- backtrace.parsed_lines.take(line_count)
68
+ def project_entries
69
+ backtrace.project_entries.take(line_count)
78
70
  end
79
71
 
80
- def source_code_for(line)
81
- filename = "#{line.path}/#{line.file}"
82
-
83
- Minitest::Heat::Source.new(filename, line_number: line.number, max_line_count: 1)
72
+ def all_entries
73
+ backtrace.parsed_entries.take(line_count)
84
74
  end
85
75
 
86
76
  def most_recently_modified?(line)
87
77
  # If there's more than one line being displayed, and the current line is the freshest
88
- backtrace_lines.size > 1 && line == backtrace.freshest_project_location
78
+ backtrace_entries.size > 1 && line == backtrace.freshest_project_location
89
79
  end
90
80
 
91
81
  def indentation_token
@@ -93,25 +83,31 @@ module Minitest
93
83
  end
94
84
 
95
85
  def path_token(line)
86
+ style = line.to_s.include?(Dir.pwd) ? :default : :muted
96
87
  path = "#{line.path}/"
97
88
 
98
89
  # If all of the backtrace lines are from the project, no point in the added redundant
99
90
  # noise of showing the project root directory over and over again
100
- path = path.delete_prefix(project_root_dir) if all_backtrace_lines_from_project?
91
+ path = path.delete_prefix(project_root_dir) if all_backtrace_entries_from_project?
101
92
 
102
- [:muted, path]
93
+ [style, path]
103
94
  end
104
95
 
105
- def file_and_line_number_token(backtrace_line)
106
- [:default, "#{backtrace_line.file}:#{backtrace_line.number}"]
96
+ def file_and_line_number_tokens(backtrace_entry)
97
+ style = backtrace_entry.to_s.include?(Dir.pwd) ? :bold : :muted
98
+ [
99
+ [style, backtrace_entry.file],
100
+ [:muted, ':'],
101
+ [style, backtrace_entry.line_number]
102
+ ]
107
103
  end
108
104
 
109
105
  def source_code_line_token(source_code)
110
- [:muted, " `#{source_code.line.strip}`"]
106
+ [:muted, " #{Output::SYMBOLS[:arrow]} `#{source_code.line.strip}`"]
111
107
  end
112
108
 
113
- def file_freshness(line)
114
- [:bold, " < Most Recently Modified"]
109
+ def file_freshness(_line)
110
+ [:default, " #{Output::SYMBOLS[:middot]} Most Recently Modified File"]
115
111
  end
116
112
 
117
113
  # The number of spaces each line of code should be indented. Currently defaults to 2 in
@@ -125,6 +121,10 @@ module Minitest
125
121
  def indentation
126
122
  DEFAULT_INDENTATION_SPACES
127
123
  end
124
+
125
+ def style_for(path)
126
+ style = path.to_s.include?(Dir.pwd) ? :default : :muted
127
+ end
128
128
  end
129
129
  end
130
130
  end
@@ -11,7 +11,151 @@ module Minitest
11
11
  end
12
12
 
13
13
  def tokens
14
+ case issue.type
15
+ when :error then error_tokens
16
+ when :broken then broken_tokens
17
+ when :failure then failure_tokens
18
+ when :skipped then skipped_tokens
19
+ when :painful then painful_tokens
20
+ when :slow then slow_tokens
21
+ end
14
22
  end
23
+
24
+ private
25
+
26
+ def error_tokens
27
+ [
28
+ headline_tokens,
29
+ test_location_tokens,
30
+ summary_tokens,
31
+ *backtrace_tokens,
32
+ newline_tokens
33
+ ]
34
+ end
35
+
36
+ def broken_tokens
37
+ [
38
+ headline_tokens,
39
+ test_location_tokens,
40
+ summary_tokens,
41
+ *backtrace_tokens,
42
+ newline_tokens
43
+ ]
44
+ end
45
+
46
+ def failure_tokens
47
+ [
48
+ headline_tokens,
49
+ test_location_tokens,
50
+ summary_tokens,
51
+ newline_tokens
52
+ ]
53
+ end
54
+
55
+ def skipped_tokens
56
+ [
57
+ headline_tokens,
58
+ test_location_tokens,
59
+ newline_tokens
60
+ ]
61
+ end
62
+
63
+ def painful_tokens
64
+ [
65
+ headline_tokens,
66
+ slowness_summary_tokens,
67
+ newline_tokens
68
+ ]
69
+ end
70
+
71
+ def slow_tokens
72
+ [
73
+ headline_tokens,
74
+ slowness_summary_tokens,
75
+ newline_tokens
76
+ ]
77
+ end
78
+
79
+ def headline_tokens
80
+ [[issue.type, issue.label], spacer_token, [:default, issue.test_name]]
81
+ end
82
+
83
+ def test_name_and_class_tokens
84
+ [[:default, issue.test_class], *test_location_tokens ]
85
+ end
86
+
87
+ def backtrace_tokens
88
+ backtrace = ::Minitest::Heat::Output::Backtrace.new(issue.location)
89
+
90
+ backtrace.tokens
91
+ end
92
+
93
+ def test_location_tokens
94
+ [[:default, test_file_short_location], [:muted, ':'], [:default, issue.test_definition_line], arrow_token, [:default, issue.test_failure_line], [:muted, test_line_source]]
95
+ end
96
+
97
+ def location_tokens
98
+ [[:default, most_relevant_short_location], [:muted, ':'], [:default, issue.location.most_relevant_failure_line], [:muted, most_relevant_line_source]]
99
+ end
100
+
101
+ def source_tokens
102
+ filename = issue.location.project_file
103
+ line_number = issue.location.project_failure_line
104
+
105
+ source = Minitest::Heat::Source.new(filename, line_number: line_number)
106
+ [[:muted, " #{Output::SYMBOLS[:arrow]} `#{source.line.strip}`"]]
107
+ end
108
+
109
+ def summary_tokens
110
+ [[:italicized, issue.summary.delete_suffix("---------------")]]
111
+ end
112
+
113
+ def slowness_summary_tokens
114
+ [
115
+ [:bold, issue.slowness],
116
+ spacer_token,
117
+ [:default, issue.location.test_file.to_s.delete_prefix(Dir.pwd)],
118
+ [:muted, ':'],
119
+ [:default, issue.location.test_definition_line]
120
+ ]
121
+ end
122
+
123
+ def newline_tokens
124
+ []
125
+ end
126
+
127
+ def most_relevant_short_location
128
+ issue.location.most_relevant_file.to_s.delete_prefix("#{Dir.pwd}/")
129
+ end
130
+
131
+ def test_file_short_location
132
+ issue.location.test_file.to_s.delete_prefix("#{Dir.pwd}/")
133
+ end
134
+
135
+ def most_relevant_line_source
136
+ filename = issue.location.project_file
137
+ line_number = issue.location.project_failure_line
138
+
139
+ source = Minitest::Heat::Source.new(filename, line_number: line_number)
140
+ "\n #{source.line.strip}"
141
+ end
142
+
143
+ def test_line_source
144
+ filename = issue.location.test_file
145
+ line_number = issue.location.test_failure_line
146
+
147
+ source = Minitest::Heat::Source.new(filename, line_number: line_number)
148
+ "\n #{source.line.strip}"
149
+ end
150
+
151
+ def spacer_token
152
+ Output::TOKENS[:spacer]
153
+ end
154
+
155
+ def arrow_token
156
+ Output::TOKENS[:muted_arrow]
157
+ end
158
+
15
159
  end
16
160
  end
17
161
  end
@@ -4,16 +4,72 @@ module Minitest
4
4
  module Heat
5
5
  class Output
6
6
  class Map
7
- attr_accessor :map
7
+ attr_accessor :results
8
8
 
9
- def initialize(map)
10
- @map = map
9
+ def initialize(results)
10
+ @results = results
11
+ @tokens = []
11
12
  end
12
13
 
13
14
  def tokens
15
+ map.file_hits.each do |hit|
16
+ file_tokens = pathname(hit)
17
+ line_number_tokens = line_numbers(hit)
18
+
19
+ next if line_number_tokens.empty?
20
+
21
+ @tokens << [
22
+ *file_tokens,
23
+ *line_number_tokens
24
+ ]
25
+ end
26
+
27
+ @tokens
14
28
  end
15
29
 
16
30
  private
31
+
32
+ def map
33
+ results.heat_map
34
+ end
35
+
36
+ def relevant_issue_types
37
+ issue_types = %i[error broken failure]
38
+
39
+ issue_types << :skipped unless results.problems?
40
+ issue_types << :painful unless results.problems? || results.skips.any?
41
+ issue_types << :slow unless results.problems? || results.skips.any?
42
+
43
+ issue_types
44
+ end
45
+
46
+ def pathname(file)
47
+ directory = "#{file.pathname.dirname.to_s.delete_prefix(Dir.pwd)}/"
48
+ filename = file.pathname.basename.to_s
49
+
50
+ [
51
+ [:default, directory],
52
+ [:bold, filename],
53
+ [:default, ' · ']
54
+ ]
55
+ end
56
+
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} "]
62
+ end
63
+ numbers
64
+ end
65
+
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)
70
+ end
71
+ line_number_tokens.compact.sort_by { |number_token| number_token[1] }
72
+ end
17
73
  end
18
74
  end
19
75
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ module Heat
5
+ # Friendly API for printing nicely-formatted output to the console
6
+ class Output
7
+ class Marker
8
+ SYMBOLS = {
9
+ success: '·',
10
+ slow: '♦',
11
+ painful: '♦',
12
+ broken: 'B',
13
+ error: 'E',
14
+ skipped: 'S',
15
+ failure: 'F'
16
+ }.freeze
17
+
18
+ STYLES = {
19
+ success: :success,
20
+ slow: :slow,
21
+ painful: :painful,
22
+ broken: :error,
23
+ error: :error,
24
+ skipped: :skipped,
25
+ failure: :failure
26
+ }.freeze
27
+
28
+ attr_accessor :issue_type
29
+
30
+ def initialize(issue_type)
31
+ @issue_type = issue_type
32
+ end
33
+
34
+ def token
35
+ [style, symbol]
36
+ end
37
+
38
+ private
39
+
40
+ def style
41
+ STYLES.fetch(issue_type, :default)
42
+ end
43
+
44
+ def symbol
45
+ SYMBOLS.fetch(issue_type, '?')
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end