minitest-heat 0.0.5 → 0.0.6

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,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,