minitest-heat 0.0.4 → 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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/lib/minitest/heat/backtrace.rb +48 -16
- data/lib/minitest/heat/issue.rb +9 -5
- data/lib/minitest/heat/location.rb +134 -19
- data/lib/minitest/heat/map.rb +1 -1
- data/lib/minitest/heat/output/backtrace.rb +131 -0
- data/lib/minitest/heat/output/issue.rb +18 -0
- data/lib/minitest/heat/output/location.rb +20 -0
- data/lib/minitest/heat/output/map.rb +20 -0
- data/lib/minitest/heat/output/results.rb +106 -0
- data/lib/minitest/heat/output/source_code.rb +131 -0
- data/lib/minitest/heat/output/token.rb +99 -0
- data/lib/minitest/heat/output.rb +71 -158
- data/lib/minitest/heat/results.rb +4 -0
- data/lib/minitest/heat/source.rb +6 -1
- data/lib/minitest/heat/version.rb +1 -1
- data/lib/minitest/heat_reporter.rb +0 -2
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c760c15a45811d5773dee18e32aadc063ed61c3eb04447c637719d5d3d46178a
|
4
|
+
data.tar.gz: 90c345c56cbd2589719eef8a0264a5973f77a8104c91149f4e4ad2320067a49a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 47be296804a2171023bfcdb5c4192376698349bd76c4c3a2991d9accd9f631b88d2ffb867d821f2479c41bd6fd2cf919af325ecde975f98ee4592277b3b09432
|
7
|
+
data.tar.gz: 79a180260e9a1d8c55d898883f13969f2576ceb6450dc9e080050e55674c73fc7979202a2c8a78542958491fce91fee2a3cb1b66fd2705edc4a8d432f5e28fcc
|
data/Gemfile.lock
CHANGED
@@ -6,11 +6,23 @@ module Minitest
|
|
6
6
|
class Backtrace
|
7
7
|
Line = Struct.new(:path, :file, :number, :container, :mtime, keyword_init: true) do
|
8
8
|
def to_s
|
9
|
-
"#{
|
9
|
+
"#{location} in `#{container}`"
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
13
|
-
"#{path}/#{file}"
|
12
|
+
def pathname
|
13
|
+
Pathname("#{path}/#{file}")
|
14
|
+
end
|
15
|
+
|
16
|
+
def location
|
17
|
+
"#{pathname.to_s}:#{number}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def short_pathname
|
21
|
+
pathname.delete_prefix(Dir.pwd)
|
22
|
+
end
|
23
|
+
|
24
|
+
def short_location
|
25
|
+
"#{pathname.basename.to_s}:#{number}"
|
14
26
|
end
|
15
27
|
|
16
28
|
def age_in_seconds
|
@@ -18,6 +30,16 @@ module Minitest
|
|
18
30
|
end
|
19
31
|
end
|
20
32
|
|
33
|
+
UNREADABLE_FILE_ATTRIBUTES = {
|
34
|
+
path: '(Unknown Path)',
|
35
|
+
file: '(Unknown File)',
|
36
|
+
number: '(Unknown Line Number)',
|
37
|
+
container: '(Unknown Method)',
|
38
|
+
mtime: '(Unknown Modification Time)'
|
39
|
+
}
|
40
|
+
|
41
|
+
UNREADABLE_LINE = Line.new(UNREADABLE_FILE_ATTRIBUTES)
|
42
|
+
|
21
43
|
attr_reader :raw_backtrace
|
22
44
|
|
23
45
|
def initialize(raw_backtrace)
|
@@ -29,37 +51,45 @@ module Minitest
|
|
29
51
|
end
|
30
52
|
|
31
53
|
def final_location
|
32
|
-
|
54
|
+
parsed_lines.first
|
33
55
|
end
|
34
56
|
|
35
57
|
def final_project_location
|
36
|
-
|
58
|
+
project_lines.first
|
59
|
+
end
|
60
|
+
|
61
|
+
def freshest_project_location
|
62
|
+
recently_modified_lines.first
|
63
|
+
end
|
64
|
+
|
65
|
+
def final_source_code_location
|
66
|
+
source_code_lines.first
|
37
67
|
end
|
38
68
|
|
39
69
|
def final_test_location
|
40
|
-
|
70
|
+
test_lines.first
|
41
71
|
end
|
42
72
|
|
43
|
-
def
|
44
|
-
|
73
|
+
def project_lines
|
74
|
+
@project_lines ||= parsed_lines.select { |line| line[:path].include?(Dir.pwd) }
|
45
75
|
end
|
46
76
|
|
47
|
-
def
|
48
|
-
@
|
77
|
+
def recently_modified_lines
|
78
|
+
@recently_modified_lines ||= project_lines.sort_by { |line| line[:mtime] }.reverse
|
49
79
|
end
|
50
80
|
|
51
|
-
def
|
52
|
-
@
|
81
|
+
def test_lines
|
82
|
+
@tests_lines ||= project_lines.select { |line| test_file?(line) }
|
53
83
|
end
|
54
84
|
|
55
|
-
def
|
56
|
-
@
|
85
|
+
def source_code_lines
|
86
|
+
@source_code_lines ||= project_lines - test_lines
|
57
87
|
end
|
58
88
|
|
59
|
-
def
|
89
|
+
def parsed_lines
|
60
90
|
return [] if raw_backtrace.nil?
|
61
91
|
|
62
|
-
@
|
92
|
+
@parsed_lines ||= raw_backtrace.map { |line| parse(line) }
|
63
93
|
end
|
64
94
|
|
65
95
|
private
|
@@ -83,6 +113,8 @@ module Minitest
|
|
83
113
|
container: reduce_container(parts[2]),
|
84
114
|
mtime: pathname.exist? ? pathname.mtime : nil
|
85
115
|
}
|
116
|
+
rescue
|
117
|
+
UNREADABLE_FILE_ATTRIBUTES
|
86
118
|
end
|
87
119
|
|
88
120
|
def test_file?(line)
|
data/lib/minitest/heat/issue.rb
CHANGED
@@ -40,10 +40,14 @@ module Minitest
|
|
40
40
|
@location = Location.new(result.source_location, @failure&.backtrace)
|
41
41
|
end
|
42
42
|
|
43
|
+
def short_location
|
44
|
+
location.to_s.delete_prefix(Dir.pwd)
|
45
|
+
end
|
46
|
+
|
43
47
|
def to_hit
|
44
48
|
[
|
45
|
-
location.
|
46
|
-
location.
|
49
|
+
location.project_file.to_s,
|
50
|
+
location.project_failure_line,
|
47
51
|
type
|
48
52
|
]
|
49
53
|
end
|
@@ -87,15 +91,15 @@ module Minitest
|
|
87
91
|
end
|
88
92
|
|
89
93
|
def in_test?
|
90
|
-
location.
|
94
|
+
location.broken_test?
|
91
95
|
end
|
92
96
|
|
93
97
|
def in_source?
|
94
|
-
location.
|
98
|
+
location.proper_failure?
|
95
99
|
end
|
96
100
|
|
97
101
|
def test_class
|
98
|
-
result.klass
|
102
|
+
result.klass
|
99
103
|
end
|
100
104
|
|
101
105
|
def test_identifier
|
@@ -2,6 +2,17 @@
|
|
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
|
6
17
|
attr_reader :test_location, :backtrace
|
7
18
|
|
@@ -10,48 +21,152 @@ module Minitest
|
|
10
21
|
@backtrace = Backtrace.new(backtrace)
|
11
22
|
end
|
12
23
|
|
24
|
+
# Prints the pathname and line number of the location most likely to be the source of the
|
25
|
+
# test failure
|
26
|
+
#
|
27
|
+
# @return [String] ex. 'path/to/file.rb:12'
|
13
28
|
def to_s
|
14
|
-
"#{
|
29
|
+
"#{most_relevant_file}:#{most_relevant_failure_line}"
|
15
30
|
end
|
16
31
|
|
17
|
-
def
|
18
|
-
|
32
|
+
def local?
|
33
|
+
broken_test? || proper_failure?
|
19
34
|
end
|
20
35
|
|
21
|
-
|
22
|
-
|
36
|
+
# Knows if the failure is contained within the test. For example, if there's bad code in a
|
37
|
+
# test, and it raises an exception, then it's really a broken test rather than a proper
|
38
|
+
# faiure.
|
39
|
+
#
|
40
|
+
# @return [Boolean] true if most relevant file is the same as the test location file
|
41
|
+
def broken_test?
|
42
|
+
!test_file.nil? && test_file == most_relevant_file
|
23
43
|
end
|
24
44
|
|
45
|
+
# Knows if the failure occurred in the actual project source code—as opposed to the test or
|
46
|
+
# an external piece of code like a gem.
|
47
|
+
#
|
48
|
+
# @return [Boolean] true if there's a non-test project file in the stacktrace but it's not
|
49
|
+
# a result of a broken test
|
50
|
+
def proper_failure?
|
51
|
+
!source_code_file.nil? && !broken_test?
|
52
|
+
end
|
53
|
+
|
54
|
+
# The file most likely to be the source of the underlying problem. Often, the most recent
|
55
|
+
# backtrace files will be a gem or external library that's failing indirectly as a result
|
56
|
+
# of a problem with local source code (not always, but frequently). In that case, the best
|
57
|
+
# first place to focus is on the code you control.
|
58
|
+
#
|
59
|
+
# @return [String] the relative path to the file from the project root
|
60
|
+
def most_relevant_file
|
61
|
+
Pathname(most_relevant_location[0])
|
62
|
+
end
|
63
|
+
|
64
|
+
# The line number of the `most_relevant_file` where the failure originated
|
65
|
+
#
|
66
|
+
# @return [Integer] line number
|
67
|
+
def most_relevant_failure_line
|
68
|
+
most_relevant_location[1]
|
69
|
+
end
|
70
|
+
|
71
|
+
# The final location of the stacktrace regardless of whether it's from within the project
|
72
|
+
#
|
73
|
+
# @return [String] the relative path to the file from the project root
|
74
|
+
def final_file
|
75
|
+
Pathname(final_location[0])
|
76
|
+
end
|
77
|
+
|
78
|
+
# The line number of the `final_file` where the failure originated
|
79
|
+
#
|
80
|
+
# @return [Integer] line number
|
81
|
+
def final_failure_line
|
82
|
+
final_location[1]
|
83
|
+
end
|
84
|
+
|
85
|
+
# The final location of the stacktrace regardless of whether it's from within the project
|
86
|
+
#
|
87
|
+
# @return [String] the relative path to the file from the project root
|
88
|
+
def project_file
|
89
|
+
broken_test? ? test_file : source_code_file
|
90
|
+
end
|
91
|
+
|
92
|
+
# The line number of the `project_file` where the failure originated
|
93
|
+
#
|
94
|
+
# @return [Integer] line number
|
95
|
+
def project_failure_line
|
96
|
+
broken_test? ? test_failure_line || test_definition_line : source_code_failure_line
|
97
|
+
end
|
98
|
+
|
99
|
+
# The final location from the stacktrace that is within the project directory
|
100
|
+
#
|
101
|
+
# @return [String, nil] the relative path to the file from the project root
|
102
|
+
def source_code_file
|
103
|
+
return nil unless backtrace.source_code_lines.any?
|
104
|
+
|
105
|
+
backtrace.final_source_code_location.pathname
|
106
|
+
end
|
107
|
+
|
108
|
+
# The line number of the `source_code_file` where the failure originated
|
109
|
+
#
|
110
|
+
# @return [Integer] line number
|
111
|
+
def source_code_failure_line
|
112
|
+
return nil unless backtrace.source_code_lines.any?
|
113
|
+
|
114
|
+
backtrace.final_source_code_location.number
|
115
|
+
end
|
116
|
+
|
117
|
+
# The final location from the stacktrace that is within the project's test directory
|
118
|
+
#
|
119
|
+
# @return [String, nil] the relative path to the file from the project root
|
25
120
|
def test_file
|
26
|
-
|
121
|
+
Pathname(test_location[0])
|
27
122
|
end
|
28
123
|
|
124
|
+
# The line number of the `test_file` where the test is defined
|
125
|
+
#
|
126
|
+
# @return [Integer] line number
|
29
127
|
def test_definition_line
|
30
128
|
test_location[1].to_s
|
31
129
|
end
|
32
130
|
|
131
|
+
# The line number from within the `test_file` test definition where the failure occurred
|
132
|
+
#
|
133
|
+
# @return [Integer] line number
|
33
134
|
def test_failure_line
|
34
|
-
|
135
|
+
backtrace.final_test_location&.number || test_definition_line
|
35
136
|
end
|
36
137
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
138
|
+
# The line number from within the `test_file` test definition where the failure occurred
|
139
|
+
#
|
140
|
+
# @return [Location] the last location from the backtrace or the test location if a backtrace
|
141
|
+
# was not passed to the initializer
|
142
|
+
def final_location
|
143
|
+
backtrace? ? backtrace.final_location : test_location
|
43
144
|
end
|
44
145
|
|
45
|
-
|
46
|
-
|
146
|
+
# The file most likely to be the source of the underlying problem. Often, the most recent
|
147
|
+
# backtrace files will be a gem or external library that's failing indirectly as a result
|
148
|
+
# of a problem with local source code (not always, but frequently). In that case, the best
|
149
|
+
# first place to focus is on the code you control.
|
150
|
+
#
|
151
|
+
# @return [Array] file and line number of the most likely source of the problem
|
152
|
+
def most_relevant_location
|
153
|
+
[
|
154
|
+
source_code_location,
|
155
|
+
test_location,
|
156
|
+
final_location
|
157
|
+
].compact.first
|
158
|
+
end
|
47
159
|
|
48
|
-
|
160
|
+
def project_location
|
161
|
+
source_code_location || test_location
|
49
162
|
end
|
50
163
|
|
51
|
-
|
164
|
+
def source_code_location
|
165
|
+
backtrace.final_source_code_location
|
166
|
+
end
|
52
167
|
|
53
|
-
def
|
54
|
-
|
168
|
+
def backtrace?
|
169
|
+
backtrace.parsed_lines.any?
|
55
170
|
end
|
56
171
|
end
|
57
172
|
end
|
data/lib/minitest/heat/map.rb
CHANGED
@@ -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,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,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
|
data/lib/minitest/heat/output.rb
CHANGED
@@ -1,102 +1,41 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'output/backtrace'
|
4
|
+
require_relative 'output/issue'
|
5
|
+
require_relative 'output/location'
|
6
|
+
require_relative 'output/map'
|
7
|
+
require_relative 'output/results'
|
8
|
+
require_relative 'output/source_code'
|
9
|
+
require_relative 'output/token'
|
10
|
+
|
3
11
|
module Minitest
|
4
12
|
module Heat
|
5
13
|
# Friendly API for printing nicely-formatted output to the console
|
6
14
|
class Output
|
7
|
-
ESC = "\e["
|
8
|
-
|
9
|
-
Token = Struct.new(:style, :content) do
|
10
|
-
def to_s
|
11
|
-
[
|
12
|
-
style_string,
|
13
|
-
content,
|
14
|
-
reset_string
|
15
|
-
].join
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
|
20
|
-
def style_string
|
21
|
-
"#{ESC}#{weight};#{color}m"
|
22
|
-
end
|
23
|
-
|
24
|
-
def reset_string
|
25
|
-
"#{ESC}0m"
|
26
|
-
end
|
27
|
-
|
28
|
-
def weight_key
|
29
|
-
style_components[0]
|
30
|
-
end
|
31
|
-
|
32
|
-
def color_key
|
33
|
-
style_components[1]
|
34
|
-
end
|
35
|
-
|
36
|
-
def weight
|
37
|
-
{
|
38
|
-
default: 0,
|
39
|
-
bold: 1,
|
40
|
-
light: 2,
|
41
|
-
italic: 3
|
42
|
-
}.fetch(weight_key)
|
43
|
-
end
|
44
|
-
|
45
|
-
def color
|
46
|
-
{
|
47
|
-
black: 30,
|
48
|
-
red: 31,
|
49
|
-
green: 32,
|
50
|
-
yellow: 33,
|
51
|
-
blue: 34,
|
52
|
-
magenta: 35,
|
53
|
-
cyan: 36,
|
54
|
-
gray: 37,
|
55
|
-
default: 39
|
56
|
-
}.fetch(color_key)
|
57
|
-
end
|
58
|
-
|
59
|
-
def style_components
|
60
|
-
{
|
61
|
-
success: %i[default green],
|
62
|
-
slow: %i[bold green],
|
63
|
-
error: %i[bold red],
|
64
|
-
broken: %i[bold red],
|
65
|
-
failure: %i[default red],
|
66
|
-
skipped: %i[default yellow],
|
67
|
-
warning_light: %i[light yellow],
|
68
|
-
source: %i[italic default],
|
69
|
-
bold: %i[bold default],
|
70
|
-
default: %i[default default],
|
71
|
-
muted: %i[light gray]
|
72
|
-
}.fetch(style)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
15
|
FORMATTERS = {
|
77
16
|
error: [
|
78
|
-
[ %i[error label], %i[muted
|
79
|
-
[ %i[
|
17
|
+
[ %i[error label], %i[muted spacer], %i[default test_name] ],
|
18
|
+
[ %i[italicized summary], ],
|
80
19
|
[ %i[default backtrace_summary] ],
|
81
20
|
],
|
82
21
|
broken: [
|
83
|
-
[ %i[broken label], %i[muted spacer], %i[default
|
84
|
-
[ %i[
|
22
|
+
[ %i[broken label], %i[muted spacer], %i[default test_name], %i[muted spacer], %i[muted test_class] ],
|
23
|
+
[ %i[italicized summary], ],
|
85
24
|
[ %i[default backtrace_summary] ],
|
86
25
|
],
|
87
26
|
failure: [
|
88
|
-
[ %i[failure label], %i[muted spacer], %i[default
|
89
|
-
[ %i[
|
90
|
-
[ %i[muted
|
27
|
+
[ %i[failure label], %i[muted spacer], %i[default test_name], %i[muted spacer], %i[muted test_class] ],
|
28
|
+
[ %i[italicized summary] ],
|
29
|
+
[ %i[muted short_location], ],
|
91
30
|
[ %i[default source_summary], ],
|
92
31
|
],
|
93
32
|
skipped: [
|
94
|
-
[ %i[skipped label], %i[muted spacer], %i[default
|
95
|
-
[ %i[
|
33
|
+
[ %i[skipped label], %i[muted spacer], %i[default test_name], %i[muted spacer], %i[muted test_class] ],
|
34
|
+
[ %i[italicized summary] ],
|
96
35
|
[], # New Line
|
97
36
|
],
|
98
37
|
slow: [
|
99
|
-
[ %i[slow label], %i[muted spacer], %i[default
|
38
|
+
[ %i[slow label], %i[muted spacer], %i[default test_name], %i[muted spacer], %i[default test_class] ],
|
100
39
|
[ %i[bold slowness], %i[muted spacer], %i[default location], ],
|
101
40
|
[], # New Line
|
102
41
|
]
|
@@ -120,6 +59,9 @@ module Minitest
|
|
120
59
|
end
|
121
60
|
alias newline puts
|
122
61
|
|
62
|
+
# TOOD: Convert to output class
|
63
|
+
# - This should likely live in the output/issue class
|
64
|
+
# - Add a 'fail_fast' option that shows the issue as soon as the failure occurs
|
123
65
|
def marker(value)
|
124
66
|
case value
|
125
67
|
when 'E' then text(:error, value)
|
@@ -130,6 +72,9 @@ module Minitest
|
|
130
72
|
end
|
131
73
|
end
|
132
74
|
|
75
|
+
# TOOD: Convert to output class
|
76
|
+
# - This should likely live in the output/issue class
|
77
|
+
# - There may be justification for creating different "strategies" for the various types
|
133
78
|
def issue_details(issue)
|
134
79
|
formatter = FORMATTERS[issue.type]
|
135
80
|
|
@@ -150,23 +95,31 @@ module Minitest
|
|
150
95
|
end
|
151
96
|
end
|
152
97
|
|
98
|
+
# TOOD: Convert to output class
|
153
99
|
def heat_map(map)
|
154
|
-
# text(:default, "🔥 Hot Spots 🔥\n")
|
155
100
|
map.files.each do |file|
|
156
|
-
|
157
|
-
|
101
|
+
pathname = Pathname(file[0])
|
102
|
+
|
103
|
+
path = pathname.dirname.to_s
|
104
|
+
filename = pathname.basename.to_s
|
105
|
+
|
106
|
+
values = map.hits[pathname.to_s]
|
158
107
|
|
159
|
-
filename = file.split('/').last
|
160
|
-
path = file.delete_suffix(filename)
|
161
108
|
|
162
|
-
text(:error, 'E' * values[:error].size)
|
163
|
-
text(:broken, 'B' * values[:broken].size)
|
109
|
+
text(:error, 'E' * values[:error].size) if values[:error]&.any?
|
110
|
+
text(:broken, 'B' * values[:broken].size) if values[:broken]&.any?
|
164
111
|
text(:failure, 'F' * values[:failure].size) if values[:failure]&.any?
|
165
|
-
text(:skipped, 'S' * values[:skipped].size) if values[:skipped]&.any?
|
166
|
-
text(:muted, ' ') if values[:error]&.any? || values[:broken]&.any? || values[:failure]&.any? || values[:skipped]&.any?
|
167
112
|
|
168
|
-
|
169
|
-
|
113
|
+
unless values[:error]&.any? || values[:broken]&.any? || values[:failure]&.any?
|
114
|
+
text(:skipped, 'S' * values[:skipped].size) if values[:skipped]&.any?
|
115
|
+
text(:painful, '—' * values[:painful].size) if values[:painful]&.any?
|
116
|
+
text(:slow, '–' * values[:slow].size) if values[:slow]&.any?
|
117
|
+
end
|
118
|
+
|
119
|
+
text(:muted, ' ') if map.hits.any?
|
120
|
+
|
121
|
+
text(:muted, "#{path.delete_prefix(Dir.pwd)}/")
|
122
|
+
text(:default, filename)
|
170
123
|
|
171
124
|
text(:muted, ':')
|
172
125
|
|
@@ -180,96 +133,56 @@ module Minitest
|
|
180
133
|
newline
|
181
134
|
end
|
182
135
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
slow_count = results.slows.size
|
188
|
-
skip_count = results.skips.size
|
189
|
-
|
190
|
-
counts = []
|
191
|
-
counts << pluralize(error_count, 'Error') if error_count.positive?
|
192
|
-
counts << pluralize(broken_count, 'Broken') if broken_count.positive?
|
193
|
-
counts << pluralize(failure_count, 'Failure') if failure_count.positive?
|
194
|
-
counts << pluralize(skip_count, 'Skip') if skip_count.positive?
|
195
|
-
counts << pluralize(slow_count, 'Slow') if slow_count.positive?
|
196
|
-
text(:default, counts.join(', '))
|
197
|
-
|
198
|
-
newline
|
199
|
-
text(:muted, "#{results.tests_per_second} tests/s and #{results.assertions_per_second} assertions/s ")
|
136
|
+
# TOOD: Convert to output class
|
137
|
+
def test_name_summary(issue)
|
138
|
+
text(:default, "#{issue.test_class} > #{issue.test_name}")
|
139
|
+
end
|
200
140
|
|
201
|
-
|
202
|
-
|
203
|
-
text(:muted, pluralize(results.assertion_count, 'Assertion'))
|
204
|
-
text(:muted, " in #{results.total_time.round(2)}s")
|
141
|
+
def compact_summary(results)
|
142
|
+
results_tokens = ::Minitest::Heat::Output::Results.new(results).tokens
|
205
143
|
|
206
144
|
newline
|
145
|
+
print_tokens(results_tokens)
|
207
146
|
newline
|
208
147
|
end
|
209
148
|
|
210
|
-
private
|
211
|
-
|
212
|
-
def test_name_summary(issue)
|
213
|
-
text(:default, "#{issue.test_class} > #{issue.test_name}")
|
214
|
-
end
|
215
|
-
|
216
149
|
def backtrace_summary(issue)
|
217
|
-
|
218
|
-
|
219
|
-
backtrace_line = backtrace_lines.first
|
220
|
-
filename = "#{backtrace_line.path.delete_prefix(Dir.pwd)}/#{backtrace_line.file}"
|
221
|
-
|
222
|
-
backtrace_lines.take(3).each do |line|
|
223
|
-
source = Minitest::Heat::Source.new(filename, line_number: line.number, max_line_count: 1)
|
150
|
+
location = issue.location
|
224
151
|
|
225
|
-
|
226
|
-
|
227
|
-
text(:source, " `#{source.line.strip}`")
|
228
|
-
|
229
|
-
newline
|
230
|
-
end
|
152
|
+
backtrace_tokens = ::Minitest::Heat::Output::Backtrace.new(location).tokens
|
153
|
+
print_tokens(backtrace_tokens)
|
231
154
|
end
|
232
155
|
|
233
156
|
def source_summary(issue)
|
234
|
-
filename
|
235
|
-
line_number = issue.location.
|
157
|
+
filename = issue.location.project_file
|
158
|
+
line_number = issue.location.project_failure_line
|
236
159
|
|
237
|
-
|
238
|
-
|
160
|
+
source_code_tokens = ::Minitest::Heat::Output::SourceCode.new(filename, line_number).tokens
|
161
|
+
print_tokens(source_code_tokens)
|
239
162
|
end
|
240
163
|
|
241
|
-
|
242
|
-
max_line_number_length = source.line_numbers.map(&:to_s).map(&:length).max
|
243
|
-
source.lines.each_index do |i|
|
244
|
-
line_number = source.line_numbers[i]
|
245
|
-
line = source.lines[i]
|
246
|
-
|
247
|
-
number_style, line_style = if line == source.line && highlight_line
|
248
|
-
[:default, :default]
|
249
|
-
else
|
250
|
-
[:muted, :muted]
|
251
|
-
end
|
252
|
-
text(number_style, "#{' ' * indentation}#{line_number.to_s.rjust(max_line_number_length)} ")
|
253
|
-
text(line_style, line)
|
254
|
-
puts
|
255
|
-
end
|
256
|
-
end
|
164
|
+
private
|
257
165
|
|
258
166
|
def style_enabled?
|
259
167
|
stream.tty?
|
260
168
|
end
|
261
169
|
|
262
|
-
def
|
263
|
-
|
264
|
-
|
265
|
-
# Given the narrow scope, pluralization can be relatively naive here
|
266
|
-
count > 1 ? "#{singular_style}s" : singular_style
|
170
|
+
def text(style, content)
|
171
|
+
token = Token.new(style, content)
|
172
|
+
print token.to_s(token_format)
|
267
173
|
end
|
268
174
|
|
269
|
-
def
|
270
|
-
|
175
|
+
def token_format
|
176
|
+
style_enabled? ? :styled : :unstyled
|
177
|
+
end
|
271
178
|
|
272
|
-
|
179
|
+
def print_tokens(lines_of_tokens)
|
180
|
+
lines_of_tokens.each do |tokens|
|
181
|
+
tokens.each do |token|
|
182
|
+
print Token.new(*token).to_s(token_format)
|
183
|
+
end
|
184
|
+
newline
|
185
|
+
end
|
273
186
|
end
|
274
187
|
end
|
275
188
|
end
|
data/lib/minitest/heat/source.rb
CHANGED
@@ -51,10 +51,15 @@ module Minitest
|
|
51
51
|
#
|
52
52
|
# @return [type] [description]
|
53
53
|
def file_lines
|
54
|
-
@raw_lines ||= File.readlines(
|
54
|
+
@raw_lines ||= File.readlines(filename, chomp: true)
|
55
55
|
@raw_lines.pop while @raw_lines.last.strip.empty?
|
56
56
|
|
57
57
|
@raw_lines
|
58
|
+
rescue Errno::ENOENT
|
59
|
+
# Occasionally, for a variety of reasons, a file can't be read. In those cases, it's best to
|
60
|
+
# return no source code lines rather than have the test suite raise an error unrelated to
|
61
|
+
# the code being tested becaues that gets confusing.
|
62
|
+
[]
|
58
63
|
end
|
59
64
|
|
60
65
|
private
|
@@ -62,7 +62,6 @@ module Minitest
|
|
62
62
|
issue = Heat::Issue.new(result)
|
63
63
|
|
64
64
|
@results.record(issue)
|
65
|
-
|
66
65
|
@map.add(*issue.to_hit) if issue.hit?
|
67
66
|
|
68
67
|
output.marker(issue.marker)
|
@@ -92,7 +91,6 @@ module Minitest
|
|
92
91
|
results.errors.each { |issue| output.issue_details(issue) }
|
93
92
|
|
94
93
|
output.compact_summary(results)
|
95
|
-
|
96
94
|
output.heat_map(map)
|
97
95
|
end
|
98
96
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: minitest-heat
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Garrett Dimon
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-09-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|
@@ -90,6 +90,13 @@ files:
|
|
90
90
|
- lib/minitest/heat/location.rb
|
91
91
|
- lib/minitest/heat/map.rb
|
92
92
|
- lib/minitest/heat/output.rb
|
93
|
+
- lib/minitest/heat/output/backtrace.rb
|
94
|
+
- lib/minitest/heat/output/issue.rb
|
95
|
+
- lib/minitest/heat/output/location.rb
|
96
|
+
- lib/minitest/heat/output/map.rb
|
97
|
+
- lib/minitest/heat/output/results.rb
|
98
|
+
- lib/minitest/heat/output/source_code.rb
|
99
|
+
- lib/minitest/heat/output/token.rb
|
93
100
|
- lib/minitest/heat/results.rb
|
94
101
|
- lib/minitest/heat/source.rb
|
95
102
|
- lib/minitest/heat/version.rb
|