minitest-heat 0.0.5 → 0.0.6

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,6 +3,8 @@
3
3
  module Minitest
4
4
  module Heat
5
5
  class Map
6
+ MAXIMUM_FILES_TO_SHOW = 5
7
+
6
8
  attr_reader :hits
7
9
 
8
10
  # So we can sort hot spots by liklihood of being the most important spot to check out before
@@ -14,44 +16,31 @@ module Minitest
14
16
  # misleading. It doesn't represent a proper failure, but rather a test that doesn't work.
15
17
  WEIGHTS = {
16
18
  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
19
+ broken: 2, # broken tests won't have ripple effects but can't help if they can't run
18
20
  failure: 1, # failures are kind of the whole point, and they could have ripple effects
19
21
  skipped: 0, # skips aren't failures, but they shouldn't go ignored
20
22
  painful: 0, # slow tests aren't failures, but they shouldn't be ignored
21
- slow: 0,
22
- }
23
+ slow: 0
24
+ }.freeze
23
25
 
24
26
  def initialize
25
27
  @hits = {}
26
28
  end
27
29
 
28
30
  def add(filename, line_number, type)
29
- @hits[filename] ||= { weight: 0, total: 0 }
30
- @hits[filename][:total] += 1
31
- @hits[filename][:weight] += WEIGHTS[type]
31
+ @hits[filename] ||= Hit.new(filename)
32
32
 
33
- @hits[filename][type] ||= []
34
- @hits[filename][type] << line_number
33
+ @hits[filename].log(type, line_number)
35
34
  end
36
35
 
37
- def files
38
- hot_files
39
- .sort_by { |filename, weight| weight }
40
- .reverse
41
- .take(5)
36
+ def file_hits
37
+ hot_files.take(MAXIMUM_FILES_TO_SHOW)
42
38
  end
43
39
 
44
40
  private
45
41
 
46
42
  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
43
+ hits.values.sort_by(&:weight).reverse
55
44
  end
56
45
  end
57
46
  end
@@ -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_token(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
+ project_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)
68
+ def project_entries
69
+ backtrace.project_entries.take(line_count)
74
70
  end
75
71
 
76
- def all_lines
77
- backtrace.parsed_lines.take(line_count)
78
- end
79
-
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
@@ -97,21 +87,21 @@ module Minitest
97
87
 
98
88
  # If all of the backtrace lines are from the project, no point in the added redundant
99
89
  # 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?
90
+ path = path.delete_prefix(project_root_dir) if all_backtrace_entries_from_project?
101
91
 
102
92
  [:muted, path]
103
93
  end
104
94
 
105
- def file_and_line_number_token(backtrace_line)
106
- [:default, "#{backtrace_line.file}:#{backtrace_line.number}"]
95
+ def file_and_line_number_token(backtrace_entry)
96
+ [:default, "#{backtrace_entry.file}:#{backtrace_entry.line_number}"]
107
97
  end
108
98
 
109
99
  def source_code_line_token(source_code)
110
100
  [:muted, " `#{source_code.line.strip}`"]
111
101
  end
112
102
 
113
- def file_freshness(line)
114
- [:bold, " < Most Recently Modified"]
103
+ def file_freshness(_line)
104
+ [:bold, ' < Most Recently Modified']
115
105
  end
116
106
 
117
107
  # The number of spaces each line of code should be indented. Currently defaults to 2 in
@@ -4,6 +4,11 @@ module Minitest
4
4
  module Heat
5
5
  class Output
6
6
  class Issue
7
+ SHARED_SYMBOLS = {
8
+ spacer: ' · ',
9
+ arrow: ' > '
10
+ }.freeze
11
+
7
12
  attr_accessor :issue
8
13
 
9
14
  def initialize(issue)
@@ -11,6 +16,111 @@ module Minitest
11
16
  end
12
17
 
13
18
  def tokens
