minitest-heat 0.0.9 → 0.0.13
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 +4 -4
- data/.rubocop.yml +1 -0
- data/Gemfile.lock +6 -6
- data/README.md +49 -14
- data/examples/exceptions.png +0 -0
- data/examples/failures.png +0 -0
- data/examples/map.png +0 -0
- data/examples/markers.png +0 -0
- data/examples/skips.png +0 -0
- data/examples/slows.png +0 -0
- data/lib/minitest/heat/backtrace/line_parser.rb +25 -0
- data/lib/minitest/heat/backtrace.rb +39 -43
- data/lib/minitest/heat/hit.rb +36 -19
- data/lib/minitest/heat/issue.rb +118 -81
- data/lib/minitest/heat/location.rb +115 -116
- data/lib/minitest/heat/locations.rb +105 -0
- data/lib/minitest/heat/map.rb +16 -4
- data/lib/minitest/heat/output/backtrace.rb +90 -67
- data/lib/minitest/heat/output/issue.rb +76 -67
- data/lib/minitest/heat/output/map.rb +127 -25
- data/lib/minitest/heat/output/marker.rb +5 -3
- data/lib/minitest/heat/output/results.rb +3 -2
- data/lib/minitest/heat/output/source_code.rb +1 -1
- data/lib/minitest/heat/output/token.rb +2 -1
- data/lib/minitest/heat/output.rb +76 -6
- data/lib/minitest/heat/results.rb +23 -3
- data/lib/minitest/heat/source.rb +1 -1
- data/lib/minitest/heat/version.rb +1 -1
- data/lib/minitest/heat.rb +3 -2
- data/lib/minitest/heat_plugin.rb +9 -17
- data/lib/minitest/heat_reporter.rb +25 -35
- metadata +11 -4
- data/lib/minitest/heat/line.rb +0 -74
data/lib/minitest/heat/issue.rb
CHANGED
@@ -17,36 +17,68 @@ module Minitest
|
|
17
17
|
painful: 3.0
|
18
18
|
}.freeze
|
19
19
|
|
20
|
-
attr_reader :
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
20
|
+
attr_reader :assertions,
|
21
|
+
:locations,
|
22
|
+
:message,
|
23
|
+
:test_class,
|
24
|
+
:test_identifier,
|
25
|
+
:execution_time,
|
26
|
+
:passed,
|
27
|
+
:error,
|
28
|
+
:skipped
|
29
|
+
|
30
|
+
def_delegators :@locations, :backtrace, :test_definition_line, :test_failure_line
|
31
|
+
|
32
|
+
# Extracts the necessary data from result.
|
33
|
+
# @param result [Minitest::Result] the instance of Minitest::Result to examine
|
34
|
+
#
|
35
|
+
# @return [Issue] the instance of the issue to use for examining the result
|
36
|
+
def self.from_result(result)
|
37
|
+
# Not all results are failures, so we use the safe navigation operator
|
38
|
+
exception = result.failure&.exception
|
39
|
+
|
40
|
+
new(
|
41
|
+
assertions: result.assertions,
|
42
|
+
test_location: result.source_location,
|
43
|
+
test_class: result.klass,
|
44
|
+
test_identifier: result.name,
|
45
|
+
execution_time: result.time,
|
46
|
+
passed: result.passed?,
|
47
|
+
error: result.error?,
|
48
|
+
skipped: result.skipped?,
|
49
|
+
message: exception&.message,
|
50
|
+
backtrace: exception&.backtrace
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Creates an instance of Issue. In general, the `from_result` approach will be more convenient
|
55
|
+
# for standard usage, but for lower-level purposes like testing, the initializer provides3
|
56
|
+
# more fine-grained control
|
57
|
+
# @param assertions: 1 [Integer] the number of assertions in the result
|
58
|
+
# @param message: nil [String] exception if there is one
|
59
|
+
# @param backtrace: [] [Array<String>] the array of backtrace lines from an exception
|
60
|
+
# @param test_location: nil [Array<String, Integer>] the locations identifier for a test
|
61
|
+
# @param test_class: nil [String] the class name for the test result's containing class
|
62
|
+
# @param test_identifier: nil [String] the name of the test
|
63
|
+
# @param execution_time: nil [Float] the time it took to run the test
|
64
|
+
# @param passed: false [Boolean] true if the test explicitly passed, false otherwise
|
65
|
+
# @param error: false [Boolean] true if the test raised an exception
|
66
|
+
# @param skipped: false [Boolean] true if the test was skipped
|
67
|
+
#
|
68
|
+
# @return [type] [description]
|
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
|
+
@message = message
|
27
71
|
|
28
|
-
@
|
29
|
-
@
|
30
|
-
end
|
72
|
+
@assertions = Integer(assertions)
|
73
|
+
@locations = Locations.new(test_location, backtrace)
|
31
74
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
# @return [String] the pathname for the file relative to the present working directory
|
36
|
-
def short_location
|
37
|
-
location.to_s.delete_prefix("#{Dir.pwd}/")
|
38
|
-
end
|
75
|
+
@test_class = test_class
|
76
|
+
@test_identifier = test_identifier
|
77
|
+
@execution_time = Float(execution_time)
|
39
78
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
# support generating the heat map
|
44
|
-
def to_hit
|
45
|
-
[
|
46
|
-
location.project_file.to_s,
|
47
|
-
Integer(location.project_failure_line),
|
48
|
-
type
|
49
|
-
]
|
79
|
+
@passed = passed
|
80
|
+
@error = error
|
81
|
+
@skipped = skipped
|
50
82
|
end
|
51
83
|
|
52
84
|
# Classifies different issue types so they can be categorized, organized, and prioritized.
|
@@ -58,7 +90,7 @@ module Minitest
|
|
58
90
|
# painfully slow and should get more attention.
|
59
91
|
#
|
60
92
|
# @return [Symbol] issue type for classifying issues and reporting
|
61
|
-
def type
|
93
|
+
def type # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
62
94
|
if error? && in_test?
|
63
95
|
:broken
|
64
96
|
elsif error?
|
@@ -67,9 +99,9 @@ module Minitest
|
|
67
99
|
:skipped
|
68
100
|
elsif !passed?
|
69
101
|
:failure
|
70
|
-
elsif painful?
|
102
|
+
elsif passed? && painful?
|
71
103
|
:painful
|
72
|
-
elsif slow?
|
104
|
+
elsif passed? && slow?
|
73
105
|
:slow
|
74
106
|
else
|
75
107
|
:success
|
@@ -81,93 +113,98 @@ module Minitest
|
|
81
113
|
#
|
82
114
|
# @return [Boolean] true if the test did not pass or if it was slow
|
83
115
|
def hit?
|
84
|
-
!passed? || slow?
|
116
|
+
!passed? || slow? || painful?
|
117
|
+
end
|
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
|
+
# Using a method here so that this can eventually be configurable such that the constant is
|
124
|
+
# only a fallback value if it's not specified anywhere else
|
125
|
+
SLOW_THRESHOLDS[:slow]
|
126
|
+
end
|
127
|
+
|
128
|
+
# The number, in seconds, for a test to be considered "painfully slow"
|
129
|
+
#
|
130
|
+
# @return [Float] number of seconds after which a test is considered painfully slow
|
131
|
+
def painfully_slow_threshold
|
132
|
+
# Using a method here so that this can eventually be configurable such that the constant is
|
133
|
+
# only a fallback value if it's not specified anywhere else
|
134
|
+
SLOW_THRESHOLDS[:painful]
|
85
135
|
end
|
86
136
|
|
87
137
|
# Determines if a test should be considered slow by comparing it to the low end definition of
|
88
138
|
# what is considered slow.
|
89
139
|
#
|
90
|
-
# @return [Boolean] true if the test took longer to run than `
|
140
|
+
# @return [Boolean] true if the test took longer to run than `slow_threshold`
|
91
141
|
def slow?
|
92
|
-
|
142
|
+
execution_time >= slow_threshold && execution_time < painful_threshold
|
93
143
|
end
|
94
144
|
|
95
145
|
# Determines if a test should be considered painfully slow by comparing it to the high end
|
96
146
|
# definition of what is considered slow.
|
97
147
|
#
|
98
|
-
# @return [Boolean] true if the test took longer to run than `
|
148
|
+
# @return [Boolean] true if the test took longer to run than `painful_threshold`
|
99
149
|
def painful?
|
100
|
-
|
150
|
+
execution_time >= painful_threshold
|
101
151
|
end
|
102
152
|
|
103
153
|
# Determines if the issue is an exception that was raised from directly within a test
|
104
154
|
# definition. In these cases, it's more likely to be a quick fix.
|
105
155
|
#
|
106
|
-
# @return [Boolean] true if the final
|
156
|
+
# @return [Boolean] true if the final locations of the stacktrace was a test file
|
107
157
|
def in_test?
|
108
|
-
|
158
|
+
locations.broken_test?
|
109
159
|
end
|
110
160
|
|
111
161
|
# Determines if the issue is an exception that was raised from directly within the project
|
112
162
|
# codebase.
|
113
163
|
#
|
114
|
-
# @return [Boolean] true if the final
|
164
|
+
# @return [Boolean] true if the final locations of the stacktrace was a file from the project
|
115
165
|
# (as opposed to a dependency or Ruby library)
|
116
166
|
def in_source?
|
117
|
-
|
118
|
-
end
|
119
|
-
|
120
|
-
def test_class
|
121
|
-
result.klass
|
167
|
+
locations.proper_failure?
|
122
168
|
end
|
123
169
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
def
|
129
|
-
|
130
|
-
end
|
131
|
-
|
132
|
-
def exception
|
133
|
-
failure.exception
|
134
|
-
end
|
135
|
-
|
136
|
-
def time
|
137
|
-
result.time
|
170
|
+
# Was the result a pass? i.e. Skips aren't passes or failures. Slows are still passes. So this
|
171
|
+
# is purely a measure of whether the test explicitly passed all assertions
|
172
|
+
#
|
173
|
+
# @return [Boolean] false for errors, failures, or skips, true for passes (including slows)
|
174
|
+
def passed?
|
175
|
+
passed
|
138
176
|
end
|
139
177
|
|
140
|
-
|
141
|
-
|
178
|
+
# Was there an exception that triggered a failure?
|
179
|
+
#
|
180
|
+
# @return [Boolean] true if there's an exception
|
181
|
+
def error?
|
182
|
+
error
|
142
183
|
end
|
143
184
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
'Broken Test'
|
150
|
-
elsif error? || !passed?
|
151
|
-
failure.result_label
|
152
|
-
elsif painful?
|
153
|
-
'Passed but Very Slow'
|
154
|
-
elsif slow?
|
155
|
-
'Passed but Slow'
|
156
|
-
end
|
185
|
+
# Was the test skipped?
|
186
|
+
#
|
187
|
+
# @return [Boolean] true if the test was explicitly skipped, false otherwise
|
188
|
+
def skipped?
|
189
|
+
skipped
|
157
190
|
end
|
158
191
|
|
192
|
+
# The more nuanced detail of the failure. If it's an error, digs into the exception. Otherwise
|
193
|
+
# uses the message from the result
|
194
|
+
#
|
195
|
+
# @return [String] a more detailed explanation of the issue
|
159
196
|
def summary
|
160
|
-
|
197
|
+
# When there's an exception, use the first line from the exception message. Otherwise, the
|
198
|
+
# message represents explanation for a test failure, and should be used in full
|
199
|
+
error? ? first_line_of_exception_message : message
|
161
200
|
end
|
162
201
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
def exception_parts
|
170
|
-
exception.message.split("\n")
|
202
|
+
# Returns the first line of an exception message when the issue is from a proper exception
|
203
|
+
# failure since exception messages can be long and cumbersome.
|
204
|
+
#
|
205
|
+
# @return [String] the first line of the exception message
|
206
|
+
def first_line_of_exception_message
|
207
|
+
message.split("\n")[0]
|
171
208
|
end
|
172
209
|
end
|
173
210
|
end
|
@@ -2,179 +2,178 @@
|
|
2
2
|
|
3
3
|
module Minitest
|
4
4
|
module Heat
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
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
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
13
|
+
attr_accessor :raw_pathname, :raw_line_number, :raw_container
|
26
14
|
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
#
|
33
|
-
# test failure
|
28
|
+
# Generates a formatted string describing the line of code similar to the original backtrace
|
34
29
|
#
|
35
|
-
# @return [String]
|
30
|
+
# @return [String] a consistently-formatted, human-readable string about the line of code
|
36
31
|
def to_s
|
37
|
-
"#{
|
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
|
-
#
|
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 [
|
49
|
-
def
|
50
|
-
|
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
|
-
#
|
54
|
-
# an external piece of code like a gem.
|
45
|
+
# A short relative pathname and line number pair
|
55
46
|
#
|
56
|
-
# @return [
|
57
|
-
|
58
|
-
|
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
|
-
# backtrace files will be a gem or external library that's failing indirectly as a result
|
64
|
-
# of a problem with local source code (not always, but frequently). In that case, the best
|
65
|
-
# first place to focus is on the code you control.
|
52
|
+
# Determine if there is a file and text at the given line number
|
66
53
|
#
|
67
|
-
# @return [
|
68
|
-
def
|
69
|
-
|
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?
|
70
57
|
end
|
71
58
|
|
72
|
-
# The
|
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
|
73
61
|
#
|
74
|
-
# @return [
|
75
|
-
def
|
76
|
-
|
62
|
+
# @return [Pathname] a pathname instance for the relevant file
|
63
|
+
def pathname
|
64
|
+
Pathname(raw_pathname)
|
65
|
+
rescue ArgumentError
|
66
|
+
Pathname(Dir.pwd)
|
77
67
|
end
|
78
68
|
|
79
|
-
#
|
69
|
+
# A safe interface to getting a string representing the path portion of the file
|
80
70
|
#
|
81
|
-
# @return [String] the
|
82
|
-
|
83
|
-
|
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
|
84
75
|
end
|
85
76
|
|
86
|
-
|
87
|
-
|
88
|
-
# @return [Integer] line number
|
89
|
-
def final_failure_line
|
90
|
-
final_location.line_number
|
77
|
+
def absolute_path
|
78
|
+
pathname.exist? ? "#{path}/" : UNRECOGNIZED
|
91
79
|
end
|
92
80
|
|
93
|
-
|
94
|
-
|
95
|
-
# @return [String] the relative path to the file from the project root
|
96
|
-
def project_file
|
97
|
-
broken_test? ? test_file : source_code_file
|
81
|
+
def relative_path
|
82
|
+
pathname.exist? ? absolute_path.delete_prefix("#{project_root_dir}/") : UNRECOGNIZED
|
98
83
|
end
|
99
84
|
|
100
|
-
#
|
85
|
+
# A safe interface for getting a string representing the filename portion of the file
|
101
86
|
#
|
102
|
-
# @return [
|
103
|
-
|
104
|
-
|
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
|
105
91
|
end
|
106
92
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
def source_code_file
|
111
|
-
return nil unless backtrace.source_code_entries.any?
|
93
|
+
def absolute_filename
|
94
|
+
pathname.exist? ? pathname.to_s : UNRECOGNIZED
|
95
|
+
end
|
112
96
|
|
113
|
-
|
97
|
+
def relative_filename
|
98
|
+
pathname.exist? ? pathname.to_s.delete_prefix("#{project_root_dir}/") : UNRECOGNIZED
|
114
99
|
end
|
115
100
|
|
116
|
-
#
|
101
|
+
# Line number identifying the specific line in the file
|
102
|
+
#
|
103
|
+
# @return [Integer] line number for the file
|
117
104
|
#
|
118
|
-
|
119
|
-
|
120
|
-
|
105
|
+
def line_number
|
106
|
+
Integer(raw_line_number)
|
107
|
+
rescue ArgumentError
|
108
|
+
1
|
109
|
+
end
|
121
110
|
|
122
|
-
|
111
|
+
# The containing method or block details for the location
|
112
|
+
#
|
113
|
+
# @return [String] the containing method of the line of code
|
114
|
+
def container
|
115
|
+
raw_container.nil? ? '(Unknown Container)' : String(raw_container)
|
123
116
|
end
|
124
117
|
|
125
|
-
#
|
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
|
126
120
|
#
|
127
|
-
# @
|
128
|
-
|
129
|
-
|
121
|
+
# @param [Integer] max_line_count: 1 the maximum number of lines to return from the source
|
122
|
+
#
|
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
|
+
)
|
130
130
|
end
|
131
131
|
|
132
|
-
#
|
132
|
+
# Determines if a given file is from the project directory
|
133
133
|
#
|
134
|
-
# @return [
|
135
|
-
def
|
136
|
-
|
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)
|
137
137
|
end
|
138
138
|
|
139
|
-
#
|
139
|
+
# Determines if a given file follows the standard approaching to naming test files.
|
140
140
|
#
|
141
|
-
# @return [
|
142
|
-
def
|
143
|
-
|
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')
|
144
144
|
end
|
145
145
|
|
146
|
-
#
|
146
|
+
# Determines if a given file is a non-test file from the project directory
|
147
147
|
#
|
148
|
-
# @return [
|
149
|
-
|
150
|
-
|
151
|
-
backtrace? ? backtrace.final_location : test_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?
|
152
151
|
end
|
153
152
|
|
154
|
-
#
|
155
|
-
# backtrace files will be a gem or external library that's failing indirectly as a result
|
156
|
-
# of a problem with local source code (not always, but frequently). In that case, the best
|
157
|
-
# 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
|
158
154
|
#
|
159
|
-
# @return [
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
test_location,
|
164
|
-
final_location
|
165
|
-
].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
|
166
159
|
end
|
167
160
|
|
168
|
-
|
169
|
-
|
161
|
+
# A safe interface to getting the number of seconds since the file was modified
|
162
|
+
#
|
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
|
170
167
|
end
|
171
168
|
|
172
|
-
|
173
|
-
|
169
|
+
private
|
170
|
+
|
171
|
+
def project_root_dir
|
172
|
+
Dir.pwd
|
174
173
|
end
|
175
174
|
|
176
|
-
def
|
177
|
-
|
175
|
+
def seconds_ago
|
176
|
+
(Time.now - mtime).to_i
|
178
177
|
end
|
179
178
|
end
|
180
179
|
end
|