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.
@@ -1,94 +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
- Token = Struct.new(:style, :content) do
8
- STYLES = {
9
- error: %i[bold red],
10
- broken: %i[bold red],
11
- failure: %i[default red],
12
- skipped: %i[bold yellow],
13
- success: %i[default green],
14
- slow: %i[bold green],
15
- source: %i[italic default],
16
- bold: %i[bold default],
17
- default: %i[default default],
18
- subtle: %i[light white],
19
- muted: %i[light gray],
20
- }.freeze
21
-
22
- WEIGHTS = {
23
- default: 0,
24
- bold: 1,
25
- light: 2,
26
- italic: 3,
27
- underline: 4,
28
- frame: 51,
29
- encircle: 52,
30
- overline: 53,
31
- }.freeze
32
-
33
- COLORS = {
34
- black: 30,
35
- red: 31,
36
- green: 32,
37
- yellow: 33,
38
- blue: 34,
39
- magenta: 35,
40
- cyan: 36,
41
- gray: 37, white: 97,
42
- default: 39,
43
- }.freeze
44
-
45
- def to_s
46
- "\e[#{weight};#{color}m#{content}#{reset}"
47
- end
48
-
49
- private
50
-
51
- def weight
52
- WEIGHTS.fetch(style_components[0])
53
- end
54
-
55
- def color
56
- COLORS.fetch(style_components[1])
57
- end
58
-
59
- def reset
60
- "\e[0m"
61
- end
62
-
63
- def style_components
64
- STYLES[style]
65
- end
66
- end
67
-
68
15
  FORMATTERS = {
69
16
  error: [
70
- [ %i[error label], %i[muted spacer], %i[error class], %i[muted arrow], %i[error test_name] ],
71
- [ %i[default summary], ],
17
+ [ %i[error label], %i[muted spacer], %i[default test_name] ],
18
+ [ %i[italicized summary], ],
72
19
  [ %i[default backtrace_summary] ],
73
20
  ],
74
21
  broken: [
75
- [ %i[broken label], %i[muted spacer], %i[broken test_class], %i[muted arrow], %i[broken test_name] ],
76
- [ %i[default summary], ],
22
+ [ %i[broken label], %i[muted spacer], %i[default test_name], %i[muted spacer], %i[muted test_class] ],
23
+ [ %i[italicized summary], ],
77
24
  [ %i[default backtrace_summary] ],
78
25
  ],
79
26
  failure: [
80
- [ %i[failure label], %i[muted spacer], %i[failure test_class], %i[muted arrow], %i[failure test_name], %i[muted spacer], %i[muted class] ],
81
- [ %i[default summary] ],
82
- [ %i[subtle location], ],
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], ],
83
30
  [ %i[default source_summary], ],
84
31
  ],
85
32
  skipped: [
86
- [ %i[skipped label], %i[muted spacer], %i[skipped test_class], %i[muted arrow], %i[skipped test_name] ],
87
- [ %i[default summary], %i[muted spacer], %i[default class] ],
33
+ [ %i[skipped label], %i[muted spacer], %i[default test_name], %i[muted spacer], %i[muted test_class] ],
34
+ [ %i[italicized summary] ],
88
35
  [], # New Line
89
36
  ],
90
37
  slow: [
91
- [ %i[slow label], %i[muted spacer], %i[default test_class], %i[muted arrow], %i[default test_name], %i[muted spacer], %i[muted class], ],
38
+ [ %i[slow label], %i[muted spacer], %i[default test_name], %i[muted spacer], %i[default test_class] ],
92
39
  [ %i[bold slowness], %i[muted spacer], %i[default location], ],
93
40
  [], # New Line
94
41
  ]
@@ -112,17 +59,22 @@ module Minitest
112
59
  end
113
60
  alias newline puts
114
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
115
65
  def marker(value)
116
66
  case value
117
67
  when 'E' then text(:error, value)
118
68
  when 'B' then text(:failure, value)
119
69
  when 'F' then text(:failure, value)
120
70
  when 'S' then text(:skipped, value)
121
- when 'T' then text(:slow, value)
122
71
  else text(:success, value)
123
72
  end
124
73
  end
125
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
126
78
  def issue_details(issue)
127
79
  formatter = FORMATTERS[issue.type]
128
80
 
@@ -143,25 +95,31 @@ module Minitest
143
95
  end
144
96
  end
145
97
 
98
+ # TOOD: Convert to output class
146
99
  def heat_map(map)
147
- # text(:default, "🔥 Hot Spots 🔥\n")
148
100
  map.files.each do |file|
149
- file = file[0]
150
- values = map.hits[file]
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]
151
107
 
152
- filename = file.split('/').last
153
- path = file.delete_suffix(filename)
154
108
 
