minitest-heat 0.0.9 → 0.0.13
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/Gemfile.lock +6 -6
- data/README.md +49 -14
- data/examples/exceptions.png +0 -0
- data/examples/failures.png +0 -0
- data/examples/map.png +0 -0
- data/examples/markers.png +0 -0
- data/examples/skips.png +0 -0
- data/examples/slows.png +0 -0
- data/lib/minitest/heat/backtrace/line_parser.rb +25 -0
- data/lib/minitest/heat/backtrace.rb +39 -43
- data/lib/minitest/heat/hit.rb +36 -19
- data/lib/minitest/heat/issue.rb +118 -81
- data/lib/minitest/heat/location.rb +115 -116
- data/lib/minitest/heat/locations.rb +105 -0
- data/lib/minitest/heat/map.rb +16 -4
- data/lib/minitest/heat/output/backtrace.rb +90 -67
- data/lib/minitest/heat/output/issue.rb +76 -67
- data/lib/minitest/heat/output/map.rb +127 -25
- data/lib/minitest/heat/output/marker.rb +5 -3
- data/lib/minitest/heat/output/results.rb +3 -2
- data/lib/minitest/heat/output/source_code.rb +1 -1
- data/lib/minitest/heat/output/token.rb +2 -1
- data/lib/minitest/heat/output.rb +76 -6
- data/lib/minitest/heat/results.rb +23 -3
- data/lib/minitest/heat/source.rb +1 -1
- data/lib/minitest/heat/version.rb +1 -1
- data/lib/minitest/heat.rb +3 -2
- data/lib/minitest/heat_plugin.rb +9 -17
- data/lib/minitest/heat_reporter.rb +25 -35
- metadata +11 -4
- data/lib/minitest/heat/line.rb +0 -74
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c523abe811fe169d2a919fe4495cad61c8f282f0d76809d302504f94b6030eb4
|
4
|
+
data.tar.gz: fffa2dcf19662bd6aa2b4ab46243cb797f9de5fa32723428008496d6942ad11e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7efa01e486a94c5506b6e68b0eb7c200243575909546f871dc2a905e7477d124f85c79d4f0160b9f101fc2d42f01828500565c6fb3434f207d5699b7adabd715
|
7
|
+
data.tar.gz: 114ee328cacda9818932c188368f7d808587374012af973998dcd1ee777f30c6fd878f97b3093e2fadb87010e6db5d7f3a0b739a2c805dd162ff41b51a05f99c
|
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
minitest-heat (0.0.
|
4
|
+
minitest-heat (0.0.13)
|
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 (
|
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.
|
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.
|
36
|
+
rubocop-ast (1.13.0)
|
37
37
|
parser (>= 3.0.1.1)
|
38
|
-
rubocop-minitest (0.15.
|
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.
|
67
|
+
2.2.30
|
data/README.md
CHANGED
@@ -1,12 +1,41 @@
|
|
1
1
|
# Minitest::Heat
|
2
|
-
|
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
|
-
|
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
|
-
|
6
|
+
![Example Heat Map Displayed by Minitest Heat](https://raw.githubusercontent.com/garrettdimon/minitest-heat/main/examples/map.png)
|
7
7
|
|
8
|
-
|
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
|
@@ -21,33 +50,39 @@ Or install it yourself as:
|
|
21
50
|
|
22
51
|
$ gem install minitest-heat
|
23
52
|
|
24
|
-
And
|
53
|
+
And depending on your usage, you may need to require Minitest Heat in your test suite:
|
25
54
|
|
26
55
|
```ruby
|
27
56
|
require 'minitest/heat'
|
28
57
|
```
|
29
58
|
|
30
|
-
##
|
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
|
-
|
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
|
-
|
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
|
data/examples/skips.png
ADDED
Binary file
|
data/examples/slows.png
ADDED
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,69 +1,65 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'backtrace/line_parser'
|
4
|
+
|
3
5
|
module Minitest
|
4
6
|
module Heat
|
5
7
|
# Wrapper for separating backtrace into component parts
|
6
8
|
class Backtrace
|
7
9
|
attr_reader :raw_backtrace
|
8
10
|
|
11
|
+
# Creates a more flexible backtrace data structure by parsing the lines of the backtrace to
|
12
|
+
# extract specific subsets of lines for investigating the offending files and line numbers
|
13
|
+
# @param raw_backtrace [Array] the array of lines from the backtrace
|
14
|
+
#
|
15
|
+
# @return [self]
|
9
16
|
def initialize(raw_backtrace)
|
10
|
-
@raw_backtrace = raw_backtrace
|
17
|
+
@raw_backtrace = Array(raw_backtrace)
|
11
18
|
end
|
12
19
|
|
20
|
+
# Determines if the raw backtrace has values in it
|
21
|
+
#
|
22
|
+
# @return [Boolean] true if there's no backtrace or it's empty
|
13
23
|
def empty?
|
14
|
-
raw_backtrace.
|
15
|
-
end
|
16
|
-
|
17
|
-
def final_location
|
18
|
-
parsed_entries.first
|
19
|
-
end
|
20
|
-
|
21
|
-
def final_project_location
|
22
|
-
project_entries.first
|
23
|
-
end
|
24
|
-
|
25
|
-
def freshest_project_location
|
26
|
-
recently_modified_entries.first
|
27
|
-
end
|
28
|
-
|
29
|
-
def final_source_code_location
|
30
|
-
source_code_entries.first
|
24
|
+
raw_backtrace.empty?
|
31
25
|
end
|
32
26
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
@project_entries ||= parsed_entries.select { |entry| entry.path.to_s.include?(Dir.pwd) }
|
39
|
-
end
|
40
|
-
|
41
|
-
def recently_modified_entries
|
42
|
-
@recently_modified_entries ||= project_entries.sort_by(&:mtime).reverse
|
43
|
-
end
|
27
|
+
# All lines of the backtrace converted to Backtrace::LineParser's
|
28
|
+
#
|
29
|
+
# @return [Array<Location>] the full set of backtrace lines parsed as Location instances
|
30
|
+
def locations
|
31
|
+
return [] if raw_backtrace.nil?
|
44
32
|
|
45
|
-
|
46
|
-
@tests_entries ||= project_entries.select { |entry| test_file?(entry) }
|
33
|
+
@locations ||= raw_backtrace.map { |entry| Backtrace::LineParser.read(entry) }
|
47
34
|
end
|
48
35
|
|
49
|
-
|
50
|
-
|
36
|
+
# All entries from the backtrace within the project and sorted with the most recently modified
|
37
|
+
# files at the beginning
|
38
|
+
#
|
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
|
51
42
|
end
|
52
43
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
44
|
+
# All entries from the backtrace that are files within the project
|
45
|
+
#
|
46
|
+
# @return [Array<Location>] the backtrace lines from within the project
|
47
|
+
def project_locations
|
48
|
+
@project_locations ||= locations.select(&:project_file?)
|
57
49
|
end
|
58
50
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
51
|
+
# All entries from the backtrace within the project tests
|
52
|
+
#
|
53
|
+
# @return [Array<Location>] the backtrace lines from within the tests
|
54
|
+
def test_locations
|
55
|
+
@test_locations ||= project_locations.select(&:test_file?)
|
63
56
|
end
|
64
57
|
|
65
|
-
|
66
|
-
|
58
|
+
# All source code entries from the backtrace (i.e. excluding tests)
|
59
|
+
#
|
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?)
|
67
63
|
end
|
68
64
|
end
|
69
65
|
end
|
data/lib/minitest/heat/hit.rb
CHANGED
@@ -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
|
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,34 +25,43 @@ module Minitest
|
|
23
25
|
slow: 0
|
24
26
|
}.freeze
|
25
27
|
|
26
|
-
attr_reader :pathname, :issues
|
28
|
+
attr_reader :pathname, :issues, :lines
|
27
29
|
|
30
|
+
# Creates an instance of a Hit for the given pathname. It must be the full pathname to
|
31
|
+
# uniquely identify the file or we could run into collisions that muddy the water and
|
32
|
+
# obscure which files had which errors on which line numbers
|
33
|
+
# @param pathname [Pathname,String] the full pathname to the file
|
34
|
+
#
|
35
|
+
# @return [self]
|
28
36
|
def initialize(pathname)
|
29
37
|
@pathname = Pathname(pathname)
|
30
38
|
@issues = {}
|
39
|
+
@lines = {}
|
31
40
|
end
|
32
41
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
42
|
+
# Adds a record of a given issue type for the line number
|
43
|
+
# @param type [Symbol] one of Issue::TYPES
|
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
|
46
|
+
#
|
47
|
+
# @return [void]
|
48
|
+
def log(type, line_number, backtrace: [])
|
49
|
+
line_number = Integer(line_number)
|
50
|
+
issue_type = type.to_sym
|
37
51
|
|
38
|
-
|
39
|
-
|
40
|
-
|
52
|
+
# Store issues by issue type with an array of line numbers
|
53
|
+
@issues[issue_type] ||= []
|
54
|
+
@issues[issue_type] << line_number
|
41
55
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
def issue_count
|
47
|
-
count = 0
|
48
|
-
Issue::TYPES.each do |issue_type|
|
49
|
-
count += issues.fetch(issue_type) { [] }.size
|
50
|
-
end
|
51
|
-
count
|
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)
|
52
59
|
end
|
53
60
|
|
61
|
+
# Calcuates an approximate weight to serve as a proxy for which files are most likely to be
|
62
|
+
# the most problematic across the various issue types
|
63
|
+
#
|
64
|
+
# @return [Integer] the problem weight for the file
|
54
65
|
def weight
|
55
66
|
weight = 0
|
56
67
|
issues.each_pair do |type, values|
|
@@ -59,6 +70,9 @@ module Minitest
|
|
59
70
|
weight
|
60
71
|
end
|
61
72
|
|
73
|
+
# The total issue count for the file across all issue types. Includes duplicates if they exist
|
74
|
+
#
|
75
|
+
# @return [Integer] the sum of the counts for all line numbers for all issue types
|
62
76
|
def count
|
63
77
|
count = 0
|
64
78
|
issues.each_pair do |_type, values|
|
@@ -67,6 +81,9 @@ module Minitest
|
|
67
81
|
count
|
68
82
|
end
|
69
83
|
|
84
|
+
# The full set of unique line numbers across all issue types
|
85
|
+
#
|
86
|
+
# @return [Array<Integer>] the full set of unique offending line numbers for the hit
|
70
87
|
def line_numbers
|
71
88
|
line_numbers = []
|
72
89
|
issues.each_pair do |_type, values|
|