minitest-heat 0.0.1 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 949b2b45d4b42493bc74bc98571eefbbde523ef18a70077cc32bf944e30cc172
4
- data.tar.gz: e716f191d2053ed286ed8e342e0578fe9bc97ad818d4ca89dbf199003d6b64d0
3
+ metadata.gz: c760c15a45811d5773dee18e32aadc063ed61c3eb04447c637719d5d3d46178a
4
+ data.tar.gz: 90c345c56cbd2589719eef8a0264a5973f77a8104c91149f4e4ad2320067a49a
5
5
  SHA512:
6
- metadata.gz: f662e4944204010df9e2f29ab49959b978f7b0c54e5beb9ece51e80bf8830d7c6cf988f5834dbfc3b44037f6303ac5a82817132810a508ff1fa7064e19800a74
7
- data.tar.gz: 292ecef2bd8473f74937356d8c68171801d4eb50e67849d76160fb24c9ee61838183c3c385240425ad1587c4cedf7375e292f806d593706915175cd489702520
6
+ metadata.gz: 47be296804a2171023bfcdb5c4192376698349bd76c4c3a2991d9accd9f631b88d2ffb867d821f2479c41bd6fd2cf919af325ecde975f98ee4592277b3b09432
7
+ data.tar.gz: 79a180260e9a1d8c55d898883f13969f2576ceb6450dc9e080050e55674c73fc7979202a2c8a78542958491fce91fee2a3cb1b66fd2705edc4a8d432f5e28fcc
data/Gemfile.lock CHANGED
@@ -1,13 +1,14 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- minitest-heat (0.0.1)
4
+ minitest-heat (0.0.5)
5
5
  minitest
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
10
  coderay (1.1.3)
11
+ dead_end (1.1.7)
11
12
  docile (1.4.0)
12
13
  method_source (1.0.0)
13
14
  minitest (5.14.4)
@@ -26,6 +27,7 @@ PLATFORMS
26
27
  ruby
27
28
 
28
29
  DEPENDENCIES
30
+ dead_end
29
31
  minitest (~> 5.0)
30
32
  minitest-heat!
31
33
  pry
@@ -6,11 +6,23 @@ module Minitest
6
6
  class Backtrace
7
7
  Line = Struct.new(:path, :file, :number, :container, :mtime, keyword_init: true) do
8
8
  def to_s
9
- "#{path}/#{file}:#{line} in `#{container}` #{age_in_seconds}"
9
+ "#{location} in `#{container}`"
10
10
  end
11
11
 
12
- def to_file
13
- "#{path}/#{file}"
12
+ def pathname
13
+ Pathname("#{path}/#{file}")
14
+ end
15
+
16
+ def location
17
+ "#{pathname.to_s}:#{number}"
18
+ end
19
+
20
+ def short_pathname
21
+ pathname.delete_prefix(Dir.pwd)
22
+ end
23
+
24
+ def short_location
25
+ "#{pathname.basename.to_s}:#{number}"
14
26
  end
15
27
 
16
28
  def age_in_seconds
@@ -18,6 +30,16 @@ module Minitest
18
30
  end
19
31
  end
20
32
 
33
+ UNREADABLE_FILE_ATTRIBUTES = {
34
+ path: '(Unknown Path)',
35
+ file: '(Unknown File)',
36
+ number: '(Unknown Line Number)',
37
+ container: '(Unknown Method)',
38
+ mtime: '(Unknown Modification Time)'
39
+ }
40
+
41
+ UNREADABLE_LINE = Line.new(UNREADABLE_FILE_ATTRIBUTES)
42
+
21
43
  attr_reader :raw_backtrace
22
44
 
23
45
  def initialize(raw_backtrace)
@@ -29,37 +51,45 @@ module Minitest
29
51
  end
30
52
 
31
53
  def final_location
32
- parsed.first
54
+ parsed_lines.first
33
55
  end
34
56
 
35
57
  def final_project_location
36
- project.first
58
+ project_lines.first
59
+ end
60
+
61
+ def freshest_project_location
62
+ recently_modified_lines.first
63
+ end
64
+
65
+ def final_source_code_location
66
+ source_code_lines.first
37
67
  end
38
68
 
39
69
  def final_test_location
40
- tests.first
70
+ test_lines.first
41
71
  end
42
72
 
43
- def freshest_project_location
44
- recently_modified.first
73
+ def project_lines
74
+ @project_lines ||= parsed_lines.select { |line| line[:path].include?(Dir.pwd) }
45
75
  end
