minitest-heat 0.0.10 → 0.0.14

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c60c717971a6a1858de4337247b8bfd37f63a63f31a1783de4ecaaad8ca5a8c
4
- data.tar.gz: c491cb526bd8a243810fe88da7d15679a6c5182b256a5187db4c457bcc9b7a7a
3
+ metadata.gz: 4009f20d5e5d4d92d8e0796e853df4da059e3ec7a6b49cae98a8ce71352316fd
4
+ data.tar.gz: adf5692ab8af5f08ec97607f43ac8f454ad91fb62d15d9f867ee354d920a7a45
5
5
  SHA512:
6
- metadata.gz: abf3a4c476c1041f75fedfa3eac1235f134bc99bbbf1c909e6e407e461380e9028079e3ad927180d7bec4f039c54247172c9a12bee579dec5eab53b444cf3f68
7
- data.tar.gz: fb7d2373153fdaaf6fe8b3a6ec693baf3e04a8fa8dc5636f75d1b0afb45e5a7b25365f3dbe44b0aa3c53bb79b8ed7b0551c3798ceaa3477ce1d9e122159e116a
6
+ metadata.gz: 8e0d422fbc88d79598464f1640ffb7b6ea3e8ab7324e85f9a059a61f1d14e6c8792f277525dadf6cf81ee493168708dd0eb1a3f9b836b7efd0af3e73b959f25b
7
+ data.tar.gz: dfa2aa6267e1c026da29b93e1e7ab73f4a1fc65d9d1132228efff315622b742ebe40074eec7a639e6246b7bad27f14a31feee647c9c1274b77d8a57b8d8ed22b
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- minitest-heat (0.0.10)
4
+ minitest-heat (0.0.14)
5
5
  minitest
6
6
 
7
7
  GEM
@@ -10,7 +10,7 @@ GEM
10
10
  ast (2.4.2)
11
11
  awesome_print (1.9.2)
12
12
  coderay (1.1.3)
13
- dead_end (1.1.7)
13
+ dead_end (3.0.2)
14
14
  docile (1.4.0)
15
15
  method_source (1.0.0)
16
16
  minitest (5.14.4)
@@ -24,7 +24,7 @@ GEM
24
24
  rake (12.3.3)
25
25
  regexp_parser (2.1.1)
26
26
  rexml (3.2.5)
27
- rubocop (1.22.1)
27
+ rubocop (1.22.3)
28
28
  parallel (~> 1.10)
29
29
  parser (>= 3.0.0.0)
30
30
  rainbow (>= 2.2.2, < 4.0)
@@ -33,9 +33,9 @@ GEM
33
33
  rubocop-ast (>= 1.12.0, < 2.0)
34
34
  ruby-progressbar (~> 1.7)
35
35
  unicode-display_width (>= 1.4.0, < 3.0)
36
- rubocop-ast (1.12.0)
36
+ rubocop-ast (1.13.0)
37
37
  parser (>= 3.0.1.1)
38
- rubocop-minitest (0.15.1)
38
+ rubocop-minitest (0.15.2)
39
39
  rubocop (>= 0.90, < 2.0)
40
40
  rubocop-rake (0.6.0)
41
41
  rubocop (~> 1.0)
@@ -64,4 +64,4 @@ DEPENDENCIES
64
64
  simplecov
65
65
 
66
66
  BUNDLED WITH
67
- 2.1.4
67
+ 2.2.30
data/README.md CHANGED
@@ -1,12 +1,41 @@
1
1
  # Minitest::Heat
2
- **Important:** As of September 13, 2021, Minitest::Heat is an early work-in-progress. It's usable, but it can still ocasionally be buggy as it takes shape.
2
+ Minitest::Heat helps you identify problems faster so you can more efficiently resolve test failures. It does this through a few different methods.
3
3
 
4
- Minitest::Heat aims to surface context around test failures to help you more efficiently identify and prioritize fixing failed tests to help save time.
4
+ It collects failures and inspects backtraces to identify patterns and provide a heat map summary of the files and line numbers that most frequently appear to be the causes of issues.
5
5
 
