minitest-heat 0.0.1 → 0.0.5

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: 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