minitest-heat 0.0.10 → 0.0.11

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: 6c60c717971a6a1858de4337247b8bfd37f63a63f31a1783de4ecaaad8ca5a8c
4
- data.tar.gz: c491cb526bd8a243810fe88da7d15679a6c5182b256a5187db4c457bcc9b7a7a
3
+ metadata.gz: 7a2dd8d434e13e9719addb1a2afa9f25c751ca6e5712c752244afb1823ac4740
4
+ data.tar.gz: 722f33b1145d79510f9007b54663198972c1ce4461a7a79c14a7bfdeb190721e
5
5
  SHA512:
6
- metadata.gz: abf3a4c476c1041f75fedfa3eac1235f134bc99bbbf1c909e6e407e461380e9028079e3ad927180d7bec4f039c54247172c9a12bee579dec5eab53b444cf3f68
7
- data.tar.gz: fb7d2373153fdaaf6fe8b3a6ec693baf3e04a8fa8dc5636f75d1b0afb45e5a7b25365f3dbe44b0aa3c53bb79b8ed7b0551c3798ceaa3477ce1d9e122159e116a
6
+ metadata.gz: 955b848541141572e94aeff4b02586253f70db8e313af111e54b91c3e8a64c7c44c0f0f90c672f699f9296dc5f691ea04c7852eb948f6ab680173b8261512eee
7
+ data.tar.gz: 9c3085267bea53a6734a085c982fd8348442715036ccf2b636de9c997f3dc14662e25a709faab1b4425ffb46ceeb109f43672336fadf0bca7c4e0cbbccd0463c
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- minitest-heat (0.0.10)
4
+ minitest-heat (0.0.11)
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
@@ -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,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'backtrace/line'
3
+ require_relative 'backtrace/line_parser'
4
4
 
5
5
  module Minitest
6
6
  module Heat
@@ -9,7 +9,7 @@ module Minitest
9
9
  attr_reader :raw_backtrace
10
10
 
11
11
  # Creates a more flexible backtrace data structure by parsing the lines of the backtrace to
12
- # extract individual elements for investigating the offending files and line numbers
12
+ # extract specific subsets of lines for investigating the offending files and line numbers
13
13
  # @param raw_backtrace [Array] the array of lines from the backtrace
14
14
  #
15
15
  # @return [self]
@@ -24,80 +24,42 @@ module Minitest
24
24
  raw_backtrace.empty?
25
25
  end
26
26
 
27
- # The final location exposed in the backtrace. Could be a line from the project or from a
28
- # dependency or the Ruby core libraries
27
+ # All lines of the backtrace converted to Backtrace::LineParser's
29
28
  #
30
- # @return [Line] the final location from the backtrace parsed as a Backtrace::Line
31
- def final_location
32
- parsed_entries.first
33
- end
34
-
35
- # The final location from within the project exposed in the backtrace. Could be test files or
36
- # source code files
37
- #
38
- # @return [Line] the final project location from the backtrace parsed as a Backtrace::Line
39
- def final_project_location
40
- project_entries.first
41
- end
42
-
43
- # The most recently modified location from within the project
44
- #
45
- # @return [Line] the most recently modified project location from the backtrace parsed as a
46
- # Backtrace::Line
47
- def freshest_project_location
48
- recently_modified_entries.first
49
- end
29
+ # @return [Line] the full set of backtrace lines parsed as Backtrace::LineParser instances
30
+ def locations
31
+ return [] if raw_backtrace.nil?
50
32
 
51
- # The final location from within the project source code (i.e. excluding tests)
52
- #
53
- # @return [Line] the final source code location from the backtrace parsed as a Backtrace::Line
54
- def final_source_code_location
55
- source_code_entries.first
33
+ @locations ||= raw_backtrace.map { |entry| Backtrace::LineParser.read(entry) }
56
34
  end
57
35
 
58
- # The final location from within the project's tests (i.e. excluding source code)
36
+ # All entries from the backtrace within the project and sorted with the most recently modified
37
+ # files at the beginning
59
38
  #