155
- text(:error, 'E' * values[:error].size) if values[:error]&.any?
156
- text(:broken, 'B' * values[:broken].size) if values[:broken]&.any?
109
+ text(:error, 'E' * values[:error].size) if values[:error]&.any?
110
+ text(:broken, 'B' * values[:broken].size) if values[:broken]&.any?
157
111
  text(:failure, 'F' * values[:failure].size) if values[:failure]&.any?
158
- text(:skipped, 'S' * values[:skipped].size) if values[:skipped]&.any?
159
- text(:slow, 'S' * values[:skipped].size) if values[:skipped]&.any?
160
112
 
161
- text(:muted, ' ')
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?
162
120
 
163
- text(:muted, "#{path.delete_prefix('/')}")
164
- text(:default, "#{filename}")
121
+ text(:muted, "#{path.delete_prefix(Dir.pwd)}/")
122
+ text(:default, filename)
165
123
 
166
124
  text(:muted, ':')
167
125
 
@@ -169,102 +127,62 @@ module Minitest
169
127
  all_line_numbers += values.fetch(:skipped, [])
170
128
 
171
129
  line_numbers = all_line_numbers.compact.uniq.sort
172
- line_numbers.each { |line_number| text(:subtle, "#{line_number} ") }
130
+ line_numbers.each { |line_number| text(:muted, "#{line_number} ") }
173
131
  newline
174
132
  end
175
133
  newline
176
134
  end
177
135
 
