minitest-heat 0.0.3 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +23 -0
- data/Gemfile +5 -3
- data/Gemfile.lock +30 -1
- data/README.md +12 -3
- data/Rakefile +8 -6
- data/lib/minitest/heat/backtrace.rb +25 -48
- data/lib/minitest/heat/hit.rb +83 -0
- data/lib/minitest/heat/issue.rb +69 -34
- data/lib/minitest/heat/line.rb +74 -0
- data/lib/minitest/heat/location.rb +144 -23
- data/lib/minitest/heat/map.rb +14 -24
- data/lib/minitest/heat/output/backtrace.rb +121 -0
- data/lib/minitest/heat/output/issue.rb +179 -0
- data/lib/minitest/heat/output/map.rb +67 -0
- data/lib/minitest/heat/output/marker.rb +50 -0
- data/lib/minitest/heat/output/results.rb +128 -0
- data/lib/minitest/heat/output/source_code.rb +131 -0
- data/lib/minitest/heat/output/token.rb +101 -0
- data/lib/minitest/heat/output.rb +42 -228
- data/lib/minitest/heat/results.rb +18 -78
- data/lib/minitest/heat/source.rb +6 -1
- data/lib/minitest/heat/timer.rb +81 -0
- data/lib/minitest/heat/version.rb +3 -1
- data/lib/minitest/heat.rb +3 -0
- data/lib/minitest/heat_plugin.rb +5 -5
- data/lib/minitest/heat_reporter.rb +53 -29
- data/minitest-heat.gemspec +4 -2
- metadata +70 -3
@@ -2,59 +2,180 @@
|
|
2
2
|
|
3
3
|
module Minitest
|
4
4
|
module Heat
|
5
|
+
# Convenience methods for determining the file and line number where the problem occurred.
|
6
|
+
# There are several layers of specificity to help make it easy to communicate the relative
|
7
|
+
# location of the failure:
|
8
|
+
# - 'final' represents the final line of the backtrace regardless of where it is
|
9
|
+
# - 'test' represents the last line from the project's tests. It is further differentiated by
|
10
|
+
# the line where the test is defined and the actual line of code in the test that geneated
|
11
|
+
# the failure or exception
|
12
|
+
# - 'source_code' represents the last line from the project's source code
|
13
|
+
# - 'project' represents the last source line, but falls back to the last test line
|
14
|
+
# - 'most_relevant' represents the most specific file to investigate starting with the source
|
15
|
+
# code and then looking to the test code with final line of the backtrace as a fallback
|
5
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
|
+
|
6
25
|
attr_reader :test_location, :backtrace
|
7
26
|
|
8
27
|
def initialize(test_location, backtrace = [])
|
9
|
-
@test_location = test_location
|
28
|
+
@test_location = TestDefinition.new(*test_location)
|
10
29
|
@backtrace = Backtrace.new(backtrace)
|
11
30
|
end
|
12
31
|
|
32
|
+
# Prints the pathname and line number of the location most likely to be the source of the
|
33
|
+
# test failure
|
34
|
+
#
|
35
|
+
# @return [String] ex. 'path/to/file.rb:12'
|
13
36
|
def to_s
|
14
|
-
"#{
|
37
|
+
"#{most_relevant_file}:#{most_relevant_failure_line}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def local?
|
41
|
+
broken_test? || proper_failure?
|
42
|
+
end
|
43
|
+
|
44
|
+
# Knows if the failure is contained within the test. For example, if there's bad code in a
|
45
|
+
# test, and it raises an exception, then it's really a broken test rather than a proper
|
46
|
+
# faiure.
|
47
|
+
#
|
48
|
+
# @return [Boolean] true if most relevant file is the same as the test location file
|
49
|
+
def broken_test?
|
50
|
+
!test_file.nil? && test_file == most_relevant_file
|
51
|
+
end
|
52
|
+
|
53
|
+
# Knows if the failure occurred in the actual project source code—as opposed to the test or
|
54
|
+
# an external piece of code like a gem.
|
55
|
+
#
|
56
|
+
# @return [Boolean] true if there's a non-test project file in the stacktrace but it's not
|
57
|
+
# a result of a broken test
|
58
|
+
def proper_failure?
|
59
|
+
!source_code_file.nil? && !broken_test?
|
60
|
+
end
|
61
|
+
|
62
|
+
# The file most likely to be the source of the underlying problem. Often, the most recent
|
63
|
+
# backtrace files will be a gem or external library that's failing indirectly as a result
|
64
|
+
# of a problem with local source code (not always, but frequently). In that case, the best
|
65
|
+
# first place to focus is on the code you control.
|
66
|
+
#
|
67
|
+
# @return [String] the relative path to the file from the project root
|
68
|
+
def most_relevant_file
|
69
|
+
Pathname(most_relevant_location.pathname)
|
70
|
+
end
|
71
|
+
|
72
|
+
# The line number of the `most_relevant_file` where the failure originated
|
73
|
+
#
|
74
|
+
# @return [Integer] line number
|
75
|
+
def most_relevant_failure_line
|
76
|
+
most_relevant_location.line_number
|
77
|
+
end
|
78
|
+
|
79
|
+
# The final location of the stacktrace regardless of whether it's from within the project
|
80
|
+
#
|
81
|
+
# @return [String] the relative path to the file from the project root
|
82
|
+
def final_file
|
83
|
+
Pathname(final_location.pathname)
|
15
84
|
end
|
16
85
|
|
17
|
-
|
18
|
-
|
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
|
19
91
|
end
|
20
92
|
|
21
|
-
|
22
|
-
|
93
|
+
# The final location of the stacktrace regardless of whether it's from within the project
|
94
|
+
#
|
95
|
+
# @return [String] the relative path to the file from the project root
|
96
|
+
def project_file
|
97
|
+
broken_test? ? test_file : source_code_file
|
23
98
|
end
|
24
99
|
|
100
|
+
# The line number of the `project_file` where the failure originated
|
101
|
+
#
|
102
|
+
# @return [Integer] line number
|
103
|
+
def project_failure_line
|
104
|
+
broken_test? ? test_failure_line || test_definition_line : source_code_failure_line
|
105
|
+
end
|
106
|
+
|
107
|
+
# The final location from the stacktrace that is within the project directory
|
108
|
+
#
|
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
|
114
|
+
end
|
115
|
+
|
116
|
+
# The line number of the `source_code_file` where the failure originated
|
117
|
+
#
|
118
|
+
# @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
|
123
|
+
end
|
124
|
+
|
125
|
+
# The final location from the stacktrace that is within the project's test directory
|
126
|
+
#
|
127
|
+
# @return [String, nil] the relative path to the file from the project root
|
25
128
|
def test_file
|
26
|
-
|
129
|
+
Pathname(test_location.pathname)
|
27
130
|
end
|
28
131
|
|
132
|
+
# The line number of the `test_file` where the test is defined
|
133
|
+
#
|
134
|
+
# @return [Integer] line number
|
29
135
|
def test_definition_line
|
30
|
-
test_location
|
136
|
+
test_location.line_number
|
31
137
|
end
|
32
138
|
|
139
|
+
# The line number from within the `test_file` test definition where the failure occurred
|
140
|
+
#
|
141
|
+
# @return [Integer] line number
|
33
142
|
def test_failure_line
|
34
|
-
|
143
|
+
backtrace.final_test_location&.line_number || test_definition_line
|
35
144
|
end
|
36
145
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
146
|
+
# The line number from within the `test_file` test definition where the failure occurred
|
147
|
+
#
|
148
|
+
# @return [Location] the last location from the backtrace or the test location if a backtrace
|
149
|
+
# was not passed to the initializer
|
150
|
+
def final_location
|
151
|
+
backtrace? ? backtrace.final_location : test_location
|
43
152
|
end
|
44
153
|
|
45
|
-
|
46
|
-
|
154
|
+
# The file most likely to be the source of the underlying problem. Often, the most recent
|
155
|
+
# backtrace files will be a gem or external library that's failing indirectly as a result
|
156
|
+
# of a problem with local source code (not always, but frequently). In that case, the best
|
157
|
+
# first place to focus is on the code you control.
|
158
|
+
#
|
159
|
+
# @return [Array] file and line number of the most likely source of the problem
|
160
|
+
def most_relevant_location
|
161
|
+
[
|
162
|
+
source_code_location,
|
163
|
+
test_location,
|
164
|
+
final_location
|
165
|
+
].compact.first
|
166
|
+
end
|
47
167
|
|
48
|
-
|
168
|
+
def project_location
|
169
|
+
source_code_location || test_location
|
49
170
|
end
|
50
171
|
|
51
|
-
|
172
|
+
def source_code_location
|
173
|
+
backtrace.final_source_code_location
|
174
|
+
end
|
52
175
|
|
53
|
-
def
|
54
|
-
|
176
|
+
def backtrace?
|
177
|
+
backtrace.parsed_entries.any?
|
55
178
|
end
|
56
179
|
end
|
57
180
|
end
|
58
181
|
end
|
59
|
-
|
60
|
-
|
data/lib/minitest/heat/map.rb
CHANGED
@@ -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
|
@@ -13,44 +15,32 @@ module Minitest
|
|
13
15
|
# test is broken (i.e. raising an exception), that's a special sort of failure that would be
|
14
16
|
# misleading. It doesn't represent a proper failure, but rather a test that doesn't work.
|
15
17
|
WEIGHTS = {
|
16
|
-
error: 3,
|
17
|
-
broken:
|
18
|
-
failure: 1,
|
19
|
-
skipped: 0,
|
20
|
-
|
21
|
-
|
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
|
22
25
|
|
23
26
|
def initialize
|
24
27
|
@hits = {}
|
25
28
|
end
|
26
29
|
|
27
30
|
def add(filename, line_number, type)
|
28
|
-
@hits[filename] ||=
|
29
|
-
@hits[filename][:total] += 1
|
30
|
-
@hits[filename][:weight] += WEIGHTS[type]
|
31
|
+
@hits[filename] ||= Hit.new(filename)
|
31
32
|
|
32
|
-
@hits[filename]
|
33
|
-
@hits[filename][type] << line_number
|
33
|
+
@hits[filename].log(type, line_number)
|
34
34
|
end
|
35
35
|
|
36
|
-
def
|
37
|
-
hot_files
|
38
|
-
.sort_by { |filename, weight| weight }
|
39
|
-
.reverse
|
40
|
-
.take(5)
|
36
|
+
def file_hits
|
37
|
+
hot_files.take(MAXIMUM_FILES_TO_SHOW)
|
41
38
|
end
|
42
39
|
|
43
40
|
private
|
44
41
|
|
45
42
|
def hot_files
|
46
|
-
|
47
|
-
@hits.each_pair do |filename, details|
|
48
|
-
# Can't really be a "hot spot" with just a single issue
|
49
|
-
next unless details[:total] > 1
|
50
|
-
|
51
|
-
files[filename] = details[:weight]
|
52
|
-
end
|
53
|
-
files
|
43
|
+
hits.values.sort_by(&:weight).reverse
|
54
44
|
end
|
55
45
|
end
|
56
46
|
end
|
@@ -0,0 +1,121 @@
|
|
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_entries.each do |backtrace_entry|
|
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_tokens(backtrace_entry),
|
30
|
+
source_code_line_token(backtrace_entry.source_code)
|
31
|
+
]
|
32
|
+
|
33
|
+
parts << file_freshness(backtrace_entry) if most_recently_modified?(backtrace_entry)
|
34
|
+
|
35
|
+
@tokens << parts
|
36
|
+
end
|
37
|
+
|
38
|
+
@tokens
|
39
|
+
end
|
40
|
+
|
41
|
+
def line_count
|
42
|
+
DEFAULT_LINE_COUNT
|
43
|
+
end
|
44
|
+
|
45
|
+
# This should probably be smart about what lines are displayed in a backtrace.
|
46
|
+
# Maybe...
|
47
|
+
# ...it could intelligently display the full back trace?
|
48
|
+
# ...only the backtrace from the first/last line of project source?
|
49
|
+
# ...it behaves a little different when it's a broken test vs. a true exception?
|
50
|
+
# ...it could be smart about subtly flagging the lines that show up in the heat map frequently?
|
51
|
+
# ...it could be influenced by a "compact" or "robust" reporter super-style?
|
52
|
+
# ...it's smart about exceptions that were raised outside of the project?
|
53
|
+
# ...it's smart about highlighting lines of code differently based on whether it's source code, test code, or external code?
|
54
|
+
def backtrace_entries
|
55
|
+
all_entries
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def all_backtrace_entries_from_project?
|
61
|
+
backtrace_entries.all? { |line| line.path.to_s.include?(project_root_dir) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def project_root_dir
|
65
|
+
Dir.pwd
|
66
|
+
end
|
67
|
+
|
68
|
+
def project_entries
|
69
|
+
backtrace.project_entries.take(line_count)
|
70
|
+
end
|
71
|
+
|
72
|
+
def all_entries
|
73
|
+
backtrace.parsed_entries.take(line_count)
|
74
|
+
end
|
75
|
+
|
76
|
+
def most_recently_modified?(line)
|
77
|
+
# If there's more than one line being displayed, and the current line is the freshest
|
78
|
+
backtrace_entries.size > 1 && line == backtrace.freshest_project_location
|
79
|
+
end
|
80
|
+
|
81
|
+
def indentation_token
|
82
|
+
[:default, ' ' * indentation]
|
83
|
+
end
|
84
|
+
|
85
|
+
def path_token(line)
|
86
|
+
path = "#{line.path}/"
|
87
|
+
|
88
|
+
# If all of the backtrace lines are from the project, no point in the added redundant
|
89
|
+
# noise of showing the project root directory over and over again
|
90
|
+
path = path.delete_prefix(project_root_dir) if all_backtrace_entries_from_project?
|
91
|
+
|
92
|
+
[:muted, path]
|
93
|
+
end
|
94
|
+
|
95
|
+
def file_and_line_number_tokens(backtrace_entry)
|
96
|
+
[[:default, backtrace_entry.file], [:muted, ':'], [:default, backtrace_entry.line_number]]
|
97
|
+
end
|
98
|
+
|
99
|
+
def source_code_line_token(source_code)
|
100
|
+
[:muted, " #{Output::SYMBOLS[:arrow]} `#{source_code.line.strip}`"]
|
101
|
+
end
|
102
|
+
|
103
|
+
def file_freshness(_line)
|
104
|
+
[:default, " #{Output::SYMBOLS[:middot]} Most Recently Modified File"]
|
105
|
+
end
|
106
|
+
|
107
|
+
# The number of spaces each line of code should be indented. Currently defaults to 2 in
|
108
|
+
# order to provide visual separation between test failures, but in the future, it could
|
109
|
+
# be configurable in order to save horizontal space and create more compact output. For
|
110
|
+
# example, it could be smart based on line length and total available horizontal terminal
|
111
|
+
# space, or there could be higher-level "display" setting that could have a `:compact`
|
112
|
+
# option that would reduce the space used.
|
113
|
+
#
|
114
|
+
# @return [type] [description]
|
115
|
+
def indentation
|
116
|
+
DEFAULT_INDENTATION_SPACES
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,179 @@
|
|
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
|
+
case issue.type
|
15
|
+
when :error then error_tokens
|
16
|
+
when :broken then broken_tokens
|
17
|
+
when :failure then failure_tokens
|
18
|
+
when :skipped then skipped_tokens
|
19
|
+
when :painful then painful_tokens
|
20
|
+
when :slow then slow_tokens
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def error_tokens
|
27
|
+
[
|
28
|
+
headline_tokens,
|
29
|
+
test_location_tokens,
|
30
|
+
location_tokens,
|
31
|
+
summary_tokens,
|
32
|
+
*backtrace_tokens,
|
33
|
+
newline_tokens
|
34
|
+
]
|
35
|
+
end
|
36
|
+
|
37
|
+
def broken_tokens
|
38
|
+
[
|
39
|
+
headline_tokens,
|
40
|
+
test_location_tokens,
|
41
|
+
summary_tokens,
|
42
|
+
*backtrace_tokens,
|
43
|
+
newline_tokens
|
44
|
+
]
|
45
|
+
end
|
46
|
+
|
47
|
+
def failure_tokens
|
48
|
+
[
|
49
|
+
headline_tokens,
|
50
|
+
test_location_tokens,
|
51
|
+
summary_tokens,
|
52
|
+
newline_tokens
|
53
|
+
]
|
54
|
+
end
|
55
|
+
|
56
|
+
def skipped_tokens
|
57
|
+
[
|
58
|
+
headline_tokens,
|
59
|
+
test_location_tokens,
|
60
|
+
newline_tokens
|
61
|
+
]
|
62
|
+
end
|
63
|
+
|
64
|
+
def painful_tokens
|
65
|
+
[
|
66
|
+
headline_tokens,
|
67
|
+
slowness_summary_tokens,
|
68
|
+
newline_tokens
|
69
|
+
]
|
70
|
+
end
|
71
|
+
|
72
|
+
def slow_tokens
|
73
|
+
[
|
74
|
+
headline_tokens,
|
75
|
+
slowness_summary_tokens,
|
76
|
+
newline_tokens
|
77
|
+
]
|
78
|
+
end
|
79
|
+
|
80
|
+
def headline_tokens
|
81
|
+
[[issue.type, issue.label], spacer_token, [:default, issue.test_name]]
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_name_and_class_tokens
|
85
|
+
[[:default, issue.test_class], *test_location_tokens ]
|
86
|
+
end
|
87
|
+
|
88
|
+
def backtrace_tokens
|
89
|
+
backtrace = ::Minitest::Heat::Output::Backtrace.new(issue.location)
|
90
|
+
|
91
|
+
backtrace.tokens
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_location_tokens
|
95
|
+
[[:default, test_file_short_location], [:muted, ':'], [:default, issue.test_definition_line], arrow_token, [:default, issue.test_failure_line], [:muted, test_line_source]]
|
96
|
+
end
|
97
|
+
|
98
|
+
def location_tokens
|
99
|
+
[[:default, most_relevant_short_location], [:muted, ':'], [:default, issue.location.most_relevant_failure_line], [:muted, most_relevant_line_source]]
|
100
|
+
end
|
101
|
+
|
102
|
+
def source_tokens
|
103
|
+
filename = issue.location.project_file
|
104
|
+
line_number = issue.location.project_failure_line
|
105
|
+
|
106
|
+
# source_code = ::Minitest::Heat::Output::SourceCode.new(filename, line_number, max_line_count: 1)
|
107
|
+
# source_code.tokens
|
108
|
+
|
109
|
+
source = Minitest::Heat::Source.new(filename, line_number: line_number)
|
110
|
+
[[:muted, " #{Output::SYMBOLS[:arrow]} `#{source.line.strip}`"]]
|
111
|
+
end
|
112
|
+
|
113
|
+
def summary_tokens
|
114
|
+
[[:italicized, issue.summary.delete_suffix("---------------")]]
|
115
|
+
end
|
116
|
+
|
117
|
+
def slowness_summary_tokens
|
118
|
+
[[:bold, issue.slowness], spacer_token, [:default, issue.short_location]]
|
119
|
+
end
|
120
|
+
|
121
|
+
def newline_tokens
|
122
|
+
[]
|
123
|
+
end
|
124
|
+
|
125
|
+
def most_relevant_short_location
|
126
|
+
issue.location.most_relevant_file.to_s.delete_prefix("#{Dir.pwd}/")
|
127
|
+
end
|
128
|
+
|
129
|
+
def test_file_short_location
|
130
|
+
issue.location.test_file.to_s.delete_prefix("#{Dir.pwd}/")
|
131
|
+
end
|
132
|
+
|
133
|
+
def most_relevant_line_source
|
134
|
+
filename = issue.location.project_file
|
135
|
+
line_number = issue.location.project_failure_line
|
136
|
+
|
137
|
+
source = Minitest::Heat::Source.new(filename, line_number: line_number)
|
138
|
+
"\n #{source.line.strip}"
|
139
|
+
end
|
140
|
+
|
141
|
+
def test_line_source
|
142
|
+
filename = issue.location.test_file
|
143
|
+
line_number = issue.location.test_failure_line
|
144
|
+
|
145
|
+
source = Minitest::Heat::Source.new(filename, line_number: line_number)
|
146
|
+
"\n #{source.line.strip}"
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
# def failure_summary_tokens
|
151
|
+
# return unless issue_summary_lines.any?
|
152
|
+
|
153
|
+
# # Sometimes, the exception message is multiple lines, so this adjusts the lines to
|
154
|
+
# # visually group them together a bit
|
155
|
+
# if issue_summary_lines.one?
|
156
|
+
# [[[:italicized, issue_summary_lines.first]]]
|
157
|
+
# else
|
158
|
+
# issue_summary_lines.map do |line|
|
159
|
+
# [Output::TOKENS[:muted_lead], [:italicized, line]]
|
160
|
+
# end
|
161
|
+
# end
|
162
|
+
# end
|
163
|
+
|
164
|
+
# def issue_summary_lines
|
165
|
+
# @issue_summary_lines ||= issue.summary.split("\n")
|
166
|
+
# end
|
167
|
+
|
168
|
+
def spacer_token
|
169
|
+
Output::TOKENS[:spacer]
|
170
|
+
end
|
171
|
+
|
172
|
+
def arrow_token
|
173
|
+
Output::TOKENS[:muted_arrow]
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Minitest
|
4
|
+
module Heat
|
5
|
+
class Output
|
6
|
+
class Map
|
7
|
+
# extend Forwardable
|
8
|
+
|
9
|
+
attr_accessor :map
|
10
|
+
|
11
|
+
# def_delegators :@results, :errors, :brokens, :failures, :slows, :skips, :problems?, :slows?
|
12
|
+
|
13
|
+
def initialize(map)
|
14
|
+
@map = map
|
15
|
+
@tokens = []
|
16
|
+
end
|
17
|
+
|
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
|
27
|
+
end
|
28
|
+
|
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
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
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
|