minitest-heat 0.0.1 → 0.0.5

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.
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ module Heat
5
+ class Output
6
+ # Builds the collection of tokens for a backtrace when an exception occurs
7
+ class Backtrace
8
+ DEFAULT_LINE_COUNT = 5
9
+ DEFAULT_INDENTATION_SPACES = 2
10
+
11
+ attr_accessor :location, :backtrace
12
+
13
+ def initialize(location)
14
+ @location = location
15
+ @backtrace = location.backtrace
16
+ @tokens = []
17
+ end
18
+
19
+ def tokens
20
+ # There could be option to expand and display more than one line of source code for the
21
+ # final backtrace line if it might be relevant/helpful?
22
+
23
+ # Iterate over the selected lines from the backtrace
24
+ backtrace_lines.each do |backtrace_line|
25
+ # Get the source code for the line from the backtrace
26
+ source_code = source_code_for(backtrace_line)
27
+
28
+ parts = [
29
+ indentation_token,
30
+ path_token(backtrace_line),
31
+ file_and_line_number_token(backtrace_line),
32
+ source_code_line_token(source_code)
33
+ ]
34
+
35
+ parts << file_freshness(backtrace_line) if most_recently_modified?(backtrace_line)
36
+
37
+ @tokens << parts
38
+
39
+
40
+ end
41
+
42
+ @tokens
43
+ end
44
+
45
+ def line_count
46
+ DEFAULT_LINE_COUNT
47
+ end
48
+
49
+ # This should probably be smart about what lines are displayed in a backtrace.
50
+ # Maybe...
51
+ # ...it could intelligently display the full back trace?
52
+ # ...only the backtrace from the first/last line of project source?
53
+ # ...it behaves a little different when it's a broken test vs. a true exception?
54
+ # ...it could be smart about subtly flagging the lines that show up in the heat map frequently?
55
+ # ...it could be influenced by a "compact" or "robust" reporter super-style?
56
+ # ...it's smart about exceptions that were raised outside of the project?
57
+ # ...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
60
+ end
61
+
62
+ private
63
+
64
+ def all_backtrace_lines_from_project?
65
+ backtrace_lines.all? { |line| line.path.include?(project_root_dir) }
66
+ end
67
+
68
+ def project_root_dir
69
+ Dir.pwd
70
+ end
71
+
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)
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)
84
+ end
85
+
86
+ def most_recently_modified?(line)
87
+ # 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
89
+ end
90
+
91
+ def indentation_token
92
+ [:default, ' ' * indentation]
93
+ end
94
+
95
+ def path_token(line)
96
+ path = "#{line.path}/"
97
+
98
+ # If all of the backtrace lines are from the project, no point in the added redundant
99
+ # 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?
101
+
102
+ [:muted, path]
103
+ end
104
+
105
+ def file_and_line_number_token(backtrace_line)
106
+ [:default, "#{backtrace_line.file}:#{backtrace_line.number}"]
107
+ end
108
+
109
+ def source_code_line_token(source_code)
110
+ [:muted, " `#{source_code.line.strip}`"]
111
+ end
112
+
113
+ def file_freshness(line)
114
+ [:bold, " < Most Recently Modified"]
115
+ end
116
+
117
+ # The number of spaces each line of code should be indented. Currently defaults to 2 in
118
+ # order to provide visual separation between test failures, but in the future, it could
119
+ # be configurable in order to save horizontal space and create more compact output. For
120
+ # example, it could be smart based on line length and total available horizontal terminal
121
+ # space, or there could be higher-level "display" setting that could have a `:compact`
122
+ # option that would reduce the space used.
123
+ #
124
+ # @return [type] [description]
125
+ def indentation
126
+ DEFAULT_INDENTATION_SPACES
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ module Heat
5
+ class Output
6
+ class Issue
7
+ attr_accessor :issue
8
+
9
+ def initialize(issue)
10
+ @issue = issue
11
+ end
12
+
13
+ def tokens
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ module Heat
5
+ class Output
6
+ class Location
7
+ attr_accessor :location
8
+
9
+ def initialize(location)
10
+ @location = location
11
+ end
12
+
13
+ def tokens
14
+ end
15
+
16
+ private
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ module Heat
5
+ class Output
6
+ class Map
7
+ attr_accessor :map
8
+
9
+ def initialize(map)
10
+ @map = map
11
+ end
12
+
13
+ def tokens
14
+ end
15
+
16
+ private
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ module Heat
5
+ class Output
6
+ class Results
7
+ extend Forwardable
8
+
9
+ attr_accessor :results
10
+
11
+ def_delegators :@results, :errors, :brokens, :failures, :slows, :skips, :problems?, :slows?
12
+
13
+ def initialize(results)
14
+ @results = results
15
+ @tokens = []
16
+ end
17
+
18
+ def tokens
19
+ @tokens << [*issue_counts_tokens] if issue_counts_tokens&.any?
20
+ @tokens << [assertions_count_token, test_count_token]
21
+ @tokens << [assertions_performance_token, tests_performance_token, timing_token]
22
+
23
+ @tokens
24
+ end
25
+
26
+ private
27
+
28
+ def pluralize(count, singular)
29
+ singular_style = "#{count} #{singular}"
30
+
31
+ # Given the narrow scope, pluralization can be relatively naive here
32
+ count > 1 ? "#{singular_style}s" : singular_style
33
+ end
34
+
35
+ def issue_counts_tokens
36
+ return unless problems? || slows?
37
+
38
+ counts = [error_count_token, broken_count_token, failure_count_token, skip_count_token, slow_count_token].compact
39
+
40
+ # # Create an array of separator tokens one less than the total number of issue count tokens
41
+ separator_tokens = Array.new(counts.size, separator_token)
42
+
43
+ counts_with_separators = counts
44
+ .zip(separator_tokens) # Add separators between the counts
45
+ .flatten(1) # Flatten the zipped separators, but no more
46
+
47
+ counts_with_separators.pop # Remove the final trailing zipped separator that's not needed
48
+
49
+ counts_with_separators
50
+ end
51
+
52
+ def error_count_token
53
+ issue_count_token(:error, errors)
54
+ end
55
+
56
+ def broken_count_token
57
+ issue_count_token(:broken, brokens)
58
+ end
59
+
60
+ def failure_count_token
61
+ issue_count_token(:failure, failures)
62
+ end
63
+
64
+ def skip_count_token
65
+ style = problems? ? :muted : :skipped
66
+ issue_count_token(style, skips, name: 'Skip')
67
+ end
68
+
69
+ def slow_count_token
70
+ style = problems? ? :muted : :slow
71
+ issue_count_token(style, slows, name: 'Slow')
72
+ end
73
+
74
+ def assertions_performance_token
75
+ [:bold, "#{results.assertions_per_second} assertions/s"]
76
+ end
77
+
78
+ def tests_performance_token
79
+ [:default, " and #{results.tests_per_second} tests/s"]
80
+ end
81
+
82
+ def timing_token
83
+ [:default, " in #{results.total_time.round(2)}s"]
84
+ end
85
+
86
+ def assertions_count_token
87
+ [:muted, pluralize(results.assertion_count, 'Assertion')]
88
+ end
89
+
90
+ def test_count_token
91
+ [:muted, " across #{pluralize(results.test_count, 'Test')}"]
92
+ end
93
+
94
+ def issue_count_token(type, collection, name: type.capitalize)
95
+ return nil if collection.empty?
96
+
97
+ [type, pluralize(collection.size, name)]
98
+ end
99
+
100
+ def separator_token
101
+ [:muted, ' · ']
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ module Heat
5
+ class Output
6
+ # Builds the collection of tokens representing a specific set of source code lines
7
+ class SourceCode
8
+ DEFAULT_LINE_COUNT = 3
9
+ DEFAULT_INDENTATION_SPACES = 2
10
+ HIGHLIGHT_KEY_LINE = true
11
+
12
+ attr_reader :filename, :line_number, :max_line_count
13
+
14
+ # Provides a collection of tokens representing the output of source code
15
+ # @param filename [String] the absolute path to the file containing the source code
16
+ # @param line_number [Integer, String] the primary line number of interest for the file
17
+ # @param max_line_count: DEFAULT_LINE_COUNT [Integer] maximum total number of lines to
18
+ # retrieve around the target line (including the target line)
19
+ #
20
+ # @return [self]
21
+ def initialize(filename, line_number, max_line_count: DEFAULT_LINE_COUNT)
22
+ @filename = filename
23
+ @line_number = line_number.to_s
24
+ @max_line_count = max_line_count
25
+ @tokens = []
26
+ end
27
+
28
+ # The collection of style content tokens to print
29
+ #
30
+ # @return [Array<Array<Token>>] an array of arrays of tokens where each top-level array
31
+ # represents a line where the first element is the line_number and the second is the line
32
+ # of code to display
33
+ def tokens
34
+ source.lines.each_index do |i|
35
+ current_line_number = source.line_numbers[i]
36
+ current_line_of_code = source.lines[i]
37
+
38
+ number_style, line_style = styles_for(current_line_of_code)
39
+
40
+ @tokens << [
41
+ line_number_token(number_style, current_line_number),
42
+ line_of_code_token(line_style, current_line_of_code)
43
+ ]
44
+ end
45
+ @tokens
46
+ end
47
+
48
+ # The number of digits for the largest line number returned. This is used for formatting and
49
+ # text justification so that line numbers are right-aligned
50
+ #
51
+ # @return [Integer] the number of digits in the longest line number returned
52
+ def max_line_number_digits
53
+ source
54
+ .line_numbers
55
+ .map(&:to_s)
56
+ .map(&:length)
57
+ .max
58
+ end
59
+
60
+ # Whether to visually highlight the target line when displaying the source code. Currently
61
+ # defauls to true, but long-term, this is a likely candidate to be configurable. For
62
+ # example, in the future, highlighting could only be used if the source includes more than
63
+ # three lines. Or it could be something end users could disable in order to reduce noise.
64
+ #
65
+ # @return [Boolean] true if the target line should be highlighted
66
+ def highlight_key_line?
67
+ HIGHLIGHT_KEY_LINE
68
+ end
69
+
70
+ # The number of spaces each line of code should be indented. Currently defaults to 2 in
71
+ # order to provide visual separation between test failures, but in the future, it could
72
+ # be configurable in order to save horizontal space and create more compact output. For
73
+ # example, it could be smart based on line length and total available horizontal terminal
74
+ # space, or there could be higher-level "display" setting that could have a `:compact`
75
+ # option that would reduce the space used.
76
+ #
77
+ # @return [type] [description]
78
+ def indentation
79
+ DEFAULT_INDENTATION_SPACES
80
+ end
81
+
82
+ private
83
+
84
+ # The source instance for retrieving the relevant lines of source code
85
+ #
86
+ # @return [Source] a Minitest::Heat::Source instance
87
+ def source
88
+ @source ||= Minitest::Heat::Source.new(
89
+ filename,
90
+ line_number: line_number,
91
+ max_line_count: max_line_count
92
+ )
93
+ end
94
+
95
+ # Determines how to style a given line of code token. For now, it's only used for
96
+ # highlighting the targeted line of code, but it could also be adjusted to mute the line
97
+ # number or otherwise change the styling of how lines of code are displayed
98
+ # @param line_of_code [String] the content representing the line of code we're currently
99
+ # generating a token for
100
+ #
101
+ # @return [Array<Symbol>] the Token styles for the line number and line of code
102
+ def styles_for(line_of_code)
103
+ if line_of_code == source.line && highlight_key_line?
104
+ [:default, :default]
105
+ else
106
+ [:muted, :muted]
107
+ end
108
+ end
109
+
110
+ # The token representing a given line number. Adds the appropriate indention and
111
+ # justification to right align the line numbers
112
+ # @param style [Symbol] the symbol representing the style for the line number token
113
+ # @param line_number [Integer,String] the digits representing the line number
114
+ #
115
+ # @return [Array] the style/content token for the current line number
116
+ def line_number_token(style, line_number)
117
+ [style, "#{' ' * indentation}#{line_number.to_s.rjust(max_line_number_digits)} "]
118
+ end
119
+
120
+ # The token representing the content of a given line of code.
121
+ # @param style [Symbol] the symbol representing the style for the line of code token
122
+ # @param line_number [Integer,String] the content of the line of code
123
+ #
124
+ # @return [Array] the style/content token for the current line of code
125
+ def line_of_code_token(style, line_of_code)
126
+ [style, line_of_code]
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,99 @@
1
+ module Minitest
2
+ module Heat
3
+ # Friendly API for printing nicely-formatted output to the console
4
+ class Output
5
+ class Token
6
+ class InvalidStyle < ArgumentError; end
7
+
8
+ 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],
16
+ 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]
21
+ }.freeze
22
+
23
+ attr_accessor :style_key, :content
24
+
25
+ def initialize(style_key, content)
26
+ @style_key = style_key
27
+ @content = content
28
+ end
29
+
30
+ def to_s(format = :styled)
31
+ return content unless format == :styled
32
+
33
+ [
34
+ style_string,
35
+ content,
36
+ reset_string
37
+ ].join
38
+ end
39
+
40
+ def eql?(other)
41
+ style_key == other.style_key && content == other.content
42
+ end
43
+ alias :== eql?
44
+
45
+ private
46
+
47
+ ESC_SEQUENCE = "\e["
48
+ END_SEQUENCE = "m"
49
+
50
+ WEIGHTS = {
51
+ default: 0,
52
+ bold: 1,
53
+ light: 2,
54
+ italic: 3
55
+ }.freeze
56
+
57
+ COLORS = {
58
+ black: 30,
59
+ red: 31,
60
+ green: 32,
61
+ yellow: 33,
62
+ blue: 34,
63
+ magenta: 35,
64
+ cyan: 36,
65
+ gray: 37,
66
+ default: 39
67
+ }.freeze
68
+
69
+ def style_string
70
+ "#{ESC_SEQUENCE}#{weight};#{color}#{END_SEQUENCE}"
71
+ end
72
+
73
+ def reset_string
74
+ "#{ESC_SEQUENCE}0#{END_SEQUENCE}"
75
+ end
76
+
77
+ def weight_key
78
+ style_components[0]
79
+ end
80
+
81
+ def color_key
82
+ style_components[1]
83
+ end
84
+
85
+ def weight
86
+ WEIGHTS.fetch(weight_key)
87
+ end
88
+
89
+ def color
90
+ COLORS.fetch(color_key)
91
+ end
92
+
93
+ def style_components
94
+ STYLES.fetch(style_key) { raise InvalidStyle, "'#{style_key}' is not a valid style option for tokens" }
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end