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.
@@ -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