46
76
 
47
- def project
48
- @project ||= parsed.select { |line| line[:path].include?(Dir.pwd) }
77
+ def recently_modified_lines
78
+ @recently_modified_lines ||= project_lines.sort_by { |line| line[:mtime] }.reverse
49
79
  end
50
80
 
51
- def tests
52
- @tests ||= project.select { |line| test_file?(line) }
81
+ def test_lines
82
+ @tests_lines ||= project_lines.select { |line| test_file?(line) }
53
83
  end
54
84
 
55
- def recently_modified
56
- @recently_modified ||= project.sort_by { |line| line[:mtime] }.reverse
85
+ def source_code_lines
86
+ @source_code_lines ||= project_lines - test_lines
57
87
  end
58
88
 
59
- def parsed
89
+ def parsed_lines
60
90
  return [] if raw_backtrace.nil?
61
91
 
62
- @parsed ||= raw_backtrace.map { |line| parse(line) }
92
+ @parsed_lines ||= raw_backtrace.map { |line| parse(line) }
63
93
  end
64
94
 
65
95
  private
@@ -83,6 +113,8 @@ module Minitest
83
113
  container: reduce_container(parts[2]),
84
114
  mtime: pathname.exist? ? pathname.mtime : nil
85
115
  }
116
+ rescue
117
+ UNREADABLE_FILE_ATTRIBUTES
86
118
  end
87
119
 
88
120
  def test_file?(line)
@@ -8,7 +8,20 @@ module Minitest
8
8
  class Issue
9
9
  extend Forwardable
10
10
 
11
- SLOW_THRESHOLD = 0.5
11
+ SLOW_THRESHOLDS = {
12
+ slow: 1.0,
13
+ painful: 3.0
14
+ }
15
+
16
+ MARKERS = {
17
+ success: '·',
18
+ slow: '–',
19
+ painful: '—',
20
+ broken: 'B',
21
+ error: 'E',
22
+ skipped: 'S',
23
+ failure: 'F',
24
+ }
12
25
 