60
- # @return [Line] the final test location from the backtrace parsed as a Backtrace::Line
61
- def final_test_location
62
- test_entries.first
39
+ # @return [Line] the sorted backtrace lines from the project parsed as Backtrace::LineParser's
40
+ def recently_modified_locations
41
+ @recently_modified_locations ||= project_locations.sort_by(&:mtime).reverse
63
42
  end
64
43
 
65
44
  # All entries from the backtrace that are files within the project
66
45
  #
67
- # @return [Line] the backtrace lines from within the project parsed as Backtrace::Line's
68
- def project_entries
69
- @project_entries ||= parsed_entries.select { |entry| entry.path.to_s.include?(Dir.pwd) }
70
- end
71
-
72
- # All entries from the backtrace within the project and sorted with the most recently modified
73
- # files at the beginning
74
- #
75
- # @return [Line] the sorted backtrace lines from the project parsed as Backtrace::Line's
76
- def recently_modified_entries
77
- @recently_modified_entries ||= project_entries.sort_by(&:mtime).reverse
46
+ # @return [Line] the backtrace lines from within the project parsed as Backtrace::LineParser's
47
+ def project_locations
48
+ @project_locations ||= locations.select(&:project_file?)
78
49
  end
79
50
 
80
51
  # All entries from the backtrace within the project tests
81
52
  #
82
- # @return [Line] the backtrace lines from within the project tests parsed as Backtrace::Line's
83
- def test_entries
84
- @test_entries ||= project_entries.select(&:test_file?)
53
+ # @return [Line] the backtrace lines from within the project tests parsed as Backtrace::LineParser's
54
+ def test_locations
55
+ @test_locations ||= project_locations.select(&:test_file?)
85
56
  end
86
57
 
87
58
  # All source code entries from the backtrace (i.e. excluding tests)
88
59
  #
89
- # @return [Line] the backtrace lines from within the source code parsed as Backtrace::Line's
90
- def source_code_entries
91
- @source_code_entries ||= project_entries - test_entries
92
- end
93
-
94
- # All lines of the backtrace converted to Backtrace::Line's
95
- #
96
- # @return [Line] the full set of backtrace lines parsed as Backtrace::Line instances
97
- def parsed_entries
98
- return [] if raw_backtrace.nil?
99
-
100
- @parsed_entries ||= raw_backtrace.map { |entry| Backtrace::Line.parse_backtrace(entry) }
60
+ # @return [Line] the backtrace lines from within the source code parsed as Backtrace::LineParser's
61
+ def source_code_locations
62
+ @source_code_locations ||= project_locations.select(&:source_code_file?)
101
63
  end
102
64
  end
103
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 to build a heat map of the affected files
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,7 +25,7 @@ module Minitest
23
25
  slow: 0
24
26
  }.freeze
25
27
 
26
- attr_reader :pathname, :issues
28
+ attr_reader :pathname, :issues, :lines
27
29
 
28
30
  # Creates an instance of a Hit for the given pathname. It must be the full pathname to
29
31
  # uniquely identify the file or we could run into collisions that muddy the water and
@@ -34,16 +36,26 @@ module Minitest
34
36
  def initialize(pathname)
35
37
  @pathname = Pathname(pathname)
36
38
  @issues = {}
39
+ @lines = {}
37
40
  end
38
41
 
39
42
  # Adds a record of a given issue type for the line number
40
43
  # @param type [Symbol] one of Issue::TYPES
41
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
42
46
  #
43
- # @return [type] [description]
44
- def log(type, line_number)
45
- @issues[type] ||= []
46
- @issues[type] << Integer(line_number)
47
+ # @return [void]
48
+ def log(type, line_number, backtrace: [])
49
+ line_number = Integer(line_number)
50
+ issue_type = type.to_sym
51
+
52
+ # Store issues by issue type with an array of line numbers
53
+ @issues[issue_type] ||= []
54
+ @issues[issue_type] << line_number
55
+
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)
47
59
  end
48
60
 
