minitest-heat 0.0.1 → 0.0.5

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