minitest-heat 0.0.9 → 0.0.13
Sign up to get free protection for your applications and to get access to all the features.
- 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
|