19
+ case issue.type
20
+ when :error then error_tokens
21
+ when :broken then broken_tokens
22
+ when :failure then failure_tokens
23
+ when :skipped then skipped_tokens
24
+ when :painful then painful_tokens
25
+ when :slow then slow_tokens
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def error_tokens
32
+ [
33
+ headline_tokens,
34
+ summary_tokens,
35
+ *backtrace_tokens,
36
+ newline_tokens
37
+ ]
38
+ end
39
+
40
+ def broken_tokens
41
+ [
42
+ headline_tokens,
43
+ summary_tokens,
44
+ *backtrace_tokens,
45
+ newline_tokens
46
+ ]
47
+ end
48
+
49
+ def failure_tokens
50
+ [
51
+ headline_tokens,
52
+ summary_tokens,
53
+ location_tokens,
54
+ *source_tokens,
55
+ newline_tokens
56
+ ]
57
+ end
58
+
59
+ def skipped_tokens
60
+ [
61
+ headline_tokens,
62
+ summary_tokens,
63
+ newline_tokens
64
+ ]
65
+ end
66
+
67
+ def painful_tokens
68
+ [
69
+ headline_tokens,
70
+ slowness_tokens,
71
+ newline_tokens
72
+ ]
73
+ end
74
+
75
+ def slow_tokens
76
+ [
77
+ headline_tokens,
78
+ slowness_tokens,
79
+ newline_tokens
80
+ ]
81
+ end
82
+
83
+ def headline_tokens
84
+ [[issue.type, issue.label], [:muted, spacer], [:default, issue.test_name], [:muted, spacer], [:muted, issue.test_class]]
85
+ end
86
+
87
+ def summary_tokens
88
+ [[:italicized, issue.summary]]
89
+ end
90
+
91
+ def backtrace_tokens
92
+ backtrace = ::Minitest::Heat::Output::Backtrace.new(issue.location)
93
+
94
+ backtrace.tokens
95
+ end
96
+
97
+ def location_tokens
98
+ [[:muted, issue.short_location]]
99
+ end
100
+
101
+ def source_tokens
102
+ filename = issue.location.project_file
103
+ line_number = issue.location.project_failure_line
104
+
105
+ source_code = ::Minitest::Heat::Output::SourceCode.new(filename, line_number)
106
+
107
+ source_code.tokens
108
+ end
109
+
110
+ def slowness_tokens
111
+ [[:bold, issue.slowness], [:muted, spacer], [:default, issue.short_location] ]
112
+ end
113
+
114
+ def newline_tokens
115
+ []
116
+ end
117
+
118
+ def spacer
119
+ SHARED_SYMBOLS[:spacer]
120
+ end
121
+
122
+ def arrow
123
+ SHARED_SYMBOLS[:arrow]
14
124
  end
15
125
  end
16
126
  end
@@ -4,16 +4,63 @@ module Minitest
4
4
  module Heat
5
5
  class Output
6
6
  class Map
7
+ # extend Forwardable
8
+
7
9
  attr_accessor :map
8
10
 
11
+ # def_delegators :@results, :errors, :brokens, :failures, :slows, :skips, :problems?, :slows?
12
+
9
13
  def initialize(map)
10
14
  @map = map
15
+ @tokens = []
11
16
  end
12
17
 
13
18
  def tokens
19
+ map.file_hits.each do |file|
20
+ @tokens << [
21
+ *pathname(file),
22
+ *line_numbers(file)
23
+ ]
24
+ end
25
+
26
+ @tokens
14
27
  end
15
28
 
16
29
  private
30
+
31
+ def pathname(file)
32
+ directory = "#{file.pathname.dirname.to_s.delete_prefix(Dir.pwd)}/"
33
+ filename = file.pathname.basename.to_s
34
+
35
+ [
36
+ [:default, directory],
37
+ [:bold, filename],
38
+ [:default, ' · ']
39
+ ]
40
+ end
41
+
42
+ 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
+ numbers = []
48
+ line_numbers_for_issue_type.sort.map do |line_number|
49
+ numbers << [issue_type, "#{line_number} "]
50
+ end
51
+ numbers
52
+ end
53
+
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] }
63
+ end
17
64
  end
18
65
  end
19
66
  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
@@ -8,7 +8,7 @@ module Minitest
8
8
 
9
9
  attr_accessor :results
10
10
 
11
- def_delegators :@results, :errors, :brokens, :failures, :slows, :skips, :problems?, :slows?
11
+ def_delegators :@results, :errors, :brokens, :failures, :skips, :painfuls, :slows, :problems?, :slows?
12
12
 
