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.
@@ -2,110 +2,50 @@
2
2
 
3
3
  module Minitest
4
4
  module Heat
5
+ # A collection of test failures
5
6
  class Results
6
-
7
- attr_reader :test_count,
8
- :assertion_count,
9
- :success_count,
10
- :issues,
11
- :start_time,
12
- :stop_time
7
+ attr_reader :issues
13
8
 
14
9
  def initialize
15
- @test_count = 0
16
- @assertion_count = 0
17
- @success_count = 0
18
- @issues = {
19
- error: [],
20
- broken: [],
21
- failure: [],
22
- skipped: [],
23
- slow: []
24
- }
25
- @start_time = nil
26
- @stop_time = nil
27
- end
28
-
29
- def start_timer!
30
- @start_time = Minitest.clock_time
10
+ @issues = []
31
11
  end
32
12
 
33
- def stop_timer!
34
- @stop_time = Minitest.clock_time
35
- end
36
-
37
- def total_time
38
- delta = @stop_time - @start_time
39
-
40
- # Don't return 0
41
- delta.zero? ? 0.1 : delta
13
+ def record(issue)
14
+ @issues << issue
42
15
  end
43
16
 
44
- def tests_per_second
45
- (assertion_count / total_time).round(2)
46
- end
47
-
48
- def assertions_per_second
49
- (assertion_count / total_time).round(2)
50
- end
51
-
52
- def issues?
53
- errors? || failures? || skips?
17
+ def problems?
18
+ errors.any? || brokens.any? || failures.any?
54
19
  end
55
20
 
56
21
  def errors
57
- issues.fetch(:error) { [] }
22
+ @errors ||= select_issues(:error)
58
23
  end
59
24
 
60
25
  def brokens
61
- issues.fetch(:broken) { [] }
26
+ @brokens ||= select_issues(:broken)
62
27
  end
63
28
 
64
29
  def failures
65
- issues.fetch(:failure) { [] }
30
+ @failures ||= select_issues(:failure)
66
31
  end
67
32
 
68
33
  def skips
69
- issues.fetch(:skipped) { [] }
34
+ @skips ||= select_issues(:skipped)
70
35
  end
71
36
 
72
- def slows
73
- issues
74
- .fetch(:slow) { [] }
75
- .sort { |issue| issue.time }
76
- .reverse
77
- .take(3)
37
+ def painfuls
38
+ @painfuls ||= select_issues(:painful).sort_by(&:time).reverse
78
39
  end
79
40
 
80
- def errors?
81
- errors.any?
82
- end
83
-
84
- def brokens?
85
- brokens.any?
86
- end
87
-
88
- def failures?
89
- failures.any?
90
- end
91
-
92
- def skips?
93
- skips.any?
94
- end
95
-
96
- def count(result)
97
- @test_count += 1
98
- @assertion_count += result.assertions
99
- @success_count += 1 if result.passed?
41
+ def slows
42
+ @slows ||= select_issues(:slow).sort_by(&:time).reverse
100
43
  end
101
44
 
102
- def record_issue(result)
103
- issue = Heat::Issue.new(result)
104
-
105
- @issues[issue.type] ||= []
106
- @issues[issue.type] << issue
45
+ private
107
46
 
108
- issue
47
+ def select_issues(issue_type)
48
+ issues.select { |issue| issue.type == issue_type }
109
49
  end
110
50
  end