49
61
  # Calcuates an approximate weight to serve as a proxy for which files are most likely to be
@@ -18,7 +18,7 @@ module Minitest
18
18
  }.freeze
19
19
 
20
20
  attr_reader :assertions,
21
- :location,
21
+ :locations,
22
22
  :message,
23
23
  :test_class,
24
24
  :test_identifier,
@@ -27,7 +27,7 @@ module Minitest
27
27
  :error,
28
28
  :skipped
29
29
 
30
- def_delegators :@location, :backtrace, :test_definition_line, :test_failure_line
30
+ def_delegators :@locations, :backtrace, :test_definition_line, :test_failure_line
31
31
 
32
32
  # Extracts the necessary data from result.
33
33
  # @param result [Minitest::Result] the instance of Minitest::Result to examine
@@ -39,7 +39,7 @@ module Minitest
39
39
 
40
40
  new(
41
41
  assertions: result.assertions,
42
- location: result.source_location,
42
+ test_location: result.source_location,
43
43
  test_class: result.klass,
44
44
  test_identifier: result.name,
45
45
  execution_time: result.time,
@@ -47,7 +47,7 @@ module Minitest
47
47
  error: result.error?,
48
48
  skipped: result.skipped?,
49
49
  message: exception&.message,
50
- backtrace: exception&.backtrace,
50
+ backtrace: exception&.backtrace
51
51
  )
52
52
  end
53
53
 
@@ -57,7 +57,7 @@ module Minitest
57
57
  # @param assertions: 1 [Integer] the number of assertions in the result
58
58
  # @param message: nil [String] exception if there is one
59
59
  # @param backtrace: [] [Array<String>] the array of backtrace lines from an exception
60
- # @param location: nil [String] the location identifier for a test
60
+ # @param test_location: nil [Array<String, Integer>] the locations identifier for a test
61
61
  # @param test_class: nil [String] the class name for the test result's containing class
62
62
  # @param test_identifier: nil [String] the name of the test
63
63
  # @param execution_time: nil [Float] the time it took to run the test
@@ -66,11 +66,11 @@ module Minitest
66
66
  # @param skipped: false [Boolean] true if the test was skipped
67
67
  #
68
68
  # @return [type] [description]
69
- def initialize(assertions: 1, location: ['unknown', 1], backtrace: [], execution_time: 0.0, message: nil, test_class: nil, test_identifier: nil, passed: false, error: false, skipped: false)
69
+ def initialize(assertions: 1, test_location: ['Unrecognized Test File', 1], backtrace: [], execution_time: 0.0, message: nil, test_class: nil, test_identifier: nil, passed: false, error: false, skipped: false)
70
70
  @message = message
71
71
 
72
72
  @assertions = Integer(assertions)
73
- @location = Location.new(location, backtrace)
73
+ @locations = Locations.new(test_location, backtrace)
74
74
 
75
75
  @test_class = test_class
76
76
  @test_identifier = test_identifier
@@ -135,18 +135,18 @@ module Minitest
135
135
  # Determines if the issue is an exception that was raised from directly within a test
136
136
  # definition. In these cases, it's more likely to be a quick fix.
137
137
  #
138
- # @return [Boolean] true if the final location of the stacktrace was a test file
138
+ # @return [Boolean] true if the final locations of the stacktrace was a test file
139
139
  def in_test?
140
- location.broken_test?
140
+ locations.broken_test?
141
141
  end
142
142
 
143
143
  # Determines if the issue is an exception that was raised from directly within the project
144
144
  # codebase.
145
145
  #
146
- # @return [Boolean] true if the final location of the stacktrace was a file from the project
146
+ # @return [Boolean] true if the final locations of the stacktrace was a file from the project
147
147
  # (as opposed to a dependency or Ruby library)
148
148
  def in_source?
149
- location.proper_failure?
149
+ locations.proper_failure?
150
150
  end
151
151
 
152
152
  # Was the result a pass? i.e. Skips aren't passes or failures. Slows are still passes. So this
