minitest-heat 0.0.6 → 0.0.10

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.
@@ -22,10 +22,10 @@ module Minitest
22
22
  end
23
23
  end
24
24
 
25
- attr_reader :test_location, :backtrace
25
+ attr_reader :test_definition_location, :backtrace
26
26
 
27
- def initialize(test_location, backtrace = [])
28
- @test_location = TestDefinition.new(*test_location)
27
+ def initialize(test_definition_location, backtrace = [])
28
+ @test_definition_location = TestDefinition.new(*test_definition_location)
29
29
  @backtrace = Backtrace.new(backtrace)
30
30
  end
31
31
 
@@ -45,9 +45,9 @@ module Minitest
45
45
  # test, and it raises an exception, then it's really a broken test rather than a proper
46
46
  # faiure.
47
47
  #
48
- # @return [Boolean] true if most relevant file is the same as the test location file
48
+ # @return [Boolean] true if final file in the backtrace is the same as the test location file
49
49
  def broken_test?
50
- !test_file.nil? && test_file == most_relevant_file
50
+ !test_file.nil? && test_file == final_file
51
51
  end
52
52
 
53
53
  # Knows if the failure occurred in the actual project source code—as opposed to the test or
@@ -59,6 +59,15 @@ module Minitest
59
59
  !source_code_file.nil? && !broken_test?
60
60
  end
61
61
 
62
+
63
+
64
+ # The final location of the stacktrace regardless of whether it's from within the project
65
+ #
66
+ # @return [String] the relative path to the file from the project root
67
+ def final_file
68
+ Pathname(final_location.pathname)
69
+ end
70
+
62
71
  # The file most likely to be the source of the underlying problem. Often, the most recent
63
72
  # backtrace files will be a gem or external library that's failing indirectly as a result
64
73
  # of a problem with local source code (not always, but frequently). In that case, the best
@@ -69,78 +78,76 @@ module Minitest
69
78
  Pathname(most_relevant_location.pathname)
70
79
  end
71
80
 
72
- # The line number of the `most_relevant_file` where the failure originated
81
+ # The final location from the stacktrace that is a test file
73
82
  #
74
- # @return [Integer] line number
75
- def most_relevant_failure_line
76
- most_relevant_location.line_number
83
+ # @return [String, nil] the relative path to the file from the project root
84
+ def test_file
85
+ Pathname(final_test_location.pathname)
77
86
  end
78
87
 
79
- # The final location of the stacktrace regardless of whether it's from within the project
88
+ # The final location from the stacktrace that is within the project directory
80
89
  #
81
- # @return [String] the relative path to the file from the project root
82
- def final_file
83
- Pathname(final_location.pathname)
84
- end
90
+ # @return [String, nil] the relative path to the file from the project root
91
+ def source_code_file
92
+ return nil if final_source_code_location.nil?
85
93
 
86
- # The line number of the `final_file` where the failure originated
87
- #
88
- # @return [Integer] line number
89
- def final_failure_line
90
- final_location.line_number
94
+ Pathname(final_source_code_location.pathname)
91
95
  end
92
96
 
93
- # The final location of the stacktrace regardless of whether it's from within the project
97
+ # The final location of the stacktrace from within the project (source code or test code)
94
98
  #
95
99
  # @return [String] the relative path to the file from the project root
96
100
  def project_file
97
- broken_test? ? test_file : source_code_file
101
+ return nil if project_location.nil?
102
+
103
+ Pathname(project_location.pathname)
98
104
  end
99
105
 
100
- # The line number of the `project_file` where the failure originated
106
+
107
+ # The line number of the `final_file` where the failure originated
101
108
  #
102
109
  # @return [Integer] line number
103
- def project_failure_line
104
- broken_test? ? test_failure_line || test_definition_line : source_code_failure_line
110
+ def final_failure_line
111
+ final_location.line_number
105
112
  end
106
113
 
107
- # The final location from the stacktrace that is within the project directory
114
+ # The line number of the `most_relevant_file` where the failure originated
108
115
  #
