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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1b06865dbf64c7b926b600aa635f27e00a0ac65cd08c25cc35ac2ee4de1768a0
4
- data.tar.gz: 79ba518f1b7137992f35ac2489ac09c6d5687ae5a8c68e166995a63277e02f99
3
+ metadata.gz: c523abe811fe169d2a919fe4495cad61c8f282f0d76809d302504f94b6030eb4
4
+ data.tar.gz: fffa2dcf19662bd6aa2b4ab46243cb797f9de5fa32723428008496d6942ad11e
5
5
  SHA512:
6
- metadata.gz: 3e5450a68e065e1573a21acba73537e2a003afdc098e7aab6eed2b41638c66a82d74a97faf3d94c9cb79c6279ebda7567dee4bd6a5bec75599e11c170c07e5dc
7
- data.tar.gz: 8386cc9f07313cec0686e5efce192a9a7763b78a628ea0eb2b428890a7f5d43e30c472fe4dbcc65497fc2d908d7fc79457a5784890e583951b33dec964259cbf
6
+ metadata.gz: 7efa01e486a94c5506b6e68b0eb7c200243575909546f871dc2a905e7477d124f85c79d4f0160b9f101fc2d42f01828500565c6fb3434f207d5699b7adabd715
7
+ data.tar.gz: 114ee328cacda9818932c188368f7d808587374012af973998dcd1ee777f30c6fd878f97b3093e2fadb87010e6db5d7f3a0b739a2c805dd162ff41b51a05f99c
data/.rubocop.yml CHANGED
@@ -2,6 +2,7 @@ AllCops:
2
2
  NewCops: enable
3
3
  UseCache: true
4
4
  CacheRootDirectory: './'
5
+ TargetRubyVersion: 2.5.9
5
6
  Exclude:
6
7
  - 'bin/**/*'
7
8
  - 'test/files/source.rb' # An example test file for reading source code
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- minitest-heat (0.0.9)
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 (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
@@ -21,33 +50,39 @@ Or install it yourself as:
21
50
 
22
51
  $ gem install minitest-heat
23
52
 
24
- And add this line to your `test/test_helper.rb` file:
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
- ## 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,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.nil? || raw_backtrace.empty?
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
- def final_test_location
34
- test_entries.first
35
- end
36
-
37
- def project_entries
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
- def test_entries
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
- def source_code_entries
50
- @source_code_entries ||= project_entries - test_entries
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
- def parsed_entries
54
- return [] if raw_backtrace.nil?
55
-
56
- @parsed_entries ||= raw_backtrace.map { |entry| Line.parse_backtrace(entry) }
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
- private
60
-
61
- def parse(entry)
62
- Line.parse_backtrace(entry)
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
- def test_file?(entry)
66
- entry.file.to_s.end_with?('_test.rb') || entry.file.to_s.start_with?('test_')
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
@@ -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
- def log(type, line_number)
34
- @issues[type] ||= []
35
- @issues[type] << line_number
36
- end
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
- def mtime
39
- pathname.mtime
40
- end
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
- def age_in_seconds
43
- (Time.now - mtime).to_i
44
- end
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|