@@ -2,197 +2,178 @@
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
+ # Consistent structure for extracting information about a given location. In addition to the
6
+ # pathname to the file and the line number in the file, it can also include information about
7
+ # the containing method or block and retrieve source code for the location.
16
8
  class Location
17
- TestDefinition = Struct.new(:pathname, :line_number) do
18
- def initialize(pathname, line_number)
19
- @pathname = Pathname(pathname)
20
- @line_number = Integer(line_number)
21
- super
22
- end
23
- end
9
+ UNRECOGNIZED = '(Unrecognized File)'
10
+ UNKNOWN_MODIFICATION_TIME = Time.at(0)
11
+ UNKNOWN_MODIFICATION_SECONDS = -1
24
12
 
25
- attr_reader :test_definition_location, :backtrace
13
+ attr_accessor :raw_pathname, :raw_line_number, :raw_container
26
14
 
27
- def initialize(test_definition_location, backtrace = [])
28
- @test_definition_location = TestDefinition.new(*test_definition_location)
29
- @backtrace = Backtrace.new(backtrace)
15
+ # Initialize a new Location
16
+ #
17
+ # @param [Pathname, String] pathname: the pathname to the file
18
+ # @param [Integer] line_number: the line number of the location within the file
19
+ # @param [String] container: nil the containing method or block for the issue
20
+ #
21
+ # @return [self]
22
+ def initialize(pathname:, line_number:, container: nil)
23
+ @raw_pathname = pathname
24
+ @raw_line_number = line_number
25
+ @raw_container = container
30
26
  end
31
27
 
32
- # Prints the pathname and line number of the location most likely to be the source of the
33
- # test failure
28
+ # Generates a formatted string describing the line of code similar to the original backtrace
34
29
  #
35
- # @return [String] ex. 'path/to/file.rb:12'
30
+ # @return [String] a consistently-formatted, human-readable string about the line of code
36
31
  def to_s
37
- "#{most_relevant_file}:#{most_relevant_failure_line}"
38
- end
39
-
40
- def local?
41
- broken_test? || proper_failure?
32
+ "#{absolute_path}#{filename}:#{line_number} in `#{container}`"
42
33
  end
43
34
 
44
- # Knows if the failure is contained within the test. For example, if there's bad code in a
45
- # test, and it raises an exception, then it's really a broken test rather than a proper
46
- # faiure.
35
+ # Generates a simplified location array with the pathname and line number
47
36
  #
48
- # @return [Boolean] true if final file in the backtrace is the same as the test location file
49
- def broken_test?
50
- !test_file.nil? && test_file == final_file
37
+ # @return [Array<Pathname, Integer>] a no-frills location pair
38
+ def to_a
39
+ [
40
+ pathname,
41
+ line_number
42
+ ]
51
43
  end
52
44
 
53
- # Knows if the failure occurred in the actual project source code—as opposed to the test or
54
- # an external piece of code like a gem.
45
+ # A short relative pathname and line number pair
55
46
  #
56
- # @return [Boolean] true if there's a non-test project file in the stacktrace but it's not
57
- # a result of a broken test
58
- def proper_failure?
59
- !source_code_file.nil? && !broken_test?
47
+ # @return [String] the short filename/line number combo. ex. `dir/file.rb:23`
48
+ def short
49
+ "#{relative_filename}:#{line_number}"
60
50
  end
61
51
 
62
-
63
-
64
- # The final location of the stacktrace regardless of whether it's from within the project
52
+ # Determine if there is a file and text at the given line number
65
53
  #
66
- # @return [String] the relative path to the file from the project root
67
- def final_file
68
- Pathname(final_location.pathname)
54
+ # @return [Boolean] true if the file exists and has text at the given line number
55
+ def exists?
56
+ pathname.exist? && source_code.lines.any?
69
57
  end
70
58
 