111
51
  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
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ module Heat
5
+ # Provides a timer to keep track of the full test suite duration and provide convenient methods
6
+ # for calculating tests/second and assertions/second
7
+ class Timer
8
+ attr_reader :test_count, :assertion_count, :start_time, :stop_time
9
+
10
+ # Creates an instance of a timer to be used for the duration of a test suite run
11
+ #
12
+ # @return [self]
13
+ def initialize
14
+ @test_count = 0
15
+ @assertion_count = 0
16
+
17
+ @start_time = nil
18
+ @stop_time = nil
19
+ end
20
+
21
+ # Records the start time for the full test suite using `Minitest.clock_time`
22
+ #
23
+ # @return [Float] the Minitest.clock_time
24
+ def start!
25
+ @start_time = Minitest.clock_time
26
+ end
27
+
28
+ # Records the stop time for the full test suite using `Minitest.clock_time`
29
+ #
30
+ # @return [Float] the Minitest.clock_time
31
+ def stop!
32
+ @stop_time = Minitest.clock_time
33
+ end
34
+
35
+ # Calculates the total time take for the full test suite to run while ensuring it never
36
+ # returns a zero that would be problematic as a denomitor in calculating average times
37
+ #
38
+ # @return [Float] the clocktime duration of the test suite run in seconds
39
+ def total_time
40
+ # Don't return 0. The time can end up being 0 for a new or realy fast test suite, and
41
+ # dividing by 0 doesn't go well when determining average time, so this ensures it uses a
42
+ # close-enough-but-not-zero value.
43
+ delta.zero? ? 0.01 : delta
44
+ end
45
+
46
+ # Records the test and assertion counts for a given test outcome
47
+ # @param count [Integer] the number of assertions from the test
48
+ #
49
+ # @return [void]
50
+ def increment_counts(count)
51
+ @test_count += 1
52
+ @assertion_count += count
53
+ end
54
+
55
+ # Provides a nice rounded answer for about how many tests were completed per second
56
+ #
57
+ # @return [Float] the average number of tests completed per second
58
+ def tests_per_second
59
+ (test_count / total_time).round(2)
60
+ end
61
+
62
+ # Provides a nice rounded answer for about how many assertions were completed per second
63
+ #
64
+ # @return [Float] the average number of assertions completed per second
65
+ def assertions_per_second
66
+ (assertion_count / total_time).round(2)
67
+ end
68
+
69
+ private
70
+
71
+ # The total time the test suite was running.
72
+ #
73
+ # @return [Float] the time in seconds elapsed between starting the timer and stopping it
74
+ def delta
75
+ return 0 unless start_time && stop_time
76
+
77
+ stop_time - start_time
78
+ end
79
+ end
80
+ end
81
+ end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Minitest
2
4
  module Heat
3
- VERSION = "0.0.3"
5
+ VERSION = '0.0.7'
4
6
  end
5
7
  end
data/lib/minitest/heat.rb CHANGED
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'heat/backtrace'
4
+ require_relative 'heat/hit'
4
5
  require_relative 'heat/issue'
6
+ require_relative 'heat/line'
5
7
  require_relative 'heat/location'
6
8
  require_relative 'heat/map'
7
9
  require_relative 'heat/output'
8
10
  require_relative 'heat/results'
9
11
  require_relative 'heat/source'
12
+ require_relative 'heat/timer'
10
13
  require_relative 'heat/version'
11
14
 
12
15
  module Minitest
@@ -3,12 +3,12 @@
3
3
  require_relative 'heat_reporter'
4
4
 
5
5
  module Minitest
6
- def self.plugin_heat_options(opts, options)
7
- opts.on '--show-fast', "Show failures as they happen instead of waiting for the entire suite." do
6
+ def self.plugin_heat_options(opts, _options)
7
+ opts.on '--show-fast', 'Show failures as they happen instead of waiting for the entire suite.' do
8
8
  # Heat.show_fast!
9
9
  end
10
10
 
11
- # TODO options.
11
+ # TODO: options.
12
12
  # 1. Fail Fast
13
13
  # 2. Don't worry about skips.
14
14
  # 3. Skip coverage.
@@ -18,9 +18,9 @@ module Minitest
18
18
  io = options[:io]
19
19
 
20
20
  # Clean out the existing reporters.
21
- self.reporter.reporters = []
21
+ reporter.reporters = []
22
22
 
23
23
  # Use Reviewer as the sole reporter.
24
- self.reporter << HeatReporter.new(io, options)
24
+ reporter << HeatReporter.new(io, options)
25
25
  end
26
26
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "heat"
3
+ require_relative 'heat'
4
4
 