178
- def compact_summary(results)
179
- error_count = results.errors.size
180
- broken_count = results.brokens.size
181
- failure_count = results.failures.size
182
- slow_count = results.slows.size
183
- skip_count = results.skips.size
184
-
185
- counts = []
186
- counts << pluralize(error_count, 'Error') if error_count.positive?
187
- counts << pluralize(broken_count, 'Broken') if broken_count.positive?
188
- counts << pluralize(failure_count, 'Failure') if failure_count.positive?
189
- counts << pluralize(skip_count, 'Skip') if skip_count.positive?
190
- counts << pluralize(slow_count, 'Slow') if slow_count.positive?
191
- text(:default, counts.join(', '))
192
-
193
- newline
194
- text(:subtle, "#{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
195
140
 
196
- newline
197
- text(:muted, pluralize(results.test_count, 'Test') + ' & ')
198
- text(:muted, pluralize(results.assertion_count, 'Assertion'))
199
- text(:muted, " in #{results.total_time.round(2)}s")
141
+ def compact_summary(results)
142
+ results_tokens = ::Minitest::Heat::Output::Results.new(results).tokens
200
143
 
201
144
  newline
145
+ print_tokens(results_tokens)
202
146
  newline
203
147
  end
204
148
 
205
- private
206
-
207
- def test_name_summary(issue)
208
- text(:default, "#{issue.test_class} > #{issue.test_name}")
209
- end
210
-
211
149
  def backtrace_summary(issue)
212
- lines = issue.backtrace.project
213
-
214
- line = lines.first
215
- filename = "#{line.path.delete_prefix(Dir.pwd)}/#{line.file}"
150
+ location = issue.location
216
151
 
217
- lines.take(3).each do |line|
218
- source = Minitest::Heat::Source.new(filename, line_number: line.number, max_line_count: 1)
219
-
220
- text(:muted, " #{line.path.delete_prefix("#{Dir.pwd}/")}/")
221
- text(:subtle, "#{line.file}:#{line.number}")
222
- text(:source, " `#{source.line.strip}`")
223
-
224
- newline
225
- end
152
+ backtrace_tokens = ::Minitest::Heat::Output::Backtrace.new(location).tokens
153
+ print_tokens(backtrace_tokens)
226
154
  end
227
155
 
228
156
  def source_summary(issue)
229
- filename = issue.location.source_file
230
- line_number = issue.location.source_failure_line
157
+ filename = issue.location.project_file
158
+ line_number = issue.location.project_failure_line
231
159
 
232
- source = Minitest::Heat::Source.new(filename, line_number: line_number, max_line_count: 3)
233
- show_source(source, highlight_line: true, indentation: 2)
160
+ source_code_tokens = ::Minitest::Heat::Output::SourceCode.new(filename, line_number).tokens
161
+ print_tokens(source_code_tokens)
234
162
  end
235
163
 
236
- def show_source(source, indentation: 0, highlight_line: false)
237
- max_line_number_length = source.line_numbers.map(&:to_s).map(&:length).max
238
- source.lines.each_index do |i|
239
- line_number = source.line_numbers[i]
240
- line = source.lines[i]
241
-
242
- number_style, line_style = if line == source.line && highlight_line
243
- [:default, :default]
244
- else
245
- [:subtle, :subtle]
246
- end
247
- text(number_style, "#{' ' * indentation}#{line_number.to_s.rjust(max_line_number_length)} ")
248
- text(line_style, line)
249
- puts
250
- end
251
- end
164
+ private
252
165
 
253
166
  def style_enabled?
254
167
  stream.tty?
255
168
  end
256
169
 
257
- def pluralize(count, singular)
258
- singular_style = "#{count} #{singular}"
259
-
260
- # Given the narrow scope, pluralization can be relatively naive here
261
- 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)
262
173
  end
263
174
 
264
- def text(style, content)
265
- formatted_content = style_enabled? ? Token.new(style, content).to_s : content
175
+ def token_format
176
+ style_enabled? ? :styled : :unstyled
177
+ end
266
178
 
267
- print formatted_content
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
268
186
  end
269
187
  end
270
188
  end
@@ -49,8 +49,8 @@ module Minitest
49
49
  (assertion_count / total_time).round(2)
50
50
  end
51
51
 
52
- def issues?
53
- errors? || failures? || skips?
52
+ def problems?
53
+ errors? || brokens? || failures? || skips?
54
54
  end
55
55
 
56
56
  def errors
@@ -93,19 +93,17 @@ module Minitest
93
93
  skips.any?
94
94
  end
95
95
 
96
- def count(result)
97
- @test_count += 1
98
- @assertion_count += result.assertions
99
- @success_count += 1 if result.passed?
96
+ def slows?
97
+ slows.any?
100
98
  end
101
99
 
102
- def record_issue(result)
103
- issue = Heat::Issue.new(result)
100
+ def record(issue)
101
+ @test_count += 1
102
+ @assertion_count += issue.result.assertions
103
+ @success_count += 1 if issue.result.passed?
104
104
 
105
105
  @issues[issue.type] ||= []
106
106
  @issues[issue.type] << issue
107
-
108
- issue
109
107
  end
110
108
  end
111
109
  end
@@ -51,10 +51,15 @@ module Minitest
51
51
  #
52
52
  # @return [type] [description]
53
53
  def file_lines
54
- @raw_lines ||= File.readlines("#{Dir.pwd}#{filename}", chomp: true)
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
@@ -1,5 +1,5 @@
1
1
  module Minitest
2
2
  module Heat
3
- VERSION = "0.0.1"
3
+ VERSION = "0.0.5"
4
4
  end
5
5
  end
@@ -59,14 +59,12 @@ module Minitest
59
59
  # Minitest::Result source:
60
60
  # https://github.com/seattlerb/minitest/blob/f4f57afaeb3a11bd0b86ab0757704cb78db96cf4/lib/minitest.rb#L504
61
61
  def record(result)
62
- @results.count(result)
63
- if !result.passed? || result.time > ::Minitest::Heat::Issue::SLOW_THRESHOLD
64
- issue = @results.record_issue(result)
65
- @map.add(*issue.to_hit)
66
- output.marker(issue.marker)
67
- else
68
- output.marker(result.result_code)
69
- end
62
+ issue = Heat::Issue.new(result)
63
+
64
+ @results.record(issue)
65
+ @map.add(*issue.to_hit) if issue.hit?
66
+
67
+ output.marker(issue.marker)
70
68
  end
71
69
 
72
70
  # Outputs the summary of the run.
@@ -80,14 +78,19 @@ module Minitest
80
78
  # pressing issues are displayed at the bottom of the report in order to reduce scrolling.
81
79
  # This way, as you fix issues, the list gets shorter, and eventually the least critical
82
80
  # issues will be displayed without scrolling once more problematic issues are resolved.
83
- results.slows.each { |issue| output.issue_details(issue) }
84
- results.skips.each { |issue| output.issue_details(issue) }
81
+ if results.failures.empty? && results.brokens.empty? && results.errors.empty? && results.skips.empty?
82
+ results.slows.each { |issue| output.issue_details(issue) }
83
+ end
84
+
85
+ if results.failures.empty? && results.brokens.empty? && results.errors.empty?
86
+ results.skips.each { |issue| output.issue_details(issue) }
87
+ end
88
+
85
89
  results.failures.each { |issue| output.issue_details(issue) }
86
90
  results.brokens.each { |issue| output.issue_details(issue) }
87
91
  results.errors.each { |issue| output.issue_details(issue) }
88
92
 
89
93
  output.compact_summary(results)
90
-
91
94
  output.heat_map(map)
92
95
  end
93
96
 
@@ -34,6 +34,7 @@ Gem::Specification.new do |spec|
34
34
 
35
35
  spec.add_runtime_dependency 'minitest'
36
36
 
37
+ spec.add_development_dependency 'dead_end'
37
38
  spec.add_development_dependency 'pry'
38
39
  spec.add_development_dependency 'simplecov'
39
40
  end
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.1
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-08-26 00:00:00.000000000 Z
11
+ date: 2021-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dead_end
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: pry
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -76,6 +90,13 @@ files:
76
90
  - lib/minitest/heat/location.rb
77
91
  - lib/minitest/heat/map.rb
78
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
79
100
  - lib/minitest/heat/results.rb
80
101
  - lib/minitest/heat/source.rb
81
102
  - lib/minitest/heat/version.rb