71
- # The file most likely to be the source of the underlying problem. Often, the most recent
72
- # backtrace files will be a gem or external library that's failing indirectly as a result
73
- # of a problem with local source code (not always, but frequently). In that case, the best
74
- # first place to focus is on the code you control.
59
+ # The pathanme for the location. Written to be safe and fallbackto the project directory if
60
+ # an exception is raised ocnverting the value to a pathname
75
61
  #
76
- # @return [String] the relative path to the file from the project root
77
- def most_relevant_file
78
- Pathname(most_relevant_location.pathname)
62
+ # @return [Pathname] a pathname instance for the relevant file
63
+ def pathname
64
+ Pathname(raw_pathname)
65
+ rescue ArgumentError
66
+ Pathname(Dir.pwd)
79
67
  end
80
68
 
81
- # The final location from the stacktrace that is a test file
69
+ # A safe interface to getting a string representing the path portion of the file
82
70
  #
83
- # @return [String, nil] the relative path to the file from the project root
84
- def test_file
85
- Pathname(final_test_location.pathname)
71
+ # @return [String] either the path/directory portion of the file name or '(Unrecognized File)'
72
+ # if the offending file can't be found for some reason
73
+ def path
74
+ pathname.exist? ? pathname.dirname.to_s : UNRECOGNIZED
86
75
  end
87
76
 
88
- # The final location from the stacktrace that is within the project directory
89
- #
90
- # @return [String, nil] the relative path to the file from the project root
91
- def source_code_file
92
- return nil if final_source_code_location.nil?
77
+ def absolute_path
78
+ pathname.exist? ? "#{path}/" : UNRECOGNIZED
79
+ end
93
80
 
94
- Pathname(final_source_code_location.pathname)
81
+ def relative_path
82
+ pathname.exist? ? absolute_path.delete_prefix("#{project_root_dir}/") : UNRECOGNIZED
95
83
  end
96
84
 
97
- # The final location of the stacktrace from within the project (source code or test code)
85
+ # A safe interface for getting a string representing the filename portion of the file
98
86
  #
99
- # @return [String] the relative path to the file from the project root
100
- def project_file
101
- return nil if project_location.nil?
102
-
103
- Pathname(project_location.pathname)
87
+ # @return [String] either the filename portion of the file or '(Unrecognized File)'
88
+ # if the offending file can't be found for some reason
89
+ def filename
90
+ pathname.exist? ? pathname.basename.to_s : UNRECOGNIZED
104
91
  end
105
92
 
93
+ def absolute_filename
94
+ pathname.exist? ? pathname.to_s : UNRECOGNIZED
95
+ end
106
96
 
107
- # The line number of the `final_file` where the failure originated
108
- #
109
- # @return [Integer] line number
110
- def final_failure_line
111
- final_location.line_number
97
+ def relative_filename
98
+ pathname.exist? ? pathname.to_s.delete_prefix("#{project_root_dir}/") : UNRECOGNIZED
112
99
  end
113
100
 
114
- # The line number of the `most_relevant_file` where the failure originated
101
+ # Line number identifying the specific line in the file
115
102
  #
116
- # @return [Integer] line number
117
- def most_relevant_failure_line
118
- most_relevant_location.line_number
103
+ # @return [Integer] line number for the file
104
+ #
105
+ def line_number
106
+ Integer(raw_line_number)
107
+ rescue ArgumentError
108
+ 1
119
109
  end
120
110
 
121
- # The line number of the `test_file` where the test is defined
111
+ # The containing method or block details for the location
122
112
  #
123
- # @return [Integer] line number
124
- def test_definition_line
125
- test_definition_location.line_number
113
+ # @return [String] the containing method of the line of code
114
+ def container
115
+ raw_container.nil? ? '(Unknown Container)' : String(raw_container)
126
116
  end
127
117
 
128
- # The line number from within the `test_file` test definition where the failure occurred
118
+ # Looks up the source code for the location. Can return multiple lines of source code from
119
+ # the surrounding lines of code for the primary line
120
+ #
121
+ # @param [Integer] max_line_count: 1 the maximum number of lines to return from the source
129
122
  #