5
5
  module Minitest
6
6
  # Custom minitest reporter to proactively identify likely culprits in test failures by focusing on
@@ -29,50 +29,55 @@ module Minitest
29
29
  # Pulls from minitest-color as well:
30
30
  # https://github.com/teoljungberg/minitest-color/blob/master/lib/minitest/color_plugin.rb
31
31
  class HeatReporter < AbstractReporter
32
-
33
32
  attr_reader :output,
34
33
  :options,
34
+ :timer,
35
35
  :results,
36
36
  :map
37
37
 
38
38
  def initialize(io = $stdout, options = {})
39
- @output = Heat::Output.new(io)
40
39
  @options = options
41
40
 
42
- @results = Heat::Results.new
43
- @map = Heat::Map.new
41
+ @timer = Heat::Timer.new
42
+ @results = Heat::Results.new
43
+ @map = Heat::Map.new
44
+ @output = Heat::Output.new(io)
44
45
  end
45
46
 
46
47
  # Starts reporting on the run.
47
48
  def start
48
- output.puts
49
- output.puts
50
- @results.start_timer!
49
+ timer.start!
50
+
51
+ # A couple of blank lines to create some breathing room
52
+ output.newline
53
+ output.newline
51
54
  end
52
55
 
53
56
  # About to start running a test. This allows a reporter to show that it is starting or that we
54
57
  # are in the middle of a test run.
55
- def prerecord(klass, name)
56
- end
58
+ def prerecord(klass, name); end
57
59
 
58
60
  # Records the data from a result.
61
+ #
59
62
  # Minitest::Result source:
60
63
  # https://github.com/seattlerb/minitest/blob/f4f57afaeb3a11bd0b86ab0757704cb78db96cf4/lib/minitest.rb#L504
61
64
  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
65
+ # Convert a Minitest Result into an "issue" to more consistently expose the data needed to
66
+ # adjust the failure output to the type of failure
67
+ issue = Heat::Issue.new(result)
68
+
69
+ timer.increment_counts(issue.result.assertions)
70
+ results.record(issue)
71
+ map.add(*issue.to_hit) if issue.hit?
72
+
73
+ output.marker(issue.type)
70
74
  end
71
75
 
72
76
  # Outputs the summary of the run.
73
77
  def report
74
- @results.stop_timer!
78
+ timer.stop!
75
79
 
80
+ # A couple of blank lines to create some breathing room
76
81
  output.newline
77
82
  output.newline
78
83
 
@@ -80,26 +85,45 @@ module Minitest
80
85
  # pressing issues are displayed at the bottom of the report in order to reduce scrolling.
81
86
  # This way, as you fix issues, the list gets shorter, and eventually the least critical
82
87
  # issues will be displayed without scrolling once more problematic issues are resolved.
83
- if results.failures.empty? && results.brokens.empty? && results.errors.empty? && results.skips.empty?
84
- results.slows.each { |issue| output.issue_details(issue) }
85
- end
88
+ %i[slows painfuls skips failures brokens errors].each do |issue_category|
89
+ next unless show?(issue_category)
86
90
 
87
- if results.failures.empty? && results.brokens.empty? && results.errors.empty?
88
- results.skips.each { |issue| output.issue_details(issue) }
91
+ results.send(issue_category).each { |issue| output.issue_details(issue) }
89
92
  end
90
93
 
91
- results.failures.each { |issue| output.issue_details(issue) }
92
- results.brokens.each { |issue| output.issue_details(issue) }
93
- results.errors.each { |issue| output.issue_details(issue) }
94
-
95
- output.compact_summary(results)
94
+ # Display a short summary of the total issue counts fore ach category as well as performance
95
+ # details for the test suite as a whole
96
+ output.compact_summary(results, timer)
96
97
 
98
+ # If there were issues, shows a short heat map summary of which files and lines were the most
99
+ # common sources of issues
97
100
  output.heat_map(map)
101
+
102
+ # A blank line to create some breathing room
103
+ output.newline
98
104
  end
99
105
 