13
26
  SHARED_SYMBOLS = {
14
27
  spacer: ' · ',
@@ -27,10 +40,14 @@ module Minitest
27
40
  @location = Location.new(result.source_location, @failure&.backtrace)
28
41
  end
29
42
 
43
+ def short_location
44
+ location.to_s.delete_prefix(Dir.pwd)
45
+ end
46
+
30
47
  def to_hit
31
48
  [
32
- location.source_file,
33
- location.source_failure_line,
49
+ location.project_file.to_s,
50
+ location.project_failure_line,
34
51
  type
35
52
  ]
36
53
  end
@@ -52,6 +69,8 @@ module Minitest
52
69
  :skipped
53
70
  elsif !passed?
54
71
  :failure
72
+ elsif painful?
73
+ :painful
55
74
  elsif slow?
56
75
  :slow
57
76
  else
@@ -59,20 +78,28 @@ module Minitest
59
78
  end
60
79
  end
61
80
 
81
+ def hit?
82
+ !passed? || slow?
83
+ end
84
+
62
85
  def slow?
63
- time > SLOW_THRESHOLD
86
+ time >= SLOW_THRESHOLDS[:slow]
87
+ end
88
+
89
+ def painful?
90
+ time >= SLOW_THRESHOLDS[:painful]
64
91
  end
65
92
 
66
93
  def in_test?
67
- location.failure_in_test?
94
+ location.broken_test?
68
95
  end
69
96
 
70
97
  def in_source?
71
- location.failure_in_source?
98
+ location.proper_failure?
72
99
  end
73
100
 
74
101
  def test_class
75
- result.klass.delete_prefix('Minitest::')
102
+ result.klass
76
103
  end
77
104
 
78
105
  def test_identifier
@@ -100,7 +127,7 @@ module Minitest
100
127
  # When the exception came out of the test itself, that's a different kind of exception
101
128
  # that really only indicates there's a problem with the code in the test. It's kind of
102
129
  # between an error and a test.
103
- 'Broken Test'
130
+ 'Test Error'
104
131
  elsif error? || !passed?
105
132
  failure.result_label
106
133
  elsif slow?
@@ -111,14 +138,7 @@ module Minitest
111
138
  end
112
139
 
113
140
  def marker
114
- case type
115
- when :broken then 'B'
116
- when :error then 'E'
117
- when :skipped then 'S'
118
- when :failure then 'F'
119
- when :slow then 'S'
120
- else '.'
121
- end
141
+ MARKERS.fetch(type.to_sym)
122
142
  end
123
143
 
124
144
  def summary
@@ -2,6 +2,17 @@
2
2
 
3
3
  module Minitest
4
4
  module Heat
5
+ # Convenience methods for determining the file and line number where the problem occurred.
6
+ # There are several layers of specificity to help make it easy to communicate the relative
7
+ # location of the failure:
8
+ # - 'final' represents the final line of the backtrace regardless of where it is
9
+ # - 'test' represents the last line from the project's tests. It is further differentiated by
10
+ # the line where the test is defined and the actual line of code in the test that geneated
11
+ # the failure or exception
12
+ # - 'source_code' represents the last line from the project's source code
13
+ # - 'project' represents the last source line, but falls back to the last test line
14
+ # - 'most_relevant' represents the most specific file to investigate starting with the source
15
+ # code and then looking to the test code with final line of the backtrace as a fallback
5
16
  class Location
6
17
  attr_reader :test_location, :backtrace
7
18
 
@@ -10,52 +21,152 @@ module Minitest
10
21
  @backtrace = Backtrace.new(backtrace)
11
22
  end
12
23
 
24
+ # Prints the pathname and line number of the location most likely to be the source of the
25
+ # test failure
26
+ #
27
+ # @return [String] ex. 'path/to/file.rb:12'
13
28
  def to_s
14
- "#{source_file}:#{source_failure_line}"
29
+ "#{most_relevant_file}:#{most_relevant_failure_line}"
15
30
  end
16
31
 
17
- def failure_in_test?
18
- !test_file.nil? && test_file == source_file
32
+ def local?
33
+ broken_test? || proper_failure?
19
34
  end
20
35
 
21
- def failure_in_source?
22
- !failure_in_test?
36
+ # Knows if the failure is contained within the test. For example, if there's bad code in a
37
+ # test, and it raises an exception, then it's really a broken test rather than a proper
38
+ # faiure.
39
+ #
40
+ # @return [Boolean] true if most relevant file is the same as the test location file
41
+ def broken_test?
42
+ !test_file.nil? && test_file == most_relevant_file
23
43
  end
24
44
 
45
+ # Knows if the failure occurred in the actual project source code—as opposed to the test or
46
+ # an external piece of code like a gem.
47
+ #
48
+ # @return [Boolean] true if there's a non-test project file in the stacktrace but it's not
49
+ # a result of a broken test
50
+ def proper_failure?
51
+ !source_code_file.nil? && !broken_test?
52
+ end
53
+
54
+ # The file most likely to be the source of the underlying problem. Often, the most recent
55
+ # backtrace files will be a gem or external library that's failing indirectly as a result
56
+ # of a problem with local source code (not always, but frequently). In that case, the best
57
+ # first place to focus is on the code you control.
58
+ #
59
+ # @return [String] the relative path to the file from the project root
60
+ def most_relevant_file
61
+ Pathname(most_relevant_location[0])
62
+ end
63
+
64
+ # The line number of the `most_relevant_file` where the failure originated
65
+ #
66
+ # @return [Integer] line number
67
+ def most_relevant_failure_line
68
+ most_relevant_location[1]
69
+ end
70
+
71
+ # The final location of the stacktrace regardless of whether it's from within the project
72
+ #
73
+ # @return [String] the relative path to the file from the project root
74
+ def final_file
75
+ Pathname(final_location[0])
76
+ end
77
+
78
+ # The line number of the `final_file` where the failure originated
79
+ #
80
+ # @return [Integer] line number
81
+ def final_failure_line
82
+ final_location[1]
83
+ end
84
+
85
+ # The final location of the stacktrace regardless of whether it's from within the project
86
+ #
87
+ # @return [String] the relative path to the file from the project root
88
+ def project_file
89
+ broken_test? ? test_file : source_code_file
90
+ end
91
+
92
+ # The line number of the `project_file` where the failure originated
93
+ #
94
+ # @return [Integer] line number
95
+ def project_failure_line
96
+ broken_test? ? test_failure_line || test_definition_line : source_code_failure_line
97
+ end
98
+
99
+ # The final location from the stacktrace that is within the project directory
100
+ #
101
+ # @return [String, nil] the relative path to the file from the project root
102
+ def source_code_file
103
+ return nil unless backtrace.source_code_lines.any?
104
+
105
+ backtrace.final_source_code_location.pathname
106
+ end
107
+
108
+ # The line number of the `source_code_file` where the failure originated
109
+ #
110
+ # @return [Integer] line number
111
+ def source_code_failure_line
112
+ return nil unless backtrace.source_code_lines.any?
113
+
114
+ backtrace.final_source_code_location.number
115
+ end
116
+
117
+ # The final location from the stacktrace that is within the project's test directory
118
+ #
119
+ # @return [String, nil] the relative path to the file from the project root
25
120
  def test_file
26
- reduced_path(test_location[0])
121
+ Pathname(test_location[0])
27
122
  end
28
123
 
124
+ # The line number of the `test_file` where the test is defined
125
+ #
126
+ # @return [Integer] line number
29
127
  def test_definition_line
30
128
  test_location[1].to_s
31
129
  end
32
130
 
131
+ # The line number from within the `test_file` test definition where the failure occurred
132
+ #
133
+ # @return [Integer] line number
33
134
  def test_failure_line
34
- @backtrace.final_test_location.number
135
+ backtrace.final_test_location&.number || test_definition_line
35
136
  end
36
137
 
37
- def source_file
38
- return test_file if backtrace.empty?
39
-
40
- source_line = backtrace.final_project_location
41
-
42
- reduced_path("#{source_line.path}/#{source_line.file}")
138
+ # The line number from within the `test_file` test definition where the failure occurred
139
+ #
140
+ # @return [Location] the last location from the backtrace or the test location if a backtrace
141
+ # was not passed to the initializer
142
+ def final_location
143
+ backtrace? ? backtrace.final_location : test_location
43
144
  end
44
145
 
45
- def source_failure_line
46
- return test_definition_line if backtrace.empty?
47
-
48
- backtrace.final_project_location.number
146
+ # The file most likely to be the source of the underlying problem. Often, the most recent
147
+ # backtrace files will be a gem or external library that's failing indirectly as a result
148
+ # of a problem with local source code (not always, but frequently). In that case, the best
149
+ # first place to focus is on the code you control.
150
+ #
151
+ # @return [Array] file and line number of the most likely source of the problem
152
+ def most_relevant_location
153
+ [
154
+ source_code_location,
155
+ test_location,
156
+ final_location
157
+ ].compact.first
49
158
  end
50
159
 
51
- def project_directory_name
52
- Dir.pwd.split('/').last
160
+ def project_location
161
+ source_code_location || test_location
53
162
  end
54
163
 
55
- private
164
+ def source_code_location
165
+ backtrace.final_source_code_location
166
+ end
56
167
 
57
- def reduced_path(path)
58
- "/#{path.split("/#{project_directory_name}/").last}"
168
+ def backtrace?
169
+ backtrace.parsed_lines.any?
59
170
  end
60
171
  end
61
172
  end
@@ -13,11 +13,12 @@ module Minitest
13
13
  # test is broken (i.e. raising an exception), that's a special sort of failure that would be
14
14
  # misleading. It doesn't represent a proper failure, but rather a test that doesn't work.
15
15
  WEIGHTS = {
16
- error: 3, # exceptions from source code have the highest liklihood of a ripple effect
17
- broken: 1, # broken tests won't have ripple effects but can't help if they can't run
18
- failure: 1, # failures are kind of the whole point, and they could have ripple effects
19
- skipped: 0, # skips aren't failures, but they shouldn't go ignored
20
- slow: 0 # slow tests aren't failures, but they shouldn't be ignored
16
+ error: 3, # exceptions from source code have the highest liklihood of a ripple effect
17
+ broken: 1, # broken tests won't have ripple effects but can't help if they can't run
18
+ failure: 1, # failures are kind of the whole point, and they could have ripple effects
19
+ skipped: 0, # skips aren't failures, but they shouldn't go ignored
20
+ painful: 0, # slow tests aren't failures, but they shouldn't be ignored
21
+ slow: 0,
21
22
  }
22
23
 
23
24
  def initialize
@@ -46,7 +47,7 @@ module Minitest
46
47
  files = {}
47
48
  @hits.each_pair do |filename, details|
48
49
  # Can't really be a "hot spot" with just a single issue
49
- next unless details[:total] > 1
50
+ # next unless details[:weight] > 1
50
51
 
51
52
  files[filename] = details[:weight]
52
53
  end