13
13
  def initialize(results)
14
14
  @results = results
@@ -17,8 +17,8 @@ module Minitest
17
17
 
18
18
  def tokens
19
19
  @tokens << [*issue_counts_tokens] if issue_counts_tokens&.any?
20
- @tokens << [assertions_count_token, test_count_token]
21
20
  @tokens << [assertions_performance_token, tests_performance_token, timing_token]
21
+ @tokens << [assertions_count_token, test_count_token]
22
22
 
23
23
  @tokens
24
24
  end
@@ -35,14 +35,21 @@ module Minitest
35
35
  def issue_counts_tokens
36
36
  return unless problems? || slows?
37
37
 
38
- counts = [error_count_token, broken_count_token, failure_count_token, skip_count_token, slow_count_token].compact
38
+ counts = [
39
+ error_count_token,
40
+ broken_count_token,
41
+ failure_count_token,
42
+ skip_count_token,
43
+ painful_count_token,
44
+ slow_count_token
45
+ ].compact
39
46
 
40
47
  # # Create an array of separator tokens one less than the total number of issue count tokens
41
48
  separator_tokens = Array.new(counts.size, separator_token)
42
49
 
43
50
  counts_with_separators = counts
44
- .zip(separator_tokens) # Add separators between the counts
45
- .flatten(1) # Flatten the zipped separators, but no more
51
+ .zip(separator_tokens) # Add separators between the counts
52
+ .flatten(1) # Flatten the zipped separators, but no more
46
53
 
47
54
  counts_with_separators.pop # Remove the final trailing zipped separator that's not needed
48
55
 
@@ -66,6 +73,11 @@ module Minitest
66
73
  issue_count_token(style, skips, name: 'Skip')
67
74
  end
68
75
 
76
+ def painful_count_token
77
+ style = problems? ? :muted : :painful
78
+ issue_count_token(style, painfuls, name: 'Painfully Slow')
79
+ end
80
+
69
81
  def slow_count_token
70
82
  style = problems? ? :muted : :slow
71
83
  issue_count_token(style, slows, name: 'Slow')
@@ -101,9 +101,9 @@ module Minitest
101
101
  # @return [Array<Symbol>] the Token styles for the line number and line of code
102
102
  def styles_for(line_of_code)
103
103
  if line_of_code == source.line && highlight_key_line?
104
- [:default, :default]
104
+ %i[default default]
105
105
  else
106
- [:muted, :muted]
106
+ %i[muted muted]
107
107
  end
108
108
  end
109
109
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Minitest
2
4
  module Heat
3
5
  # Friendly API for printing nicely-formatted output to the console
@@ -6,18 +8,18 @@ module Minitest
6
8
  class InvalidStyle < ArgumentError; end
7
9
 
8
10
  STYLES = {
9
- success: %i[default green],
10
- slow: %i[bold green],
11
- painful: %i[bold green],
12
- error: %i[bold red],
13
- broken: %i[bold red],
14
- failure: %i[default red],
15
- skipped: %i[default yellow],
11
+ success: %i[default green],
12
+ slow: %i[default green],
13
+ painful: %i[bold green],
14
+ error: %i[bold red],
15
+ broken: %i[bold red],
16
+ failure: %i[default red],
17
+ skipped: %i[default yellow],
16
18
  warning_light: %i[light yellow],
17
- italicized: %i[italic gray],
18
- bold: %i[bold default],
19
- default: %i[default default],
20
- muted: %i[light gray]
19
+ italicized: %i[italic gray],
20
+ bold: %i[bold default],
21
+ default: %i[default default],
22
+ muted: %i[light gray]
21
23
  }.freeze
22
24
 
23
25
  attr_accessor :style_key, :content
@@ -38,14 +40,14 @@ module Minitest
38
40
  end
39
41
 
40
42
  def eql?(other)
41
- style_key == other.style_key && content == other.content
43
+ style_key == other.style_key && content == other.content
42
44
  end
43
45
  alias :== eql?
44
46
 
45
47
  private
46
48
 
47
49
  ESC_SEQUENCE = "\e["
48
- END_SEQUENCE = "m"
50
+ END_SEQUENCE = 'm'
49
51
 
50
52
  WEIGHTS = {
51
53
  default: 0,