109
- # @return [String, nil] the relative path to the file from the project root
110
- def source_code_file
111
- return nil unless backtrace.source_code_entries.any?
112
-
113
- backtrace.final_source_code_location.pathname
116
+ # @return [Integer] line number
117
+ def most_relevant_failure_line
118
+ most_relevant_location.line_number
114
119
  end
115
120
 
116
- # The line number of the `source_code_file` where the failure originated
121
+ # The line number of the `test_file` where the test is defined
117
122
  #
118
123
  # @return [Integer] line number
119
- def source_code_failure_line
120
- return nil unless backtrace.source_code_entries.any?
121
-
122
- backtrace.final_source_code_location.line_number
124
+ def test_definition_line
125
+ test_definition_location.line_number
123
126
  end
124
127
 
125
- # The final location from the stacktrace that is within the project's test directory
128
+ # The line number from within the `test_file` test definition where the failure occurred
126
129
  #
127
- # @return [String, nil] the relative path to the file from the project root
128
- def test_file
129
- Pathname(test_location.pathname)
130
+ # @return [Integer] line number
131
+ def test_failure_line
132
+ final_test_location.line_number
130
133
  end
131
134
 
132
- # The line number of the `test_file` where the test is defined
135
+ # The line number of the `source_code_file` where the failure originated
133
136
  #
134
137
  # @return [Integer] line number
135
- def test_definition_line
136
- test_location.line_number
138
+ def source_code_failure_line
139
+ final_source_code_location&.line_number
137
140
  end
138
141
 
139
- # The line number from within the `test_file` test definition where the failure occurred
142
+ # The line number of the `project_file` where the failure originated
140
143
  #
141
144
  # @return [Integer] line number
142
- def test_failure_line
143
- backtrace.final_test_location&.line_number || test_definition_line
145
+ def project_failure_line
146
+ if !broken_test? && !source_code_file.nil?
147
+ source_code_failure_line
148
+ else
149
+ test_failure_line
150
+ end
144
151
  end
145
152
 
146
153
  # The line number from within the `test_file` test definition where the failure occurred
@@ -148,7 +155,7 @@ module Minitest
148
155
  # @return [Location] the last location from the backtrace or the test location if a backtrace
149
156
  # was not passed to the initializer
150
157
  def final_location
151
- backtrace? ? backtrace.final_location : test_location
158
+ backtrace.parsed_entries.any? ? backtrace.final_location : test_definition_location
152
159
  end
153
160
 
154
161
  # The file most likely to be the source of the underlying problem. Often, the most recent
@@ -159,22 +166,33 @@ module Minitest
159
166
  # @return [Array] file and line number of the most likely source of the problem
160
167
  def most_relevant_location
161
168
  [
162
- source_code_location,
163
- test_location,
169
+ final_source_code_location,
170
+ final_test_location,
164
171
  final_location
165
172
  ].compact.first
166
173
  end
167
174
 
168
- def project_location
169
- source_code_location || test_location
175
+ # Returns the final test location based on the backtrace if present. Otherwise falls back to
176
+ # the test location which represents the test definition.
177
+ #
178
+ # @return [Location] the final location from the test files
179
+ def final_test_location
180
+ backtrace.final_test_location || test_definition_location
170
181
  end
171
182
 
172
- def source_code_location
183
+ # Returns the final source code location based on the backtrace
184
+ #
185
+ # @return [Location] the final location from the source code files
186
+ def final_source_code_location
173
187
  backtrace.final_source_code_location
174
188
  end
175
189
 
176
- def backtrace?
177
- backtrace.parsed_entries.any?
190
+ # Returns the final project location based on the backtrace if present. Otherwise falls back
191
+ # to the test location which represents the test definition.
192
+ #
193
+ # @return [Location] the final location from the project files
194
+ def project_location
195
+ backtrace.final_project_location || test_definition_location
178
196
  end
179
197
  end
180
198
  end
@@ -2,43 +2,40 @@
2
2
 
3
3
  module Minitest
