minitest-heat 0.0.10 → 0.0.11

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