24
24
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25
25
# SUCH DAMAGE.
26
26
27
+ class LeakError < StandardError
28
+ end
29
+
27
30
class LeakChecker
31
+ attr_reader :leaks
32
+
28
33
def initialize
29
34
@fd_info = find_fds
30
35
@tempfile_info = find_tempfiles
@@ -34,19 +39,18 @@ def initialize
34
39
@encoding_info = find_encodings
35
40
end
36
41
37
- def check ( test_name )
38
- @no_leaks = true
39
- leaks = [
40
- check_fd_leak ( test_name ) ,
41
- check_tempfile_leak ( test_name ) ,
42
- check_thread_leak ( test_name ) ,
43
- check_process_leak ( test_name ) ,
44
- check_env ( test_name ) ,
45
- check_argv ( test_name ) ,
46
- check_encodings ( test_name )
47
- ]
48
- GC . start if leaks . any?
49
- return leaks . none?
42
+ def check ( state )
43
+ @state = state
44
+ @leaks = [ ]
45
+ check_fd_leak
46
+ check_tempfile_leak
47
+ check_thread_leak
48
+ check_process_leak
49
+ check_env
50
+ check_argv
51
+ check_encodings
52
+ GC . start if !@leaks . empty?
53
+ @leaks . empty?
50
54
end
51
55
52
56
private
@@ -66,8 +70,7 @@ def find_fds
66
70
end
67
71
end
68
72
69
- def check_fd_leak ( test_name )
70
- leaked = false
73
+ def check_fd_leak
71
74
live1 = @fd_info
72
75
if IO . respond_to? ( :console ) and ( m = IO . method ( :console ) ) . arity . nonzero?
73
76
m [ :close ]
@@ -76,12 +79,11 @@ def check_fd_leak(test_name)
76
79
fd_closed = live1 - live2
77
80
if !fd_closed . empty?
78
81
fd_closed . each { |fd |
79
- puts "Closed file descriptor: #{ test_name } : #{ fd } "
82
+ leak "Closed file descriptor: #{ fd } "
80
83
}
81
84
end
82
85
fd_leaked = live2 - live1
83
86
if !fd_leaked . empty?
84
- leaked = true
85
87
h = { }
86
88
ObjectSpace . each_object ( IO ) { |io |
87
89
inspect = io . inspect
@@ -105,19 +107,18 @@ def check_fd_leak(test_name)
105
107
str << s
106
108
}
107
109
end
108
- puts "Leaked file descriptor: #{ test_name } : #{ fd } #{ str } "
110
+ leak "Leaked file descriptor: #{ fd } #{ str } "
109
111
}
110
112
#system("lsof -p #$$") if !fd_leaked.empty?
111
113
h . each { |fd , list |
112
114
next if list . length <= 1
113
115
if 1 < list . count { |io , autoclose , inspect | autoclose }
114
116
str = list . map { |io , autoclose , inspect | " #{ inspect } " + ( autoclose ? "(autoclose)" : "" ) } . sort . join
115
- puts "Multiple autoclose IO object for a file descriptor:#{ str } "
117
+ leak "Multiple autoclose IO object for a file descriptor:#{ str } "
116
118
end
117
119
}
118
120
end
119
121
@fd_info = live2
120
- return leaked
121
122
end
122
123
123
124
def extend_tempfile_counter
@@ -152,22 +153,19 @@ def find_tempfiles(prev_count=-1)
152
153
end
153
154
end
154
155
155
- def check_tempfile_leak ( test_name )
156
+ def check_tempfile_leak
156
157
return false unless defined? Tempfile
157
158
count1 , initial_tempfiles = @tempfile_info
158
159
count2 , current_tempfiles = find_tempfiles ( count1 )
159
- leaked = false
160
160
tempfiles_leaked = current_tempfiles - initial_tempfiles
161
161
if !tempfiles_leaked . empty?
162
- leaked = true
163
162
list = tempfiles_leaked . map { |t | t . inspect } . sort
164
163
list . each { |str |
165
- puts "Leaked tempfile: #{ test_name } : #{ str } "
164
+ leak "Leaked tempfile: #{ str } "
166
165
}
167
166
tempfiles_leaked . each { |t | t . close! }
168
167
end
169
168
@tempfile_info = [ count2 , initial_tempfiles ]
170
- return leaked
171
169
end
172
170
173
171
def find_threads
@@ -176,108 +174,98 @@ def find_threads
176
174
}
177
175
end
178
176
179
- def check_thread_leak ( test_name )
177
+ def check_thread_leak
180
178
live1 = @thread_info
181
179
live2 = find_threads
182
180
thread_finished = live1 - live2
183
- leaked = false
184
181
if !thread_finished . empty?
185
182
list = thread_finished . map { |t | t . inspect } . sort
186
183
list . each { |str |
187
- puts "Finished thread: #{ test_name } : #{ str } "
184
+ leak "Finished thread: #{ str } "
188
185
}
189
186
end
190
187
thread_leaked = live2 - live1
191
188
if !thread_leaked . empty?
192
- leaked = true
193
189
list = thread_leaked . map { |t | t . inspect } . sort
194
190
list . each { |str |
195
- puts "Leaked thread: #{ test_name } : #{ str } "
191
+ leak "Leaked thread: #{ str } "
196
192
}
197
193
end
198
194
@thread_info = live2
199
- return leaked
200
195
end
201
196
202
- def check_process_leak ( test_name )
197
+ def check_process_leak
203
198
subprocesses_leaked = Process . waitall
204
199
subprocesses_leaked . each { |pid , status |
205
- puts "Leaked subprocess: #{ pid } : #{ status } "
200
+ leak "Leaked subprocess: #{ pid } : #{ status } "
206
201
}
207
- return !subprocesses_leaked . empty?
208
202
end
209
203
210
204
def find_env
211
205
ENV . to_h
212
206
end
213
207
214
- def check_env ( test_name )
208
+ def check_env
215
209
old_env = @env_info
216
210
new_env = find_env
217
- return false if old_env == new_env
211
+ return if old_env == new_env
212
+
218
213
( old_env . keys | new_env . keys ) . sort . each { |k |
219
214
if old_env . has_key? ( k )
220
215
if new_env . has_key? ( k )
221
216
if old_env [ k ] != new_env [ k ]
222
- puts "Environment variable changed: #{ test_name } : #{ k . inspect } changed : #{ old_env [ k ] . inspect } -> #{ new_env [ k ] . inspect } "
217
+ leak "Environment variable changed : #{ k . inspect } changed : #{ old_env [ k ] . inspect } -> #{ new_env [ k ] . inspect } "
223
218
end
224
219
else
225
- puts "Environment variable changed: #{ test_name } : #{ k . inspect } deleted"
220
+ leak "Environment variable changed: #{ k . inspect } deleted"
226
221
end
227
222
else
228
223
if new_env . has_key? ( k )
229
- puts "Environment variable changed: #{ test_name } : #{ k . inspect } added"
224
+ leak "Environment variable changed: #{ k . inspect } added"
230
225
else
231
226
flunk "unreachable"
232
227
end
233
228
end
234
229
}
235
230
@env_info = new_env
236
- return true
237
231
end
238
232
239
233
def find_argv
240
234
ARGV . map { |e | e . dup }
241
235
end
242
236
243
- def check_argv ( test_name )
237
+ def check_argv
244
238
old_argv = @argv_info
245
239
new_argv = find_argv
246
- leaked = false
247
240
if new_argv != old_argv
248
- puts "ARGV changed: #{ test_name } : #{ old_argv . inspect } to #{ new_argv . inspect } "
241
+ leak "ARGV changed: #{ old_argv . inspect } to #{ new_argv . inspect } "
249
242
@argv_info = new_argv
250
- leaked = true
251
243
end
252
- return leaked
253
244
end
254
245
255
246
def find_encodings
256
247
[ Encoding . default_internal , Encoding . default_external ]
257
248
end
258
249
259
- def check_encodings ( test_name )
250
+ def check_encodings
260
251
old_internal , old_external = @encoding_info
261
252
new_internal , new_external = find_encodings
262
- leaked = false
263
253
if new_internal != old_internal
264
- leaked = true
265
- puts "Encoding.default_internal changed: #{ test_name } : #{ old_internal . inspect } to #{ new_internal . inspect } "
254
+ leak "Encoding.default_internal changed: #{ old_internal . inspect } to #{ new_internal . inspect } "
266
255
end
267
256
if new_external != old_external
268
- leaked = true
269
- puts "Encoding.default_external changed: #{ test_name } : #{ old_external . inspect } to #{ new_external . inspect } "
257
+ leak "Encoding.default_external changed: #{ old_external . inspect } to #{ new_external . inspect } "
270
258
end
271
259
@encoding_info = [ new_internal , new_external ]
272
- return leaked
273
260
end
274
261
275
- def puts ( * args )
276
- if @no_leaks
277
- @no_leaks = false
278
- print " \n "
262
+ def leak ( message )
263
+ if @leaks . empty?
264
+ $stderr . puts " \n "
265
+ $stderr . puts @state . description
279
266
end
280
- super ( *args )
267
+ @leaks << message
268
+ $stderr. puts message
281
269
end
282
270
end
283
271
@@ -292,9 +280,14 @@ def start
292
280
end
293
281
294
282
def after ( state )
295
- unless @checker . check ( state . description )
283
+ unless @checker . check ( state )
284
+ leak_messages = @checker . leaks
285
+ location = state . description
296
286
if state . example
297
- puts state . example . source_location . join ( ':' )
287
+ location = "#{ location } \n #{ state . example . source_location . join ( ':' ) } "
288
+ end
289
+ MSpec . protect ( location ) do
290
+ raise LeakError , leak_messages . join ( "\n " )
298
291
end
299
292
end
300
293
end
0 commit comments