100
106
  # Did this run pass?
101
107
  def passed?
102
108
  results.errors.empty? && results.failures.empty?
103
109
  end
110
+
111
+ private
112
+
113
+ def no_problems?
114
+ !results.problems?
115
+ end
116
+
117
+ def no_problems_or_skips?
118
+ !results.problems? && !results.skips.any?
119
+ end
120
+
121
+ def show?(issue_category)
122
+ case issue_category
123
+ when :skips then no_problems?
124
+ when :painfuls, :slows then no_problems_or_skips?
125
+ else true
126
+ end
127
+ end
104
128
  end
105
129
  end
@@ -21,8 +21,6 @@ Gem::Specification.new do |spec|
21
21
  spec.metadata['source_code_uri'] = 'https://github.com/garrettdimon/minitest-heat'
22
22
  spec.metadata['wiki_uri'] = 'https://github.com/garrettdimon/minitest-heat/wiki'
23
23
 
24
-
25
-
26
24
  # Specify which files should be added to the gem when it is released.
27
25
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
28
26
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
@@ -34,7 +32,11 @@ Gem::Specification.new do |spec|
34
32
 
35
33
  spec.add_runtime_dependency 'minitest'
36
34
 
35
+ spec.add_development_dependency 'awesome_print'
37
36
  spec.add_development_dependency 'dead_end'
38
37
  spec.add_development_dependency 'pry'
38
+ spec.add_development_dependency 'rubocop'
39
+ spec.add_development_dependency 'rubocop-minitest'
40
+ spec.add_development_dependency 'rubocop-rake'
39
41
  spec.add_development_dependency 'simplecov'
40
42
  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.3
4
+ version: 0.0.7
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-30 00:00:00.000000000 Z
11
+ date: 2021-10-14 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: awesome_print
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: dead_end
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +66,48 @@ dependencies:
52
66
  - - ">="
53
67
  - !ruby/object:Gem::Version
54
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-minitest
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
55
111
  - !ruby/object:Gem::Dependency
56
112
  name: simplecov
57
113
  requirement: !ruby/object:Gem::Requirement
@@ -75,6 +131,7 @@ extensions: []
75
131
  extra_rdoc_files: []
76
132
  files:
77
133
  - ".gitignore"
134
+ - ".rubocop.yml"
78
135
  - ".travis.yml"
79
136
  - CODE_OF_CONDUCT.md
80
137
  - Gemfile
@@ -86,12 +143,22 @@ files:
86
143
  - bin/setup
87
144
  - lib/minitest/heat.rb
88
145
  - lib/minitest/heat/backtrace.rb
146
+ - lib/minitest/heat/hit.rb
89
147
  - lib/minitest/heat/issue.rb
148
+ - lib/minitest/heat/line.rb
90
149
  - lib/minitest/heat/location.rb
91
150
  - lib/minitest/heat/map.rb
92
151
  - lib/minitest/heat/output.rb
152
+ - lib/minitest/heat/output/backtrace.rb
153
+ - lib/minitest/heat/output/issue.rb
154
+ - lib/minitest/heat/output/map.rb
155
+ - lib/minitest/heat/output/marker.rb
156
+ - lib/minitest/heat/output/results.rb
157
+ - lib/minitest/heat/output/source_code.rb
158
+ - lib/minitest/heat/output/token.rb
93
159
  - lib/minitest/heat/results.rb
94
160
  - lib/minitest/heat/source.rb
161
+ - lib/minitest/heat/timer.rb
95
162
  - lib/minitest/heat/version.rb
96
163
  - lib/minitest/heat_plugin.rb
97
164
  - lib/minitest/heat_reporter.rb
@@ -121,7 +188,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
121
188
  - !ruby/object:Gem::Version
122
189
  version: '0'
123
190
  requirements: []
124
- rubygems_version: 3.1.4
191
+ rubygems_version: 3.1.6
125
192
  signing_key:
126
193
  specification_version: 4
127
194
  summary: Presents test results in a visual manner to guide you to where to look first.