4
4
  module Heat
5
+ # Structured approach to collecting the locations of issues for generating a heat map
5
6
  class Map
6
7
  MAXIMUM_FILES_TO_SHOW = 5
7
8
 
8
9
  attr_reader :hits
9
10
 
10
- # So we can sort hot spots by liklihood of being the most important spot to check out before
11
- # trying to fix something. These are ranked based on the possibility they represent ripple
12
- # effects where fixing one problem could potentially fix multiple other failures.
13
- #
14
- # For example, if there's an exception in the file, start there. Broken code can't run. If a
15
- # test is broken (i.e. raising an exception), that's a special sort of failure that would be
16
- # misleading. It doesn't represent a proper failure, but rather a test that doesn't work.
17
- WEIGHTS = {
18
- error: 3, # exceptions from source code have the highest liklihood of a ripple effect
19
- broken: 2, # broken tests won't have ripple effects but can't help if they can't run
20
- failure: 1, # failures are kind of the whole point, and they could have ripple effects
21
- skipped: 0, # skips aren't failures, but they shouldn't go ignored
22
- painful: 0, # slow tests aren't failures, but they shouldn't be ignored
23
- slow: 0
24
- }.freeze
25
-
26
11
  def initialize
27
12
  @hits = {}
28
13
  end
29
14
 
15
+ # Records a hit to the list of files and issue types
16
+ # @param filename [String] the unique path and file name for recordings hits
17
+ # @param line_number [Integer] the line number where the issue was encountered
18
+ # @param type [Symbol] the type of issue that was encountered (i.e. :failure, :error, etc.)
19
+ #
20
+ # @return [void]
30
21
  def add(filename, line_number, type)
31
22
  @hits[filename] ||= Hit.new(filename)
32
23
 
33
- @hits[filename].log(type, line_number)
24
+ @hits[filename].log(type.to_sym, line_number)
34
25
  end
35
26
 
27
+ # Returns a subset of affected files to keep the list from being overwhelming
28
+ #
29
+ # @return [Array] the list of files and the line numbers for each encountered issue type
36
30
  def file_hits
37
31
  hot_files.take(MAXIMUM_FILES_TO_SHOW)
38
32
  end
39
33
 
40
34
  private
41
35
 
36
+ # Sorts the files by hit "weight" so that the most problematic files are at the beginning
37
+ #
38
+ # @return [Array] the collection of files that encountred issues
42
39
  def hot_files
43
40
  hits.values.sort_by(&:weight).reverse
44
41
  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
@@ -23,13 +23,9 @@ module Minitest
23
23
  # Iterate over the selected lines from the backtrace
24
24
  backtrace_entries.each do |backtrace_entry|
25
25
  # Get the source code for the line from the backtrace
26
- parts = [
27
- indentation_token,
28
- path_token(backtrace_entry),
29
- file_and_line_number_token(backtrace_entry),
30
- source_code_line_token(backtrace_entry.source_code)
31
- ]
26
+ parts = backtrace_line_tokens(backtrace_entry)
32
27
 
28
+ # If it's the most recently modified file in the trace, add the token for that
33
29
  parts << file_freshness(backtrace_entry) if most_recently_modified?(backtrace_entry)
34
30
 
35
31
  @tokens << parts
@@ -52,11 +48,20 @@ module Minitest
52
48
  # ...it's smart about exceptions that were raised outside of the project?
53
49
  # ...it's smart about highlighting lines of code differently based on whether it's source code, test code, or external code?
54
50
  def backtrace_entries
55
- project_entries
51
+ all_entries
56
52
  end
57
53
 
58
54
  private
59
55
 
56
+ def backtrace_line_tokens(backtrace_entry)
57
+ [
58
+ indentation_token,
59
+ path_token(backtrace_entry),
60
+ *file_and_line_number_tokens(backtrace_entry),
61
+ source_code_line_token(backtrace_entry.source_code)
62
+ ]
63
+ end
64
+
60
65
  def all_backtrace_entries_from_project?