6
- For some early insight about priorities and how it works, this [Twitter thread](https://twitter.com/garrettdimon/status/1432703746526560266) is currently the best place to start.
6
+ ![Example Heat Map Displayed by Minitest Heat](https://raw.githubusercontent.com/garrettdimon/minitest-heat/main/examples/map.png)
7
7
 
8
- ## Installation
8
+ It suppresses less critical issues like skips or slows when there are legitimate failures. It won't display information about slow tests unless all tests are passing (meaning no errors, failures, or skips)
9
+
10
+ It presents failures differently depending on the context of failure. For instance, it treats exceptions differently based on whether they arose directly from a test or from source code. It also treats extremely slow tests differently from moderately slow tests.
11
+
12
+ Markers get some nuance so that slow tests receive different markers than standard passing tests, and exception-triggered failures get different markers for source-code triggered exceptions (E) and test-triggered exceptions ('B' for 'Broken Test').
13
+
14
+ ![Example Markers Displayed by Minitest Heat](https://raw.githubusercontent.com/garrettdimon/minitest-heat/main/examples/markers.png)
15
+
16
+ It also formats the failure details and backtraces to make them more scannable by emphasizing the project-relates lines from the backtrace.
17
+
18
+ It intelligently recognizes when an exception was raised from a test defintion vs. when an exception is genuinely triggered from the source code in order to help focus on fixing deeper exceptions first.
19
+
20
+ ![Example Exceptions Displayed by Minitest Heat](https://raw.githubusercontent.com/garrettdimon/minitest-heat/main/examples/exceptions.png)
21
+
22
+ Failures are displayed ina fairly predictable manner but formatted to show the source code from the test so you can see the assertion that failed in addition to the summary of values that didn't satisfy the assertion.
23
+
24
+ ![Example Failures Displayed by Minitest Heat](https://raw.githubusercontent.com/garrettdimon/minitest-heat/main/examples/failures.png)
25
+
26
+ Skipped tests are displayed in a simple manner as well so that it's easy to see the source of the skipped test as well as the reason it was skipped.
27
+
28
+ ![Example Skips Displayed by Minitest Heat](https://raw.githubusercontent.com/garrettdimon/minitest-heat/main/examples/skips.png)
29
+
30
+ Slow tests get slightly more informative labels to indicate that they did pass, but they could use performance improvements. Tests that are particularly slow are called out with a little more emphasis so it's easier to focus on really slow tests first as they frequently represent the most potential for performance gains.
9
31
 
32
+ ![Example Slows Displayed by Minitest Heat](https://raw.githubusercontent.com/garrettdimon/minitest-heat/main/examples/slows.png)
33
+
34
+ It also always displays the most significant issues at the bottom of the list in order to reduce the need to scroll up through the test failures. As you fix issues, the list becomes shorter, and the less significant issues will make there way to the bottom and be visible without scrolling.
35
+
36
+ For some additional insight about priorities and how it works, this [Twitter thread](https://twitter.com/garrettdimon/status/1432703746526560266) is currently the best place to start.
37
+
38
+ ## Installation
10
39
  Add this line to your application's Gemfile:
11
40
 
12
41
  ```ruby
@@ -27,27 +56,33 @@ And depending on your usage, you may need to require Minitest Heat in your test
27
56
  require 'minitest/heat'
28
57
  ```
29
58
 
30
- ## Usage
31
-
32
- **Important:** In its current state, `Minitest::Heat` replaces any other reporter plugins you may have. Long-term, it should play nicer with other reporters, but during the initial heavy development cycle, it's been easier to have a high confidence that other reporters aren't the source of unexpected behavior.
33
-
34
- Otherwise, once it's bundled and added to your `test_helper`, it shold "just work" whenever you run your test suite.
59
+ ## Configuration
60
+ Minitest Heat doesn't currently offer a significant set of configuration options, but it will eventually support customizing the thresholds for "Slow" and "Painfully Slow". By default, it considers anything over 1.0s to be 'slow' and anything over 3.0s to be 'painfully slow'.
35
61
 
36
62
  ## Development
37
-
38
63
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
39
64
 
40
65
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
41
66
 
42
- ## Contributing
67
+ ### Forcing Test Failures
68
+ In order to easily see how Minitest Heat handles different combinations of different types of failures, the following environment variables can be used to force failures.
69
+
70
+ ```bash
71
+ IMPLODE=true # Every possible type of failure, skip, and slow is generated
72
+ FORCE_EXCEPTIONS=true # Only exception-triggered failures
73
+ FORCE_FAILURES=true # Only standard assertion failures
74
+ FORCE_SKIPS=true # No errors, just the skipped tests
75
+ FORCE_SLOWS=true # No errors or skipped tests, just slow tests
76
+ ```
43
77
 
44
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/minitest-heat. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/minitest-heat/blob/master/CODE_OF_CONDUCT.md).
78
+ So to see the full context of a test suite, `IMPLODE=true bundle exec rake` will work its magic.
45
79
 
80
+ ## Contributing
81
+ Bug reports and pull requests are welcome on GitHub at https://github.com/garrettdimon/minitest-heat. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/minitest-heat/blob/master/CODE_OF_CONDUCT.md).
46
82
 
47
83
  ## License
48
84
 
49
85
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
50
86
 
51
87
  ## Code of Conduct
52
-
53
88
  Everyone interacting in the Minitest::Heat project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/minitest-heat/blob/master/CODE_OF_CONDUCT.md).
Binary file
Binary file
data/examples/map.png ADDED
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module Minitest
6
+ module Heat
7
+ class Backtrace
8
+ # Represents a line from a backtrace to provide more convenient access to information about
9
+ # the relevant file and line number for displaying in test results
10
+ module LineParser
11
+ # Parses a line from a backtrace in order to convert it to usable components
12
+ def self.read(raw_text)
13
+ raw_pathname, raw_line_number, raw_container = raw_text.split(':')
14
+ raw_container = raw_container.delete_prefix('in `').delete_suffix("'")
15
+
16
+ ::Minitest::Heat::Location.new(
17
+ pathname: raw_pathname,
18
+ line_number: raw_line_number,
19
+ container: raw_container
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'backtrace/line'
3
+ require_relative 'backtrace/line_parser'
4
4
 
5
5
  module Minitest
6
6
  module Heat
@@ -9,7 +9,7 @@ module Minitest
9
9
  attr_reader :raw_backtrace
10
10
 
11
11
  # Creates a more flexible backtrace data structure by parsing the lines of the backtrace to
12
- # extract individual elements for investigating the offending files and line numbers
12
+ # extract specific subsets of lines for investigating the offending files and line numbers
13
13
  # @param raw_backtrace [Array] the array of lines from the backtrace
14
14
  #
15
15
  # @return [self]
@@ -24,80 +24,42 @@ module Minitest
24
24
  raw_backtrace.empty?
25
25
  end
26
26
 
27
- # The final location exposed in the backtrace. Could be a line from the project or from a
28
- # dependency or the Ruby core libraries
27
+ # All lines of the backtrace converted to Backtrace::LineParser's
29
28
  #
30
- # @return [Line] the final location from the backtrace parsed as a Backtrace::Line
31
- def final_location
32
- parsed_entries.first
33
- end
34
-
35
- # The final location from within the project exposed in the backtrace. Could be test files or
36
- # source code files
37
- #
38
- # @return [Line] the final project location from the backtrace parsed as a Backtrace::Line
39
- def final_project_location
40
- project_entries.first
41
- end
42
-
43
- # The most recently modified location from within the project
44
- #
45
- # @return [Line] the most recently modified project location from the backtrace parsed as a
46
- # Backtrace::Line
47
- def freshest_project_location
48
- recently_modified_entries.first
49
- end
29
+ # @return [Array<Location>] the full set of backtrace lines parsed as Location instances
30
+ def locations
31
+ return [] if raw_backtrace.nil?
50
32
 
51
- # The final location from within the project source code (i.e. excluding tests)
52
- #
53
- # @return [Line] the final source code location from the backtrace parsed as a Backtrace::Line
54
- def final_source_code_location
55
- source_code_entries.first
33
+ @locations ||= raw_backtrace.map { |entry| Backtrace::LineParser.read(entry) }
56
34
  end
57
35
 
58
- # The final location from within the project's tests (i.e. excluding source code)
36
+ # All entries from the backtrace within the project and sorted with the most recently modified
37
+ # files at the beginning
59
38
  #
60
- # @return [Line] the final test location from the backtrace parsed as a Backtrace::Line
61
- def final_test_location
62
- test_entries.first
39
+ # @return [Array<Location>] the sorted backtrace lines from the project
40
+ def recently_modified_locations
41
+ @recently_modified_locations ||= project_locations.sort_by(&:mtime).reverse
63
42
  end
64
43
 
65
44
  # All entries from the backtrace that are files within the project
66
45
  #
67
- # @return [Line] the backtrace lines from within the project parsed as Backtrace::Line's
68
- def project_entries
69
- @project_entries ||= parsed_entries.select { |entry| entry.path.to_s.include?(Dir.pwd) }
70
- end
71
-
72
- # All entries from the backtrace within the project and sorted with the most recently modified
73
- # files at the beginning
74
- #
75
- # @return [Line] the sorted backtrace lines from the project parsed as Backtrace::Line's
76
- def recently_modified_entries
77
- @recently_modified_entries ||= project_entries.sort_by(&:mtime).reverse
46
+ # @return [Array<Location>] the backtrace lines from within the project
47
+ def project_locations
48
+ @project_locations ||= locations.select(&:project_file?)
78
49
  end
79
50
 
80
51
  # All entries from the backtrace within the project tests
81
52
  #
82
- # @return [Line] the backtrace lines from within the project tests parsed as Backtrace::Line's
83
- def test_entries
84
- @test_entries ||= project_entries.select(&:test_file?)
53
+ # @return [Array<Location>] the backtrace lines from within the tests
54
+ def test_locations
55
+ @test_locations ||= project_locations.select(&:test_file?)
85
56
  end
86
57
 
87
58
  # All source code entries from the backtrace (i.e. excluding tests)
88
59
  #
89
- # @return [Line] the backtrace lines from within the source code parsed as Backtrace::Line's
90
- def source_code_entries
91
- @source_code_entries ||= project_entries - test_entries
92
- end
93
-
94
- # All lines of the backtrace converted to Backtrace::Line's
95
- #
96
- # @return [Line] the full set of backtrace lines parsed as Backtrace::Line instances
97
- def parsed_entries
98
- return [] if raw_backtrace.nil?
99
-
100
- @parsed_entries ||= raw_backtrace.map { |entry| Backtrace::Line.parse_backtrace(entry) }
60
+ # @return [Array<Location>] the backtrace lines from within the source code
61
+ def source_code_locations
62
+ @source_code_locations ||= project_locations.select(&:source_code_file?)
101
63
  end
102
64
  end
103
65
  end
@@ -5,8 +5,10 @@ require 'forwardable'
5
5
  module Minitest
6
6
  module Heat
7
7
  # Kind of like an issue, but instead of focusing on a failing test, it covers all issues for a
8
- # given file to build a heat map of the affected files
8
+ # given file to build a heat map of the affected files and line numbers
9
9
  class Hit
10
+ Trace = Struct.new(:type, :line_number, :locations)
11
+
10
12
  # So we can sort hot spots by liklihood of being the most important spot to check out before
11
13
  # trying to fix something. These are ranked based on the possibility they represent ripple
12
14
  # effects where fixing one problem could potentially fix multiple other failures.
@@ -23,7 +25,7 @@ module Minitest
23
25
  slow: 0
24
26
  }.freeze
25
27
 
26
- attr_reader :pathname, :issues
28
+ attr_reader :pathname, :issues, :lines
27
29
 
28
30
  # Creates an instance of a Hit for the given pathname. It must be the full pathname to
29
31
  # uniquely identify the file or we could run into collisions that muddy the water and
@@ -34,16 +36,26 @@ module Minitest
34
36
  def initialize(pathname)
35
37
  @pathname = Pathname(pathname)
36
38
  @issues = {}
39
+ @lines = {}
37
40
  end
38
41
 
39
42
  # Adds a record of a given issue type for the line number
40
43
  # @param type [Symbol] one of Issue::TYPES
41
44
  # @param line_number [Integer,String] the line number to record the issue on
45
+ # @param backtrace: nil [Array<Location>] the project locations from the backtrace
42
46
  #
43
- # @return [type] [description]
44
- def log(type, line_number)
45
- @issues[type] ||= []
46
- @issues[type] << Integer(line_number)
47
+ # @return [void]
48
+ def log(type, line_number, backtrace: [])
49
+ line_number = Integer(line_number)
50
+ issue_type = type.to_sym
51
+
52
+ # Store issues by issue type with an array of line numbers
53
+ @issues[issue_type] ||= []
54
+ @issues[issue_type] << line_number
55
+
56
+ # Store issues by line number with an array of Traces
57
+ @lines[line_number.to_s] ||= []
58
+ @lines[line_number.to_s] << Trace.new(issue_type, line_number, backtrace)
47
59
  end
48
60
 
49
61
  # Calcuates an approximate weight to serve as a proxy for which files are most likely to be
@@ -18,7 +18,7 @@ module Minitest
18
18
  }.freeze
19
19
 
20
20
  attr_reader :assertions,
21
- :location,
21
+ :locations,
22
22
  :message,
23
23
  :test_class,
24
24
  :test_identifier,
@@ -27,7 +27,7 @@ module Minitest
27
27
  :error,
28
28
  :skipped
29
29
 
30
- def_delegators :@location, :backtrace, :test_definition_line, :test_failure_line
30
+ def_delegators :@locations, :backtrace, :test_definition_line, :test_failure_line
31
31
 
32
32
  # Extracts the necessary data from result.
33
33
  # @param result [Minitest::Result] the instance of Minitest::Result to examine
@@ -39,7 +39,7 @@ module Minitest
39
39
 
40
40
  new(
41
41
  assertions: result.assertions,
42
- location: result.source_location,
42
+ test_location: result.source_location,
43
43
  test_class: result.klass,
44
44
  test_identifier: result.name,
45
45
  execution_time: result.time,
@@ -47,7 +47,7 @@ module Minitest
47
47
  error: result.error?,
48
48
  skipped: result.skipped?,
49
49
  message: exception&.message,
50
- backtrace: exception&.backtrace,
50
+ backtrace: exception&.backtrace
51
51
  )
52
52
  end
53
53
 
@@ -57,7 +57,7 @@ module Minitest
57
57
  # @param assertions: 1 [Integer] the number of assertions in the result
58
58
  # @param message: nil [String] exception if there is one
59
59
  # @param backtrace: [] [Array<String>] the array of backtrace lines from an exception
60
- # @param location: nil [String] the location identifier for a test
60
+ # @param test_location: nil [Array<String, Integer>] the locations identifier for a test
61
61
  # @param test_class: nil [String] the class name for the test result's containing class
62
62
  # @param test_identifier: nil [String] the name of the test
63
63
  # @param execution_time: nil [Float] the time it took to run the test
@@ -66,11 +66,11 @@ module Minitest
66
66
  # @param skipped: false [Boolean] true if the test was skipped
67
67
  #
68
68
  # @return [type] [description]
69
- def initialize(assertions: 1, location: ['unknown', 1], backtrace: [], execution_time: 0.0, message: nil, test_class: nil, test_identifier: nil, passed: false, error: false, skipped: false)
69
+ def initialize(assertions: 1, test_location: ['Unrecognized Test File', 1], backtrace: [], execution_time: 0.0, message: nil, test_class: nil, test_identifier: nil, passed: false, error: false, skipped: false)
70
70
  @message = message
71
71
 
72
72
  @assertions = Integer(assertions)
73
- @location = Location.new(location, backtrace)
73
+ @locations = Locations.new(test_location, backtrace)
74
74
 
75
75
  @test_class = test_class
76
76
  @test_identifier = test_identifier
@@ -99,9 +99,9 @@ module Minitest
99
99
  :skipped
100
100
  elsif !passed?
101
101
  :failure
102
- elsif painful?
102
+ elsif passed? && painful?
103
103
  :painful
104
- elsif slow?
104
+ elsif passed? && slow?
105
105
  :slow
106
106
  else
107
107
  :success
@@ -116,37 +116,55 @@ module Minitest
116
116
  !passed? || slow? || painful?
117
117
  end
118
118
 
119
+ # The number, in seconds, for a test to be considered "slow"
120
+ #
121
+ # @return [Float] number of seconds after which a test is considered slow
122
+ def slow_threshold
123
+ # Using a method here so that this can eventually be configurable such that the constant is
124
+ # only a fallback value if it's not specified anywhere else
125
+ SLOW_THRESHOLDS[:slow]
126
+ end
127
+
128
+ # The number, in seconds, for a test to be considered "painfully slow"
129
+ #
130
+ # @return [Float] number of seconds after which a test is considered painfully slow
131
+ def painfully_slow_threshold
132
+ # Using a method here so that this can eventually be configurable such that the constant is
133
+ # only a fallback value if it's not specified anywhere else
134
+ SLOW_THRESHOLDS[:painful]
135
+ end
136
+
119
137
  # Determines if a test should be considered slow by comparing it to the low end definition of
120
138
  # what is considered slow.
121
139
  #
122
- # @return [Boolean] true if the test took longer to run than `SLOW_THRESHOLDS[:slow]`
140
+ # @return [Boolean] true if the test took longer to run than `slow_threshold`
123
141
  def slow?
124
- execution_time >= SLOW_THRESHOLDS[:slow] && execution_time < SLOW_THRESHOLDS[:painful]
142
+ execution_time >= slow_threshold && execution_time < painfully_slow_threshold
125
143
  end
126
144
 
127
145
  # Determines if a test should be considered painfully slow by comparing it to the high end
128
146
  # definition of what is considered slow.
129
147
  #
130
- # @return [Boolean] true if the test took longer to run than `SLOW_THRESHOLDS[:painful]`
148
+ # @return [Boolean] true if the test took longer to run than `painfully_slow_threshold`
131
149
  def painful?
132
- execution_time >= SLOW_THRESHOLDS[:painful]
150
+ execution_time >= painfully_slow_threshold
133
151
  end
134
152
 
135
153
  # Determines if the issue is an exception that was raised from directly within a test
136
154
  # definition. In these cases, it's more likely to be a quick fix.
137
155
  #
138
- # @return [Boolean] true if the final location of the stacktrace was a test file
156
+ # @return [Boolean] true if the final locations of the stacktrace was a test file
139
157
  def in_test?
140
- location.broken_test?
158
+ locations.broken_test?
141
159
  end
142
160
 
143
161
  # Determines if the issue is an exception that was raised from directly within the project
144
162
  # codebase.
145
163
  #
146
- # @return [Boolean] true if the final location of the stacktrace was a file from the project
164
+ # @return [Boolean] true if the final locations of the stacktrace was a file from the project
147
165
  # (as opposed to a dependency or Ruby library)
148
166
  def in_source?
149
- location.proper_failure?
167
+ locations.proper_failure?
150
168
  end
151
169
 
152
170
  # Was the result a pass? i.e. Skips aren't passes or failures. Slows are still passes. So this