130
- # @return [Integer] line number
131
- def test_failure_line
132
- final_test_location.line_number
123
+ # @return [Source] an instance of Source for accessing lines and their line numbers
124
+ def source_code(max_line_count: 1)
125
+ Minitest::Heat::Source.new(
126
+ pathname.to_s,
127
+ line_number: line_number,
128
+ max_line_count: max_line_count
129
+ )
133
130
  end
134
131
 
135
- # The line number of the `source_code_file` where the failure originated
132
+ # Determines if a given file is from the project directory
136
133
  #
137
- # @return [Integer] line number
138
- def source_code_failure_line
139
- final_source_code_location&.line_number
134
+ # @return [Boolean] true if the file is in the project (source code or test)
135
+ def project_file?
136
+ path.include?(project_root_dir)
140
137
  end
141
138
 
142
- # The line number of the `project_file` where the failure originated
139
+ # Determines if a given file follows the standard approaching to naming test files.
143
140
  #
144
- # @return [Integer] line number
145
- def project_failure_line
146
- if !broken_test? && !source_code_file.nil?
147
- source_code_failure_line
148
- else
149
- test_failure_line
150
- end
141
+ # @return [Boolean] true if the file name starts with `test_` or ends with `_test.rb`
142
+ def test_file?
143
+ filename.to_s.start_with?('test_') || filename.to_s.end_with?('_test.rb')
151
144
  end
152
145
 
153
- # The line number from within the `test_file` test definition where the failure occurred
146
+ # Determines if a given file is a non-test file from the project directory
154
147
  #
155
- # @return [Location] the last location from the backtrace or the test location if a backtrace
156
- # was not passed to the initializer
157
- def final_location
158
- backtrace.parsed_entries.any? ? backtrace.final_location : test_definition_location
148
+ # @return [Boolean] true if the file is in the project but not a test file
149
+ def source_code_file?
150
+ project_file? && !test_file?
159
151
  end
160
152
 
161
- # The file most likely to be the source of the underlying problem. Often, the most recent
162
- # backtrace files will be a gem or external library that's failing indirectly as a result
163
- # of a problem with local source code (not always, but frequently). In that case, the best
164
- # first place to focus is on the code you control.
153
+ # A safe interface to getting the last modified time for the file in question
165
154
  #
166
- # @return [Array] file and line number of the most likely source of the problem
167
- def most_relevant_location
168
- [
169
- final_source_code_location,
170
- final_test_location,
171
- final_location
172
- ].compact.first
155
+ # @return [Time] the timestamp for when the file in question was last modified or `Time.at(0)`
156
+ # if the offending file can't be found for some reason
157
+ def mtime
158
+ pathname.exist? ? pathname.mtime : UNKNOWN_MODIFICATION_TIME
173
159
  end
174
160
 
175
- # Returns the final test location based on the backtrace if present. Otherwise falls back to
176
- # the test location which represents the test definition.
161
+ # A safe interface to getting the number of seconds since the file was modified
177
162
  #
178
- # @return [Location] the final location from the test files
179
- def final_test_location
180
- backtrace.final_test_location || test_definition_location
163
+ # @return [Integer] the number of seconds since the file was modified or `-1` if the offending
164
+ # file can't be found for some reason
165
+ def age_in_seconds
166
+ pathname.exist? ? seconds_ago : UNKNOWN_MODIFICATION_SECONDS
181
167
  end
182
168
 
183
- # Returns the final source code location based on the backtrace
184
- #
185
- # @return [Location] the final location from the source code files
186
- def final_source_code_location
187
- backtrace.final_source_code_location
169
+ private
170
+
171
+ def project_root_dir
172
+ Dir.pwd
188
173
  end
189
174
 
190
- # Returns the final project location based on the backtrace if present. Otherwise falls back
191
- # to the test location which represents the test definition.
192
- #
193
- # @return [Location] the final location from the project files
194
- def project_location
195
- backtrace.final_project_location || test_definition_location
175
+ def seconds_ago
176
+ (Time.now - mtime).to_i
196
177
  end
197
178
  end
198
179
  end