61
66
  backtrace_entries.all? { |line| line.path.to_s.include?(project_root_dir) }
62
67
  end
@@ -83,25 +88,31 @@ module Minitest
83
88
  end
84
89
 
85
90
  def path_token(line)
91
+ style = line.to_s.include?(Dir.pwd) ? :default : :muted
86
92
  path = "#{line.path}/"
87
93
 
88
94
  # If all of the backtrace lines are from the project, no point in the added redundant
89
95
  # noise of showing the project root directory over and over again
90
96
  path = path.delete_prefix(project_root_dir) if all_backtrace_entries_from_project?
91
97
 
92
- [:muted, path]
98
+ [style, path]
93
99
  end
94
100
 
95
- def file_and_line_number_token(backtrace_entry)
96
- [:default, "#{backtrace_entry.file}:#{backtrace_entry.line_number}"]
101
+ def file_and_line_number_tokens(backtrace_entry)
102
+ style = backtrace_entry.to_s.include?(Dir.pwd) ? :bold : :muted
103
+ [
104
+ [style, backtrace_entry.file],
105
+ [:muted, ':'],
106
+ [style, backtrace_entry.line_number]
107
+ ]
97
108
  end
98
109
 
99
110
  def source_code_line_token(source_code)
100
- [:muted, " `#{source_code.line.strip}`"]
111
+ [:muted, " #{Output::SYMBOLS[:arrow]} `#{source_code.line.strip}`"]
101
112
  end
102
113
 
103
114
  def file_freshness(_line)
104
- [:bold, ' < Most Recently Modified']
115
+ [:default, " #{Output::SYMBOLS[:middot]} Most Recently Modified File"]
105
116
  end
106
117
 
107
118
  # The number of spaces each line of code should be indented. Currently defaults to 2 in
@@ -115,6 +126,10 @@ module Minitest
115
126
  def indentation
116
127
  DEFAULT_INDENTATION_SPACES
117
128
  end
129
+
130
+ def style_for(path)
131
+ path.to_s.include?(Dir.pwd) ? :default : :muted
132
+ end
118
133
  end
119
134
  end
120
135
  end
@@ -3,12 +3,8 @@
3
3
  module Minitest
4
4
  module Heat
5
5
  class Output
6
- class Issue
7
- SHARED_SYMBOLS = {
8
- spacer: ' · ',
9
- arrow: ' > '
10
- }.freeze
11
-
6
+ # Formats issues to output based on the issue type
7
+ class Issue # rubocop:disable Metrics/ClassLength
12
8
  attr_accessor :issue
13
9
 
14
10
  def initialize(issue)
@@ -31,6 +27,7 @@ module Minitest
31
27
  def error_tokens
