minitest-heat 0.0.5 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -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