minitest-heat 0.0.12 → 1.1.0

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: d95ecb7dad79fef49909bf766fb8c428489a71a0a2a50f815e4f0e7b0dbe480b
4
- data.tar.gz: f9e03a7fcb915c0f5f0e74eb20fb7cec2d65de7e4d73f6130b61d50d48d8fafc
3
+ metadata.gz: cf4115aeb375e2fef72d98e7f706dc023ae1e61fa9d1fc3e8f490797a31e2ae0
4
+ data.tar.gz: ba968646ff2b12fa2891b47fb58d2d98330d117c2a038e29f1caef449bfbd857
5
5
  SHA512:
6
- metadata.gz: d9903cc0ea5edb14005ddcef3027ea09414100013f3e829989670a238bce2c2bd02b0227835632658022b82fff658d86dcab07aa58c7ddecdfb2452408258f97
7
- data.tar.gz: 2ea218a55b33d8ba6fdb41f8aa3ef7d07443537e482a2a995a7aad2c01fc952c22135263739aa0963d9e0e88a153ffd146ac32e4a9979c5a32ccf218391f66cf
6
+ metadata.gz: 67a7e17e10fe0f75f5a5cad93bd97a7f4f87e136e6dfe6f338b0e8df0e80f83533d8e70b07e218fc0431e3e7c5bbdc2d14f71c8c3d32a98d846f276cb573f225
7
+ data.tar.gz: 2a0cb94a433fe5b69bef58cd4fe24393adaad5bf69e76120b9296a3f8224b2e9a55da10e176bb0fa13e228e3aa57f0836d61fe0ca8116bd973681cc7e458912f
@@ -0,0 +1,3 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: garrettdimon
@@ -0,0 +1,23 @@
1
+ name: Ruby
2
+
3
+ on: [push,pull_request]
4
+
5
+ env:
6
+ CI: true
7
+
8
+ jobs:
9
+ build:
10
+ strategy:
11
+ matrix:
12
+ os: [ubuntu-latest, macos-latest]
13
+ ruby: [2.5.9, 2.6.9, 2.7.5, 3.0.3, 3.1.0-preview1]
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v2
17
+ - name: Set up Ruby
18
+ uses: ruby/setup-ruby@v1
19
+ with:
20
+ ruby-version: ${{ matrix.ruby }}
21
+ bundler-cache: true
22
+ - name: Run Tests
23
+ run: bundle exec rake
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ ## [Unreleased]
2
+
3
+ Nothing at the moment.
4
+
5
+ ## [1.1.0] - 2021-12-09
6
+
7
+ The biggest update is that the slow thresholds are now configurable.
8
+
9
+ - Configurable Thresholds
10
+ - Fixed a bug where `vendor/bundle` gem files were considered project source code files
11
+ - Set up [GitHub Actions](https://github.com/garrettdimon/minitest-heat/actions) to ensure tests are run on Ubuntu latest and macOs for the [latest Ruby versions](https://github.com/garrettdimon/minitest-heat/blob/main/.github/workflows/main.yml)
12
+ - Fixed some tests that were accidentally left path-dependent and couldn't pass on other machines
13
+
14
+ ## [1.0.0] - 2021-12-01
15
+
16
+ Initial release.
17
+
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- minitest-heat (0.0.12)
4
+ minitest-heat (1.1.0)
5
5
  minitest
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -1,12 +1,11 @@
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.
1
+ # 🔥 Minitest::Heat 🔥
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
+ For a more detailed explanation of Minitest Heat with screenshots, [head over the wiki for the full story](https://github.com/garrettdimon/minitest-heat/wiki).
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
+ Or for some additional insight about priorities and how it works, this [Twitter thread](https://twitter.com/garrettdimon/status/1432703746526560266) is a good read.
7
7
 
8
8
  ## Installation
9
-
10
9
  Add this line to your application's Gemfile:
11
10
 
12
11
  ```ruby
@@ -27,27 +26,44 @@ And depending on your usage, you may need to require Minitest Heat in your test
27
26
  require 'minitest/heat'
28
27
  ```
29
28
 
30
- ## Usage
29
+ ## Configuration
30
+ Minitest Heat doesn't currently offer a significant set of configuration options, but the thresholds for "Slow" and "Painfully Slow" tests can be adjusted. By default, it considers anything over 1.0s to be 'slow' and anything over 3.0s to be 'painfully slow'.
31
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.
32
+ You can add a configuration block to your `test_helper.rb` file after the `require 'minitest/heat'` line.
33
33
 
34
- Otherwise, once it's bundled and added to your `test_helper`, it shold "just work" whenever you run your test suite.
34
+ For example:
35
35
 
36
- ## Development
36
+ ```ruby
37
+ Minitest::Heat.configure do |config|
38
+ config.slow_threshold = 0.01
39
+ config.painfully_slow_threshold = 0.5
40
+ end
41
+ ```
37
42
 
43
+ ## Development
38
44
  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
45
 
40
46
  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
47
 
42
- ## Contributing
48
+ ### Forcing Test Failures
49
+ 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.
43
50
 
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).
51
+ ```bash
52
+ IMPLODE=true # Every possible type of failure, skip, and slow is generated
53
+ FORCE_EXCEPTIONS=true # Only exception-triggered failures
54
+ FORCE_FAILURES=true # Only standard assertion failures
55
+ FORCE_SKIPS=true # No errors, just the skipped tests
56
+ FORCE_SLOWS=true # No errors or skipped tests, just slow tests
57
+ ```
45
58
 
59
+ So to see the full context of a test suite, `IMPLODE=true bundle exec rake` will work its magic.
60
+
61
+ ## Contributing
62
+ 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
63
 
47
64
  ## License
48
65
 
49
66
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
50
67
 
51
68
  ## Code of Conduct
52
-
53
69
  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).
@@ -26,7 +26,7 @@ module Minitest
26
26
 
27
27
  # All lines of the backtrace converted to Backtrace::LineParser's
28
28
  #
29
- # @return [Line] the full set of backtrace lines parsed as Backtrace::LineParser instances
29
+ # @return [Array<Location>] the full set of backtrace lines parsed as Location instances
30
30
  def locations
31
31
  return [] if raw_backtrace.nil?
32
32
 
@@ -36,28 +36,28 @@ module Minitest
36
36
  # All entries from the backtrace within the project and sorted with the most recently modified
37
37
  # files at the beginning
38
38
  #
39
- # @return [Line] the sorted backtrace lines from the project parsed as Backtrace::LineParser's
39
+ # @return [Array<Location>] the sorted backtrace lines from the project
40
40
  def recently_modified_locations
41
41
  @recently_modified_locations ||= project_locations.sort_by(&:mtime).reverse
42
42
  end
43
43
 
44
44
  # All entries from the backtrace that are files within the project
45
45
  #
46
- # @return [Line] the backtrace lines from within the project parsed as Backtrace::LineParser's
46
+ # @return [Array<Location>] the backtrace lines from within the project
47
47
  def project_locations
48
48
  @project_locations ||= locations.select(&:project_file?)
49
49
  end
50
50
 
51
51
  # All entries from the backtrace within the project tests
52
52
  #
53
- # @return [Line] the backtrace lines from within the project tests parsed as Backtrace::LineParser's
53
+ # @return [Array<Location>] the backtrace lines from within the tests
54
54
  def test_locations
55
55
  @test_locations ||= project_locations.select(&:test_file?)
56
56
  end
57
57
 
58
58
  # All source code entries from the backtrace (i.e. excluding tests)
59
59
  #
60
- # @return [Line] the backtrace lines from within the source code parsed as Backtrace::LineParser's
60
+ # @return [Array<Location>] the backtrace lines from within the source code
61
61
  def source_code_locations
62
62
  @source_code_locations ||= project_locations.select(&:source_code_file?)
63
63
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module Minitest
6
+ module Heat
7
+ # For managing configuration options on how Minitest Heat should handle results
8
+ class Configuration
9
+ attr_accessor :slow_threshold,
10
+ :painfully_slow_threshold
11
+
12
+ def initialize
13
+ @slow_threshold = 1.0
14
+ @painfully_slow_threshold = 3.0
15
+ end
16
+ end
17
+ end
18
+ end
@@ -116,20 +116,34 @@ 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
+ Minitest::Heat.configuration.slow_threshold || SLOW_THRESHOLDS[:slow]
124
+ end
125
+
126
+ # The number, in seconds, for a test to be considered "painfully slow"
127
+ #
128
+ # @return [Float] number of seconds after which a test is considered painfully slow
129
+ def painfully_slow_threshold
130
+ Minitest::Heat.configuration.painfully_slow_threshold || SLOW_THRESHOLDS[:painful]
131
+ end
132
+
119
133
  # Determines if a test should be considered slow by comparing it to the low end definition of
120
134
  # what is considered slow.
121
135
  #
122
- # @return [Boolean] true if the test took longer to run than `SLOW_THRESHOLDS[:slow]`
136
+ # @return [Boolean] true if the test took longer to run than `slow_threshold`
123
137
  def slow?
124
- execution_time >= SLOW_THRESHOLDS[:slow] && execution_time < SLOW_THRESHOLDS[:painful]
138
+ execution_time >= slow_threshold && execution_time < painfully_slow_threshold
125
139
  end
126
140
 
127
141
  # Determines if a test should be considered painfully slow by comparing it to the high end
128
142
  # definition of what is considered slow.
129
143
  #
130
- # @return [Boolean] true if the test took longer to run than `SLOW_THRESHOLDS[:painful]`
144
+ # @return [Boolean] true if the test took longer to run than `painfully_slow_threshold`
131
145
  def painful?
132
- execution_time >= SLOW_THRESHOLDS[:painful]
146
+ execution_time >= painfully_slow_threshold
133
147
  end
134
148
 
135
149
  # Determines if the issue is an exception that was raised from directly within a test
@@ -131,9 +131,16 @@ module Minitest
131
131
 
132
132
  # Determines if a given file is from the project directory
133
133
  #
134
- # @return [Boolean] true if the file is in the project (source code or test)
134
+ # @return [Boolean] true if the file is in the project (source code or test) but not vendored
135
135
  def project_file?
136
- path.include?(project_root_dir)
136
+ path.include?(project_root_dir) && !bundled_file?
137
+ end
138
+
139
+ # Determines if the file is in the project `vendor/bundle` directory.
140
+ #
141
+ # @return [Boolean] true if the file is in `<project_root>/vendor/bundle
142
+ def bundled_file?
143
+ path.include?("#{project_root_dir}/vendor/bundle")
137
144
  end
138
145
 
139
146
  # Determines if a given file follows the standard approaching to naming test files.
@@ -145,9 +152,9 @@ module Minitest
145
152
 
146
153
  # Determines if a given file is a non-test file from the project directory
147
154
  #
148
- # @return [Boolean] true if the file is in the project but not a test file
155
+ # @return [Boolean] true if the file is in the project but not a test file or vendored file
149
156
  def source_code_file?
150
- project_file? && !test_file?
157
+ project_file? && !test_file? && !bundled_file?
151
158
  end
152
159
 
153
160
  # A safe interface to getting the last modified time for the file in question
@@ -3,7 +3,7 @@
3
3
  module Minitest
4
4
  module Heat
5
5
  class Output
6
- # Builds the collection of tokens for a backtrace when an exception occurs
6
+ # Builds the collection of tokens for displaying a backtrace when an exception occurs
7
7
  class Backtrace
8
8
  DEFAULT_LINE_COUNT = 10
9
9
  DEFAULT_INDENTATION_SPACES = 2
@@ -17,31 +17,47 @@ module Minitest
17
17
  end
18
18
 
19
19
  def tokens
20
- # There could be option to expand and display more than one line of source code for the
21
- # final backtrace line if it might be relevant/helpful?
22
-
23
20
  # Iterate over the selected lines from the backtrace
24
- backtrace_locations.each do |location|
25
- @tokens << backtrace_location_tokens(location)
26
- end
27
-
28
- @tokens
21
+ @tokens = backtrace_locations.map { |location| backtrace_location_tokens(location) }
29
22
  end
30
23
 
24
+ # Determines the number of lines to display from the backtrace.
25
+ #
26
+ # @return [Integer] the number of lines to limit the backtrace to
31
27
  def line_count
28
+ # Defined as a method instead of using the constant directlyr in order to easily support
29
+ # adding options for controlling how many lines are displayed from a backtrace.
30
+ #
31
+ # For example, instead of a fixed number, the backtrace could dynamically calculate how
32
+ # many lines it should displaye in order to get to the origination point. Or it could have
33
+ # a default, but inteligently go back further if the backtrace meets some criteria for
34
+ # displaying more lines.
32
35
  DEFAULT_LINE_COUNT
33
36
  end
34
37
 
35
- # This should probably be smart about what lines are displayed in a backtrace.
36
- # Maybe...
37
- # ...it could intelligently display the full back trace?
38
- # ...only the backtrace from the first/last line of project source?
39
- # ...it behaves a little different when it's a broken test vs. a true exception?
40
- # ...it could be smart about subtly flagging the lines that show up in the heat map frequently?
41
- # ...it could be influenced by a "compact" or "robust" reporter super-style?
42
- # ...it's smart about exceptions that were raised outside of the project?
43
- # ...it's smart about highlighting lines of code differently based on whether it's source code, test code, or external code?
38
+ # A subset of parsed lines from the backtrace.
39
+ #
40
+ # @return [Array<Location>] the backtrace locations determined to be most relevant to the
41
+ # context of the underlying issue
44
42
  def backtrace_locations
43
+ # This could eventually have additional intelligence to determine what lines are most
44
+ # relevant for a given type of issue. For now, it simply takes the line numbers, but the
45
+ # idea is that long-term, it could adjust that on the fly to keep the line count as low
46
+ # as possible but expand it if necessary to ensure enough context is displayed.
47
+ #
48
+ # - If there's no clear cut details about the source of the error from within the project,
49
+ # it could display the entire backtrace without filtering anything.
50
+ # - It could scan the backtrace to the first appearance of project files and then display
51
+ # all of the lines that occurred after that instance
52
+ # - It coudl filter the lines differently whether the issue originated from a test or from
53
+ # the source code.
54
+ # - It could allow supporting a "compact" or "robust" reporter style so that someone on
55
+ # a smaller screen could easily reduce the information shown so that the results could
56
+ # be higher density even if it means truncating some occasionally useful details
57
+ # - It could be smarter about displaying context/guidance when the full backtrace is from
58
+ # outside the project's code
59
+ #
60
+ # But for now. It just grabs some lines.
45
61
  backtrace.locations.take(line_count)
46
62
  end
47
63
 
@@ -65,8 +81,17 @@ module Minitest
65
81
  backtrace_locations.all?(&:project_file?)
66
82
  end
67
83
 
84
+ # Determines if the file referenced by a backtrace line is the most recently modified file
85
+ # of all the files referenced in the visible backtrace locations.
86
+ #
87
+ # @param [Location] location the location to examine
88
+ #
89
+ # @return [<type>] <description>
90
+ #
68
91
  def most_recently_modified?(location)
69
- # If there's more than one line being displayed, and the current line is the freshest
92
+ # If there's more than one line being displayed (otherwise, with one line, of course it's
93
+ # the most recently modified because there_aren't any others) and the current line is the
94
+ # same as the freshest location in the backtrace
70
95
  backtrace_locations.size > 1 && location == locations.freshest
71
96
  end
72
97
 
@@ -14,28 +14,16 @@ module Minitest
14
14
 
15
15
  def tokens
16
16
  case issue.type
17
- when :error then error_tokens
18
- when :broken then broken_tokens
19
- when :failure then failure_tokens
20
- when :skipped then skipped_tokens
21
- when :painful then painful_tokens
22
- when :slow then slow_tokens
17
+ when :error, :broken then exception_tokens
18
+ when :failure then failure_tokens
19
+ when :skipped then skipped_tokens
20
+ when :painful, :slow then slow_tokens
23
21
  end
24
22
  end
25
23
 
26
24
  private
27
25
 
28
- def error_tokens
29
- [
30
- headline_tokens,
31
- test_location_tokens,
32
- summary_tokens,
33
- *backtrace_tokens,
34
- newline_tokens
35
- ]
36
- end
37
-
38
- def broken_tokens
26
+ def exception_tokens
39
27
  [
40
28
  headline_tokens,
41
29
  test_location_tokens,
@@ -62,14 +50,6 @@ module Minitest
62
50
  ]
63
51
  end
64
52
 
65
- def painful_tokens
66
- [
67
- headline_tokens,
68
- slowness_summary_tokens,
69
- newline_tokens
70
- ]
71
- end
72
-
73
53
  def slow_tokens
74
54
  [
75
55
  headline_tokens,
@@ -82,6 +62,11 @@ module Minitest
82
62
  [label_token(issue), spacer_token, [:default, test_name(issue)]]
83
63
  end
84
64
 
65
+ # Creates a display-friendly version of the test name with underscores removed and the
66
+ # first letter capitalized regardless of the formatt used for the test definition
67
+ # @param issue [Issue] the issue to use to generate the test name
68
+ #
69
+ # @return [String] the cleaned up version of the test name
85
70
  def test_name(issue)
86
71
  test_prefix = 'test_'
87
72
  identifier = issue.test_identifier
@@ -97,27 +82,10 @@ module Minitest
97
82
  [issue.type, issue_label(issue.type)]
98
83
  end
99
84
 
100
- def issue_label(issue_type)
101
- case issue_type
102
- when :error then 'Error'
103
- when :broken then 'Broken Test'
104
- when :failure then 'Failure'
105
- when :skipped then 'Skipped'
106
- when :slow then 'Passed but Slow'
107
- when :painful then 'Passed but Very Slow'
108
- when :passed then 'Success'
109
- else 'Unknown'
110
- end
111
- end
112
-
113
85
  def test_name_and_class_tokens
114
86
  [[:default, issue.test_class], *test_location_tokens]
115
87
  end
116
88
 
117
- def backtrace_tokens
118
- @backtrace_tokens ||= ::Minitest::Heat::Output::Backtrace.new(locations).tokens
119
- end
120
-
121
89
  def test_location_tokens
122
90
  [
123
91
  [:default, locations.test_definition.relative_filename],
@@ -176,6 +144,27 @@ module Minitest
176
144
  def arrow_token
177
145
  Output::TOKENS[:muted_arrow]
178
146
  end
147
+
148
+ def backtrace_tokens
149
+ @backtrace_tokens ||= ::Minitest::Heat::Output::Backtrace.new(locations).tokens
150
+ end
151
+
152
+ # The string to use to describe the failure type when displaying results/
153
+ # @param issue_type [Symbol] the symbol representing the issue's failure type
154
+ #
155
+ # @return [String] the display-friendly string describing the failure reason
156
+ def issue_label(issue_type)
157
+ case issue_type
158
+ when :error then 'Error'
159
+ when :broken then 'Broken Test'
160
+ when :failure then 'Failure'
161
+ when :skipped then 'Skipped'
162
+ when :slow then 'Passed but Slow'
163
+ when :painful then 'Passed but Very Slow'
164
+ when :passed then 'Success'
165
+ else 'Unknown'
166
+ end
167
+ end
179
168
  end
180
169
  end
181
170
  end
@@ -14,24 +14,34 @@ module Minitest
14
14
 
15
15
  def tokens
16
16
  results.heat_map.file_hits.each do |hit|
17
- # If there's legitimate failures or errors, skips and slows aren't relevant
17
+ # Focus on the relevant issues based on most significant problems. i.e. If there are
18
+ # legitimate failures or errors, skips and slows aren't relevant
18
19
  next unless relevant_issue_types?(hit)
19
20
 
21
+ # Add a new line
20
22
  @tokens << [[:muted, ""]]
23
+
24
+ # Build the summary line for the file
21
25
  @tokens << file_summary_tokens(hit)
22
26
 
23
- repeats = repeated_line_numbers(hit)
24
- next unless repeats.any?
27
+ # Get the set of line numbers that appear more than once
28
+ repeated_line_numbers = find_repeated_line_numbers_in(hit)
25
29
 
26
- repeats.each do |line_number|
27
- @tokens << [[:muted, " Issues on Line #{line_number} initially triggered from these locations:"]]
30
+ # Only display more details if the same line number shows up more than once
31
+ next unless repeated_line_numbers.any?
28
32
 
33
+ repeated_line_numbers.each do |line_number|
34
+ # Get the backtraces for the given line numbers
29
35
  traces = hit.lines[line_number.to_s]
30
- sorted_traces = traces.sort_by { |trace| trace.locations.last.line_number }
31
36
 
32
- sorted_traces.each do |trace|
33
- @tokens << origination_location_token(trace)
34
- end
37
+ # If there aren't any traces there's no way to provide additional details
38
+ break unless traces.any?
39
+
40
+ # A short summary explaining the details that will follow
41
+ @tokens << [[:default, " Line #{line_number}"], [:muted, ' issues triggered from:']]
42
+
43
+ # The last relevant location for each error's backtrace
44
+ @tokens += origination_sources(traces)
35
45
  end
36
46
  end
37
47
 
@@ -40,16 +50,28 @@ module Minitest
40
50
 
41
51
  private
42
52
 
53
+ def origination_sources(traces)
54
+ # 1. Only pull the traces that have proper locations
55
+ # 2. Sort the traces by the most recent line number so they're displayed in numeric order
56
+ # 3. Get the final relevant location from the trace
57
+ traces.
58
+ select { |trace| trace.locations.any? }.
59
+ sort_by { |trace| trace.locations.last.line_number }.
60
+ map { |trace| origination_location_token(trace) }
61
+ end
62
+
43
63
  def file_summary_tokens(hit)
44
64
  pathname_tokens = pathname(hit)
45
- line_number_list_tokens = sorted_line_number_list(hit)
65
+ line_number_list_tokens = line_number_tokens_for_hit(hit)
46
66
 
47
67
  [*pathname_tokens, *line_number_list_tokens]
48
68
  end
49
69
 
50
70
  def origination_location_token(trace)
51
71
  # The earliest project line from the backtrace—this is probabyl wholly incorrect in terms
52
- # of what would be the most helpful line to display, but it's a start.
72
+ # of what would be the most helpful line to display, but it's a start. Otherwise, the
73
+ # logic will need to compare all traces for the issue and find the unique origination
74
+ # lines
53
75
  location = trace.locations.last
54
76
 
55
77
  [
@@ -75,16 +97,18 @@ module Minitest
75
97
  end
76
98
 
77
99
  def relevant_issue_types?(hit)
100
+ # The intersection of which issue types are relevant based on the context and which issues
101
+ # matc those issue types
78
102
  intersection_issue_types = relevant_issue_types & hit.issues.keys
79
103
 
80
104
  intersection_issue_types.any?
81
105
  end
82
106
 
83
- def repeated_line_numbers(hit)
107
+ def find_repeated_line_numbers_in(hit)
84
108
  repeated_line_numbers = []
85
109
 
86
110
  hit.lines.each_pair do |line_number, traces|
87
- # If there aren't multiple traces for a line number, it's not a repeat, right?
111
+ # If there aren't multiple traces for a line number, it's not a repeat
88
112
  next unless traces.size > 1
89
113
 
90
114
  repeated_line_numbers << Integer(line_number)
@@ -93,10 +117,6 @@ module Minitest
93
117
  repeated_line_numbers.sort
94
118
  end
95
119
 
96
- def repeated_line_numbers?(hit)
97
- repeated_line_numbers(hit).any?
98
- end
99
-
100
120
  def pathname(hit)
101
121
  directory = hit.pathname.dirname.to_s.delete_prefix("#{Dir.pwd}/")
102
122
  filename = hit.pathname.basename.to_s
@@ -108,6 +128,11 @@ module Minitest
108
128
  ]
109
129
  end
110
130
 
131
+ # Gets the list of line numbers for a given hit location (i.e. file) so they can be
132
+ # displayed after the file name to show which lines were problematic
133
+ # @param hit [Hit] the instance to extract line numbers from
134
+ #
135
+ # @return [Array<Symbol,String>] [description]
111
136
  def line_number_tokens_for_hit(hit)
112
137
  line_number_tokens = []
113
138
 
@@ -116,34 +141,50 @@ module Minitest
116
141
  line_numbers_for_issue_type = hit.issues.fetch(issue_type) { [] }
117
142
 
118
143
  # Build the list of tokens representing styled line numbers
119
- line_numbers_for_issue_type.each do |line_number|
120
- line_number_tokens << line_number_token(issue_type, line_number)
144
+ line_numbers_for_issue_type.uniq.sort.each do |line_number|
145
+ frequency = line_numbers_for_issue_type.count(line_number)
146
+
147
+ line_number_tokens += line_number_token(issue_type, line_number, frequency)
121
148
  end
122
149
  end
123
150
 
124
151
  line_number_tokens.compact
125
152
  end
126
153
 
127
- def line_number_token(style, line_number)
128
- [style, "#{line_number} "]
129
- end
130
-
131
- # Generates the line number tokens styled based on their error type
154
+ # Builds a token representing a styled line number
132
155
  #
133
- # @param [Hit] hit the instance of the hit file details to build the heat map entry
156
+ # @param style [Symbol] the relevant display style for the issue
157
+ # @param line_number [Integer] the affected line number
134
158
  #
135
- # @return [Array] the arrays representing the line number tokens to display next to a file
136
- # name in the heat map
137
- def sorted_line_number_list(hit)
138
- # Sort the collected group of line number hits so they're in order
139
- line_number_tokens_for_hit(hit).sort do |a, b|
140
- # Ensure the line numbers are integers for sorting (otherwise '100' comes before '12')
141
- first_line_number = Integer(a[1].strip)
142
- second_line_number = Integer(b[1].strip)
143
-
144
- first_line_number <=> second_line_number
159
+ # @return [Array<Symbol,Integer>] array token representing the line number and issue type
160
+ def line_number_token(style, line_number, frequency)
161
+ if frequency > 1
162
+ [
163
+ [style, "#{line_number}"],
164
+ [:muted, "✕#{frequency} "]
165
+ ]
166
+ else
167
+ [[style, "#{line_number} "]]
145
168
  end
146
169
  end
170
+
171
+ # # Sorts line number tokens so that line numbers are displayed in order regardless of their
172
+ # # underlying issue type
173
+ # #
174
+ # # @param hit [Hit] the instance of the hit file details to build the heat map entry
175
+ # #
176
+ # # @return [Array] the arrays representing the line number tokens to display next to a file
177
+ # # name in the heat map. ex [[:error, 12], [:falure, 13]]
178
+ # def sorted_line_number_list(hit)
179
+ # # Sort the collected group of line number hits so they're in order
180
+ # line_number_tokens_for_hit(hit).sort do |a, b|
181
+ # # Ensure the line numbers are integers for sorting (otherwise '100' comes before '12')
182
+ # first_line_number = Integer(a[1].strip)
183
+ # second_line_number = Integer(b[1].strip)
184
+
185
+ # first_line_number <=> second_line_number
186
+ # end
187
+ # end
147
188
  end
148
189
  end
149
190
  end
@@ -48,13 +48,20 @@ module Minitest
48
48
  newline
49
49
 
50
50
  # Issues start with the least critical and go up to the most critical so that the most
51
- # pressing issues are displayed at the bottom of the report in order to reduce scrolling.
52
- # This way, as you fix issues, the list gets shorter, and eventually the least critical
53
- # issues will be displayed without scrolling once more problematic issues are resolved.
51
+ # pressing issues are displayed at the bottom of the report in order to reduce scrolling.
52
+ #
53
+ # This way, as you fix issues, the list gets shorter, and eventually the least critical
54
+ # issues will be displayed without scrolling once more problematic issues are resolved.
54
55
  %i[slows painfuls skips failures brokens errors].each do |issue_category|
56
+ # Only show categories for the most pressing issues after the suite runs, otherwise,
57
+ # suppress them until the more critical issues are resolved.
55
58
  next unless show?(issue_category, results)
56
59
 
57
- results.send(issue_category).each { |issue| issue_details(issue) }
60
+ issues = results.send(issue_category)
61
+
62
+ issues
63
+ .sort_by { |issue| issue.locations.most_relevant.to_a }
64
+ .each { |issue| issue_details(issue) }
58
65
  end
59
66
  rescue StandardError => e
60
67
  message = "Sorry, but Minitest Heat couldn't display the details of any failures."
@@ -64,7 +71,7 @@ module Minitest
64
71
  def issue_details(issue)
65
72
  print_tokens Minitest::Heat::Output::Issue.new(issue).tokens
66
73
  rescue StandardError => e
67
- message = "Sorry, but Minitest Heat couldn't display output for a failure."
74
+ message = "Sorry, but Minitest Heat couldn't display output for a specific failure."
68
75
  exception_guidance(message, e)
69
76
  end
70
77
 
@@ -89,6 +96,15 @@ module Minitest
89
96
  exception_guidance(message, e)
90
97
  end
91
98
 
99
+ private
100
+
101
+ # Displays some guidance related to exceptions generated by Minitest Heat in order to help
102
+ # people get back on track (and ideally submit issues)
103
+ # @param message [String] a slightly more specific explanation of which part of minitest-heat
104
+ # caused the failure
105
+ # @param exception [Exception] the exception that caused the problem
106
+ #
107
+ # @return [void] displays the guidance to the console
92
108
  def exception_guidance(message, exception)
93
109
  newline
94
110
  puts "#{message} Disabling Minitest Heat can get you back on track until the problem can be fixed."
@@ -100,8 +116,6 @@ module Minitest
100
116
  newline
101
117
  end
102
118
 
103
- private
104
-
105
119
  def no_problems?(results)
106
120
  !results.problems?
107
121
  end
@@ -30,8 +30,20 @@ module Minitest
30
30
  # For heat map purposes, only the project backtrace lines are interesting
31
31
  pathname, line_number = issue.locations.project.to_a
32
32
 
33
- # Backtrace is only relevant for exception-generating issues, not slows, skips, or failures
34
- backtrace = issue.error? ? issue.locations.backtrace.project_locations : []
33
+ # A backtrace is only relevant for exception-generating issues (i.e. errors), not slows or skips
34
+ # However, while assertion failures won't have a backtrace, there can still be repeated line
35
+ # numbers if the tests reference a shared method with an assertion in it. So in those cases,
36
+ # the backtrace is simply the test definition
37
+ backtrace = if issue.error?
38
+ # With errors, we have a backtrace
39
+ issue.locations.backtrace.project_locations
40
+ else
41
+ # With failures, the test definition is the most granular backtrace equivalent
42
+ location = issue.locations.test_definition
43
+ location.raw_container = issue.test_identifier
44
+
45
+ [location]
46
+ end
35
47
 
36
48
  @heat_map.add(pathname, line_number, issue.type, backtrace: backtrace)
37
49
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Minitest
4
4
  module Heat
5
- VERSION = '0.0.12'
5
+ VERSION = '1.1.0'
6
6
  end
7
7
  end
data/lib/minitest/heat.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'heat/configuration'
3
4
  require_relative 'heat/backtrace'
4
5
  require_relative 'heat/hit'
5
6
  require_relative 'heat/issue'
@@ -32,5 +33,20 @@ module Minitest
32
33
  # Pulls from minitest-color as well:
33
34
  # https://github.com/teoljungberg/minitest-color/blob/master/lib/minitest/color_plugin.rb
34
35
  module Heat
36
+ class << self
37
+ attr_writer :configuration
38
+ end
39
+
40
+ def self.configuration
41
+ @configuration ||= Configuration.new
42
+ end
43
+
44
+ def self.reset
45
+ @configuration = Configuration.new
46
+ end
47
+
48
+ def self.configure
49
+ yield(configuration)
50
+ end
35
51
  end
36
52
  end
@@ -96,7 +96,7 @@ module Minitest
96
96
  # The list of individual issues and their associated details
97
97
  output.issues_list(results)
98
98
 
99
- # Display a short summary of the total issue counts fore ach category as well as performance
99
+ # Display a short summary of the total issue counts for each category as well as performance
100
100
  # details for the test suite as a whole
101
101
  output.compact_summary(results, timer)
102
102
 
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.12
4
+ version: 1.1.0
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-11-15 00:00:00.000000000 Z
11
+ date: 2021-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -130,9 +130,12 @@ executables: []
130
130
  extensions: []
131
131
  extra_rdoc_files: []
132
132
  files:
133
+ - ".github/FUNDING.yml"
134
+ - ".github/workflows/main.yml"
133
135
  - ".gitignore"
134
136
  - ".rubocop.yml"
135
137
  - ".travis.yml"
138
+ - CHANGELOG.md
136
139
  - CODE_OF_CONDUCT.md
137
140
  - Gemfile
138
141
  - Gemfile.lock
@@ -144,6 +147,7 @@ files:
144
147
  - lib/minitest/heat.rb
145
148
  - lib/minitest/heat/backtrace.rb
146
149
  - lib/minitest/heat/backtrace/line_parser.rb
150
+ - lib/minitest/heat/configuration.rb
147
151
  - lib/minitest/heat/hit.rb
148
152
  - lib/minitest/heat/issue.rb
149
153
  - lib/minitest/heat/location.rb