Skip to content

Commit 2ee5661

Browse files
committed
Make leaks errors to catch them early
* Collect leak messages rather than using return values. * Raise leak as an error of the spec example. * Print leak early on $stderr, in case specs get stuck before error formatting.
1 parent cf14bbc commit 2ee5661

File tree

1 file changed

+52
-59
lines changed

1 file changed

+52
-59
lines changed

lib/mspec/runner/actions/leakchecker.rb

Lines changed: 52 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@
2424
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
2525
# SUCH DAMAGE.
2626

27+
class LeakError < StandardError
28+
end
29+
2730
class LeakChecker
31+
attr_reader :leaks
32+
2833
def initialize
2934
@fd_info = find_fds
3035
@tempfile_info = find_tempfiles
@@ -34,19 +39,18 @@ def initialize
3439
@encoding_info = find_encodings
3540
end
3641

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?
5054
end
5155

5256
private
@@ -66,8 +70,7 @@ def find_fds
6670
end
6771
end
6872

69-
def check_fd_leak(test_name)
70-
leaked = false
73+
def check_fd_leak
7174
live1 = @fd_info
7275
if IO.respond_to?(:console) and (m = IO.method(:console)).arity.nonzero?
7376
m[:close]
@@ -76,12 +79,11 @@ def check_fd_leak(test_name)
7679
fd_closed = live1 - live2
7780
if !fd_closed.empty?
7881
fd_closed.each {|fd|
79-
puts "Closed file descriptor: #{test_name}: #{fd}"
82+
leak "Closed file descriptor: #{fd}"
8083
}
8184
end
8285
fd_leaked = live2 - live1
8386
if !fd_leaked.empty?
84-
leaked = true
8587
h = {}
8688
ObjectSpace.each_object(IO) {|io|
8789
inspect = io.inspect
@@ -105,19 +107,18 @@ def check_fd_leak(test_name)
105107
str << s
106108
}
107109
end
108-
puts "Leaked file descriptor: #{test_name}: #{fd}#{str}"
110+
leak "Leaked file descriptor: #{fd}#{str}"
109111
}
110112
#system("lsof -p #$$") if !fd_leaked.empty?
111113
h.each {|fd, list|
112114
next if list.length <= 1
113115
if 1 < list.count {|io, autoclose, inspect| autoclose }
114116
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}"
116118
end
117119
}
118120
end
119121
@fd_info = live2
120-
return leaked
121122
end
122123

123124
def extend_tempfile_counter
@@ -152,22 +153,19 @@ def find_tempfiles(prev_count=-1)
152153
end
153154
end
154155

155-
def check_tempfile_leak(test_name)
156+
def check_tempfile_leak
156157
return false unless defined? Tempfile
157158
count1, initial_tempfiles = @tempfile_info
158159
count2, current_tempfiles = find_tempfiles(count1)
159-
leaked = false
160160
tempfiles_leaked = current_tempfiles - initial_tempfiles
161161
if !tempfiles_leaked.empty?
162-
leaked = true
163162
list = tempfiles_leaked.map {|t| t.inspect }.sort
164163
list.each {|str|
165-
puts "Leaked tempfile: #{test_name}: #{str}"
164+
leak "Leaked tempfile: #{str}"
166165
}
167166
tempfiles_leaked.each {|t| t.close! }
168167
end
169168
@tempfile_info = [count2, initial_tempfiles]
170-
return leaked
171169
end
172170

173171
def find_threads
@@ -176,108 +174,98 @@ def find_threads
176174
}
177175
end
178176

179-
def check_thread_leak(test_name)
177+
def check_thread_leak
180178
live1 = @thread_info
181179
live2 = find_threads
182180
thread_finished = live1 - live2
183-
leaked = false
184181
if !thread_finished.empty?
185182
list = thread_finished.map {|t| t.inspect }.sort
186183
list.each {|str|
187-
puts "Finished thread: #{test_name}: #{str}"
184+
leak "Finished thread: #{str}"
188185
}
189186
end
190187
thread_leaked = live2 - live1
191188
if !thread_leaked.empty?
192-
leaked = true
193189
list = thread_leaked.map {|t| t.inspect }.sort
194190
list.each {|str|
195-
puts "Leaked thread: #{test_name}: #{str}"
191+
leak "Leaked thread: #{str}"
196192
}
197193
end
198194
@thread_info = live2
199-
return leaked
200195
end
201196

202-
def check_process_leak(test_name)
197+
def check_process_leak
203198
subprocesses_leaked = Process.waitall
204199
subprocesses_leaked.each { |pid, status|
205-
puts "Leaked subprocess: #{pid}: #{status}"
200+
leak "Leaked subprocess: #{pid}: #{status}"
206201
}
207-
return !subprocesses_leaked.empty?
208202
end
209203

210204
def find_env
211205
ENV.to_h
212206
end
213207

214-
def check_env(test_name)
208+
def check_env
215209
old_env = @env_info
216210
new_env = find_env
217-
return false if old_env == new_env
211+
return if old_env == new_env
212+
218213
(old_env.keys | new_env.keys).sort.each {|k|
219214
if old_env.has_key?(k)
220215
if new_env.has_key?(k)
221216
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}"
223218
end
224219
else
225-
puts "Environment variable changed: #{test_name} : #{k.inspect} deleted"
220+
leak "Environment variable changed: #{k.inspect} deleted"
226221
end
227222
else
228223
if new_env.has_key?(k)
229-
puts "Environment variable changed: #{test_name} : #{k.inspect} added"
224+
leak "Environment variable changed: #{k.inspect} added"
230225
else
231226
flunk "unreachable"
232227
end
233228
end
234229
}
235230
@env_info = new_env
236-
return true
237231
end
238232

239233
def find_argv
240234
ARGV.map { |e| e.dup }
241235
end
242236

243-
def check_argv(test_name)
237+
def check_argv
244238
old_argv = @argv_info
245239
new_argv = find_argv
246-
leaked = false
247240
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}"
249242
@argv_info = new_argv
250-
leaked = true
251243
end
252-
return leaked
253244
end
254245

255246
def find_encodings
256247
[Encoding.default_internal, Encoding.default_external]
257248
end
258249

259-
def check_encodings(test_name)
250+
def check_encodings
260251
old_internal, old_external = @encoding_info
261252
new_internal, new_external = find_encodings
262-
leaked = false
263253
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}"
266255
end
267256
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}"
270258
end
271259
@encoding_info = [new_internal, new_external]
272-
return leaked
273260
end
274261

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
279266
end
280-
super(*args)
267+
@leaks << message
268+
$stderr.puts message
281269
end
282270
end
283271

@@ -292,9 +280,14 @@ def start
292280
end
293281

294282
def after(state)
295-
unless @checker.check(state.description)
283+
unless @checker.check(state)
284+
leak_messages = @checker.leaks
285+
location = state.description
296286
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")
298291
end
299292
end
300293
end

0 commit comments

Comments
 (0)