32
28
  [
33
29
  headline_tokens,
30
+ test_location_tokens,
34
31
  summary_tokens,
35
32
  *backtrace_tokens,
36
33
  newline_tokens
@@ -40,6 +37,7 @@ module Minitest
40
37
  def broken_tokens
41
38
  [
42
39
  headline_tokens,
40
+ test_location_tokens,
43
41
  summary_tokens,
44
42
  *backtrace_tokens,
45
43
  newline_tokens
@@ -49,9 +47,8 @@ module Minitest
49
47
  def failure_tokens
50
48
  [
51
49
  headline_tokens,
50
+ test_location_tokens,
52
51
  summary_tokens,
53
- location_tokens,
54
- *source_tokens,
55
52
  newline_tokens
56
53
  ]
57
54
  end
@@ -59,7 +56,7 @@ module Minitest
59
56
  def skipped_tokens
60
57
  [
61
58
  headline_tokens,
62
- summary_tokens,
59
+ test_location_tokens,
63
60
  newline_tokens
64
61
  ]
65
62
  end
@@ -67,7 +64,7 @@ module Minitest
67
64
  def painful_tokens
68
65
  [
69
66
  headline_tokens,
70
- slowness_tokens,
67
+ slowness_summary_tokens,
71
68
  newline_tokens
72
69
  ]
73
70
  end
@@ -75,17 +72,49 @@ module Minitest
75
72
  def slow_tokens
76
73
  [
77
74
  headline_tokens,
78
- slowness_tokens,
75
+ slowness_summary_tokens,
79
76
  newline_tokens
80
77
  ]
81
78
  end
82
79
 
83
80
  def headline_tokens
84
- [[issue.type, issue.label], [:muted, spacer], [:default, issue.test_name], [:muted, spacer], [:muted, issue.test_class]]
81
+ [[issue.type, label(issue)], spacer_token, [:default, test_name(issue)]]
85
82
  end
86
83
 
87
- def summary_tokens
88
- [[:italicized, issue.summary]]
84
+ def test_name(issue)
85
+ test_prefix = 'test_'
86
+ identifier = issue.test_identifier
87
+
88
+ if identifier.start_with?(test_prefix)
89
+ identifier.delete_prefix(test_prefix).gsub('_', ' ').capitalize
90
+ else
91
+ identifier
92
+ end
93
+ end
94
+
95
+ def label(issue)
96
+ if issue.error? && issue.in_test?
97
+ # When the exception came out of the test itself, that's a different kind of exception
98
+ # that really only indicates there's a problem with the code in the test. It's kind of
99
+ # between an error and a test.
100
+ 'Broken Test'
101
+ elsif issue.error?
102
+ 'Error'
103
+ elsif issue.skipped?
104
+ 'Skipped'
105
+ elsif issue.painful?
106
+ 'Passed but Very Slow'
107
+ elsif issue.slow?
108
+ 'Passed but Slow'
109
+ elsif !issue.passed?
110
+ 'Failure'
111
+ else
112
+ 'Success'
113
+ end
114
+ end
115
+
116
+ def test_name_and_class_tokens
117
+ [[:default, issue.test_class], *test_location_tokens]
89
118
  end
90
119
 
91
120
  def backtrace_tokens
@@ -94,33 +123,74 @@ module Minitest
94
123
  backtrace.tokens
95
124
  end
96
125
 
126
+ def test_location_tokens
127
+ [[:default, test_file_short_location], [:muted, ':'], [:default, issue.test_definition_line], arrow_token, [:default, issue.test_failure_line], [:muted, test_line_source]]
128
+ end
129
+
97
130
  def location_tokens
98
- [[:muted, issue.short_location]]
131
+ [[:default, most_relevant_short_location], [:muted, ':'], [:default, issue.location.most_relevant_failure_line], [:muted, most_relevant_line_source]]
99
132
  end
100
133
 
101
134
  def source_tokens
102
135
  filename = issue.location.project_file
103
136
  line_number = issue.location.project_failure_line
104
137
 
105
- source_code = ::Minitest::Heat::Output::SourceCode.new(filename, line_number)
138
+ source = Minitest::Heat::Source.new(filename, line_number: line_number)
139
+ [[:muted, " #{Output::SYMBOLS[:arrow]} `#{source.line.strip}`"]]
140
+ end
141
+
142
+ def summary_tokens
143
+ [[:italicized, issue.summary.delete_suffix('---------------').strip]]
144
+ end
106
145
 
107
- source_code.tokens
146
+ def slowness_summary_tokens
147
+ [
148
+ [:bold, slowness(issue)],
149
+ spacer_token,
150
+ [:default, issue.location.test_file.to_s.delete_prefix(Dir.pwd)],
151
+ [:muted, ':'],
152
+ [:default, issue.location.test_definition_line]
153
+ ]
108
154
  end
109
155
 
110
- def slowness_tokens
111
- [[:bold, issue.slowness], [:muted, spacer], [:default, issue.short_location] ]
156
+ def slowness(issue)
157
+ "#{issue.execution_time.round(2)}s"
112
158
  end
113
159
 
114
160
  def newline_tokens
115
161
  []
116
162
  end
117
163
 
118
- def spacer
119
- SHARED_SYMBOLS[:spacer]
164
+ def most_relevant_short_location
165
+ issue.location.most_relevant_file.to_s.delete_prefix("#{Dir.pwd}/")
166
+ end
167
+
168
+ def test_file_short_location
169
+ issue.location.test_file.to_s.delete_prefix("#{Dir.pwd}/")
170
+ end
171
+
172
+ def most_relevant_line_source
173
+ filename = issue.location.project_file
174
+ line_number = issue.location.project_failure_line
175
+
176
+ source = Minitest::Heat::Source.new(filename, line_number: line_number)
177
+ "\n #{source.line.strip}"
178
+ end
179
+
180
+ def test_line_source
181
+ filename = issue.location.test_file
182
+ line_number = issue.location.test_failure_line
183
+
184
+ source = Minitest::Heat::Source.new(filename, line_number: line_number)
185
+ "\n #{source.line.strip}"
186
+ end
187
+
188
+ def spacer_token
189
+ Output::TOKENS[:spacer]
120
190
  end
121
191
 
122
- def arrow
123
- SHARED_SYMBOLS[:arrow]
192
+ def arrow_token
193
+ Output::TOKENS[:muted_arrow]
124
194
  end
125
195
  end
126
196
  end
@@ -3,23 +3,25 @@
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|
16
+ map.file_hits.each do |hit|
17
+ file_tokens = pathname(hit)
18
+ line_number_tokens = line_numbers(hit)
19
+
20
+ next if line_number_tokens.empty?
21
+
20
22
  @tokens << [
21
- *pathname(file),
22
- *line_numbers(file)
23
+ *file_tokens,
24
+ *line_number_tokens
23
25
  ]
24
26
  end
25
27
 
@@ -28,8 +30,24 @@ module Minitest
28
30
 
29
31
  private
30
32
 
33
+ def map
34
+ results.heat_map
35
+ end
36
+
37
+ def relevant_issue_types
38
+ # These are always relevant.
39
+ issue_types = %i[error broken failure]
40
+
41
+ # These are only relevant if there aren't more serious isues.
42
+ issue_types << :skipped unless results.problems?
43
+ issue_types << :painful unless results.problems? || results.skips.any?
44
+ issue_types << :slow unless results.problems? || results.skips.any?
45
+
46
+ issue_types
47
+ end
48
+
31
49
  def pathname(file)
32
- directory = "#{file.pathname.dirname.to_s.delete_prefix(Dir.pwd)}/"
50
+ directory = "#{file.pathname.dirname.to_s.delete_prefix(Dir.pwd)}/".delete_prefix('/')
33
51
  filename = file.pathname.basename.to_s
34
52
 
35
53
  [
@@ -40,26 +58,31 @@ module Minitest
40
58
  end
41
59
 
42
60
  def hit_line_numbers(file, issue_type)
43
- line_numbers_for_issue_type = file.issues.fetch(issue_type) { [] }
44
-
45
- return nil if line_numbers_for_issue_type.empty?
46
-
47
61
  numbers = []
48
- line_numbers_for_issue_type.sort.map do |line_number|
62
+ line_numbers_for_issue_type = file.issues.fetch(issue_type) { [] }
63
+ line_numbers_for_issue_type.map do |line_number|
49
64
  numbers << [issue_type, "#{line_number} "]
50
65
  end
66
+
51
67
  numbers
52
68
  end
53
69
 
54
70
  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] }
71
+ line_number_tokens = []
72
+
73
+ # Merge the hits for all issue types into one list
74
+ relevant_issue_types.each do |issue_type|
75
+ line_number_tokens += hit_line_numbers(file, issue_type)
76
+ end
77
+
78
+ # Sort the collected group of line number hits so they're in order
79
+ line_number_tokens.compact.sort do |a, b|
80
+ # Ensure the line numbers are integers for sorting (otherwise '100' comes before '12')
81
+ first_line_number = Integer(a[1].strip)
82
+ second_line_number = Integer(b[1].strip)
83
+
84
+ first_line_number <=> second_line_number
85
+ end
63
86
  end
64
87
  end
65
88
  end