* test/lib/leakchecker.rb: Leak checker extracted from
test/lib/minitest/unit.rb. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@46280 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
This commit is contained in:
parent
8784787623
commit
6c6f9b19b4
@ -1,3 +1,8 @@
|
|||||||
|
Sat May 31 22:30:14 2014 Tanaka Akira <akr@fsij.org>
|
||||||
|
|
||||||
|
* test/lib/leakchecker.rb: Leak checker extracted from
|
||||||
|
test/lib/minitest/unit.rb.
|
||||||
|
|
||||||
Sat May 31 21:15:43 2014 URABE Shyouhei <shyouhei@ruby-lang.org>
|
Sat May 31 21:15:43 2014 URABE Shyouhei <shyouhei@ruby-lang.org>
|
||||||
|
|
||||||
* thread.c (rb_thread_atfork_internal): My compiler complains
|
* thread.c (rb_thread_atfork_internal): My compiler complains
|
||||||
|
159
test/lib/leakchecker.rb
Normal file
159
test/lib/leakchecker.rb
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
class LeakChecker
|
||||||
|
def initialize
|
||||||
|
@fd_info = find_fds
|
||||||
|
@tempfile_info = find_tempfiles
|
||||||
|
@thread_info = find_threads
|
||||||
|
end
|
||||||
|
|
||||||
|
def check(test_name)
|
||||||
|
leaked1 = check_fd_leak(test_name)
|
||||||
|
leaked2 = check_thread_leak(test_name)
|
||||||
|
leaked3 = check_tempfile_leak(test_name)
|
||||||
|
GC.start if leaked1 || leaked2 || leaked3
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_fds
|
||||||
|
fd_dir = "/proc/self/fd"
|
||||||
|
if File.directory?(fd_dir)
|
||||||
|
require "-test-/dir"
|
||||||
|
fds = Dir.open(fd_dir) {|d|
|
||||||
|
a = d.grep(/\A\d+\z/, &:to_i)
|
||||||
|
if d.respond_to? :fileno
|
||||||
|
a -= [d.fileno]
|
||||||
|
end
|
||||||
|
a
|
||||||
|
}
|
||||||
|
fds.sort
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_fd_leak(test_name)
|
||||||
|
leaked = false
|
||||||
|
live1 = @fd_info
|
||||||
|
live2 = find_fds
|
||||||
|
fd_closed = live1 - live2
|
||||||
|
if !fd_closed.empty?
|
||||||
|
fd_closed.each {|fd|
|
||||||
|
puts "Closed file descriptor: #{test_name}: #{fd}"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
fd_leaked = live2 - live1
|
||||||
|
if !fd_leaked.empty?
|
||||||
|
leaked = true
|
||||||
|
h = {}
|
||||||
|
ObjectSpace.each_object(IO) {|io|
|
||||||
|
begin
|
||||||
|
autoclose = io.autoclose?
|
||||||
|
fd = io.fileno
|
||||||
|
rescue IOError # closed IO object
|
||||||
|
next
|
||||||
|
end
|
||||||
|
(h[fd] ||= []) << [io, autoclose]
|
||||||
|
}
|
||||||
|
fd_leaked.each {|fd|
|
||||||
|
str = ''
|
||||||
|
if h[fd]
|
||||||
|
str << ' :'
|
||||||
|
h[fd].map {|io, autoclose|
|
||||||
|
s = ' ' + io.inspect
|
||||||
|
s << "(not-autoclose)" if !autoclose
|
||||||
|
s
|
||||||
|
}.sort.each {|s|
|
||||||
|
str << s
|
||||||
|
}
|
||||||
|
end
|
||||||
|
puts "Leaked file descriptor: #{test_name}: #{fd}#{str}"
|
||||||
|
}
|
||||||
|
h.each {|fd, list|
|
||||||
|
next if list.length <= 1
|
||||||
|
if 1 < list.count {|io, autoclose| autoclose }
|
||||||
|
str = list.map {|io, autoclose| " #{io.inspect}" + (autoclose ? "(autoclose)" : "") }.sort.join
|
||||||
|
puts "Multiple autoclose IO object for a file descriptor:#{str}"
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
@fd_info = live2
|
||||||
|
return leaked
|
||||||
|
end
|
||||||
|
|
||||||
|
def extend_tempfile_counter
|
||||||
|
return if defined? LeakChecker::TempfileCounter
|
||||||
|
m = Module.new {
|
||||||
|
@count = 0
|
||||||
|
class << self
|
||||||
|
attr_accessor :count
|
||||||
|
end
|
||||||
|
|
||||||
|
def new(data)
|
||||||
|
LeakChecker::TempfileCounter.count += 1
|
||||||
|
super(data)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
LeakChecker.const_set(:TempfileCounter, m)
|
||||||
|
|
||||||
|
class << Tempfile::Remover
|
||||||
|
prepend LeakChecker::TempfileCounter
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_tempfiles(prev_count=-1)
|
||||||
|
return [prev_count, []] unless defined? Tempfile
|
||||||
|
extend_tempfile_counter
|
||||||
|
count = TempfileCounter.count
|
||||||
|
if prev_count == count
|
||||||
|
[prev_count, []]
|
||||||
|
else
|
||||||
|
tempfiles = ObjectSpace.each_object(Tempfile).find_all {|t| t.path }
|
||||||
|
[count, tempfiles]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_tempfile_leak(test_name)
|
||||||
|
return false, @tempfile_info unless defined? Tempfile
|
||||||
|
count1, initial_tempfiles = @tempfile_info
|
||||||
|
count2, current_tempfiles = find_tempfiles(count1)
|
||||||
|
leaked = false
|
||||||
|
tempfiles_leaked = current_tempfiles - initial_tempfiles
|
||||||
|
if !tempfiles_leaked.empty?
|
||||||
|
leaked = true
|
||||||
|
list = tempfiles_leaked.map {|t| t.inspect }.sort
|
||||||
|
list.each {|str|
|
||||||
|
puts "Leaked tempfile: #{test_name}: #{str}"
|
||||||
|
}
|
||||||
|
tempfiles_leaked.each {|t| t.close! }
|
||||||
|
end
|
||||||
|
@tempfile_info = [count2, initial_tempfiles]
|
||||||
|
return leaked
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_threads
|
||||||
|
Thread.list.find_all {|t|
|
||||||
|
t != Thread.current && t.alive?
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_thread_leak(test_name)
|
||||||
|
live1 = @thread_info
|
||||||
|
live2 = find_threads
|
||||||
|
thread_finished = live1 - live2
|
||||||
|
leaked = false
|
||||||
|
if !thread_finished.empty?
|
||||||
|
list = thread_finished.map {|t| t.inspect }.sort
|
||||||
|
list.each {|str|
|
||||||
|
puts "Finished thread: #{test_name}: #{str}"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
thread_leaked = live2 - live1
|
||||||
|
if !thread_leaked.empty?
|
||||||
|
leaked = true
|
||||||
|
list = thread_leaked.map {|t| t.inspect }.sort
|
||||||
|
list.each {|str|
|
||||||
|
puts "Leaked thread: #{test_name}: #{str}"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
@thread_info = live2
|
||||||
|
return leaked
|
||||||
|
end
|
||||||
|
end
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
require "optparse"
|
require "optparse"
|
||||||
require "rbconfig"
|
require "rbconfig"
|
||||||
|
require "leakchecker"
|
||||||
|
|
||||||
##
|
##
|
||||||
# Minimal (mostly drop-in) replacement for test-unit.
|
# Minimal (mostly drop-in) replacement for test-unit.
|
||||||
@ -933,7 +934,7 @@ module MiniTest
|
|||||||
filter === m || filter === "#{suite}##{m}"
|
filter === m || filter === "#{suite}##{m}"
|
||||||
}
|
}
|
||||||
|
|
||||||
leak_info = leak_check_init
|
leakchecker = LeakChecker.new
|
||||||
|
|
||||||
assertions = filtered_test_methods.map { |method|
|
assertions = filtered_test_methods.map { |method|
|
||||||
inst = suite.new method
|
inst = suite.new method
|
||||||
@ -948,7 +949,7 @@ module MiniTest
|
|||||||
print result
|
print result
|
||||||
puts if @verbose
|
puts if @verbose
|
||||||
|
|
||||||
leak_info = leak_check(inst, leak_info)
|
leakchecker.check("#{inst.class}\##{inst.__name__}")
|
||||||
|
|
||||||
inst._assertions
|
inst._assertions
|
||||||
}
|
}
|
||||||
@ -956,164 +957,6 @@ module MiniTest
|
|||||||
return assertions.size, assertions.inject(0) { |sum, n| sum + n }
|
return assertions.size, assertions.inject(0) { |sum, n| sum + n }
|
||||||
end
|
end
|
||||||
|
|
||||||
def leak_check_init
|
|
||||||
fd_info = find_fds
|
|
||||||
thread_info = find_threads
|
|
||||||
tempfile_info = find_tempfiles
|
|
||||||
[fd_info, thread_info, tempfile_info]
|
|
||||||
end
|
|
||||||
|
|
||||||
def leak_check(inst, info)
|
|
||||||
fd_info, thread_info, tempfile_info = info
|
|
||||||
leak_p_1, fd_info = check_fd_leak(inst, fd_info)
|
|
||||||
leak_p_2, thread_info = check_thread_leak(inst, thread_info)
|
|
||||||
leak_p_3, tempfile_info = check_tempfile_leak(inst, tempfile_info)
|
|
||||||
GC.start if leak_p_1 || leak_p_2 || leak_p_3
|
|
||||||
[fd_info, thread_info, tempfile_info]
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_threads
|
|
||||||
Thread.list.find_all {|t|
|
|
||||||
t != Thread.current && t.alive?
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_thread_leak(inst, live1)
|
|
||||||
live2 = find_threads
|
|
||||||
thread_finished = live1 - live2
|
|
||||||
leak_p = false
|
|
||||||
if !thread_finished.empty?
|
|
||||||
list = thread_finished.map {|t| t.inspect }.sort
|
|
||||||
list.each {|str|
|
|
||||||
puts "Finished thread: #{inst.class}\##{inst.__name__}: #{str}"
|
|
||||||
}
|
|
||||||
end
|
|
||||||
thread_leaked = live2 - live1
|
|
||||||
if !thread_leaked.empty?
|
|
||||||
leak_p = true
|
|
||||||
list = thread_leaked.map {|t| t.inspect }.sort
|
|
||||||
list.each {|str|
|
|
||||||
puts "Leaked thread: #{inst.class}\##{inst.__name__}: #{str}"
|
|
||||||
}
|
|
||||||
end
|
|
||||||
return leak_p, live2
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_fds
|
|
||||||
fd_dir = "/proc/self/fd"
|
|
||||||
if File.directory?(fd_dir)
|
|
||||||
require "-test-/dir"
|
|
||||||
fds = Dir.open(fd_dir) {|d|
|
|
||||||
a = d.grep(/\A\d+\z/, &:to_i)
|
|
||||||
if d.respond_to? :fileno
|
|
||||||
a -= [d.fileno]
|
|
||||||
end
|
|
||||||
a
|
|
||||||
}
|
|
||||||
fds.sort
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_fd_leak(inst, live1)
|
|
||||||
leak_p = false
|
|
||||||
live2 = find_fds
|
|
||||||
name = "#{inst.class}\##{inst.__name__}"
|
|
||||||
fd_closed = live1 - live2
|
|
||||||
if !fd_closed.empty?
|
|
||||||
fd_closed.each {|fd|
|
|
||||||
puts "Closed file descriptor: #{name}: #{fd}"
|
|
||||||
}
|
|
||||||
end
|
|
||||||
fd_leaked = live2 - live1
|
|
||||||
if !fd_leaked.empty?
|
|
||||||
leak_p = true
|
|
||||||
h = {}
|
|
||||||
ObjectSpace.each_object(IO) {|io|
|
|
||||||
begin
|
|
||||||
autoclose = io.autoclose?
|
|
||||||
fd = io.fileno
|
|
||||||
rescue IOError # closed IO object
|
|
||||||
next
|
|
||||||
end
|
|
||||||
(h[fd] ||= []) << [io, autoclose]
|
|
||||||
}
|
|
||||||
fd_leaked.each {|fd|
|
|
||||||
str = ''
|
|
||||||
if h[fd]
|
|
||||||
str << ' :'
|
|
||||||
h[fd].map {|io, autoclose|
|
|
||||||
s = ' ' + io.inspect
|
|
||||||
s << "(not-autoclose)" if !autoclose
|
|
||||||
s
|
|
||||||
}.sort.each {|s|
|
|
||||||
str << s
|
|
||||||
}
|
|
||||||
end
|
|
||||||
puts "Leaked file descriptor: #{name}: #{fd}#{str}"
|
|
||||||
}
|
|
||||||
h.each {|fd, list|
|
|
||||||
next if list.length <= 1
|
|
||||||
if 1 < list.count {|io, autoclose| autoclose }
|
|
||||||
str = list.map {|io, autoclose| " #{io.inspect}" + (autoclose ? "(autoclose)" : "") }.sort.join
|
|
||||||
puts "Multiple autoclose IO object for a file descriptor:#{str}"
|
|
||||||
end
|
|
||||||
}
|
|
||||||
end
|
|
||||||
return leak_p, live2
|
|
||||||
end
|
|
||||||
|
|
||||||
def extend_tempfile_counter
|
|
||||||
return if defined? ::MiniTest::TempfileCounter
|
|
||||||
m = Module.new {
|
|
||||||
@count = 0
|
|
||||||
class << self
|
|
||||||
attr_accessor :count
|
|
||||||
end
|
|
||||||
|
|
||||||
def new(data)
|
|
||||||
MiniTest::TempfileCounter.count += 1
|
|
||||||
super(data)
|
|
||||||
end
|
|
||||||
}
|
|
||||||
MiniTest.const_set(:TempfileCounter, m)
|
|
||||||
|
|
||||||
class << Tempfile::Remover
|
|
||||||
prepend MiniTest::TempfileCounter
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_tempfiles(prev_count=-1)
|
|
||||||
return [prev_count, []] unless defined? Tempfile
|
|
||||||
extend_tempfile_counter
|
|
||||||
count = TempfileCounter.count
|
|
||||||
if prev_count == count
|
|
||||||
[prev_count, []]
|
|
||||||
else
|
|
||||||
tempfiles = ObjectSpace.each_object(Tempfile).find_all {|t| t.path }
|
|
||||||
[count, tempfiles]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_tempfile_leak(inst, info)
|
|
||||||
return false, info unless defined? Tempfile
|
|
||||||
count1, initial_tempfiles = info
|
|
||||||
count2, current_tempfiles = find_tempfiles(count1)
|
|
||||||
leak_p = false
|
|
||||||
tempfiles_leaked = current_tempfiles - initial_tempfiles
|
|
||||||
if !tempfiles_leaked.empty?
|
|
||||||
name = "#{inst.class}\##{inst.__name__}"
|
|
||||||
leak_p = true
|
|
||||||
list = tempfiles_leaked.map {|t| t.inspect }.sort
|
|
||||||
list.each {|str|
|
|
||||||
puts "Leaked tempfile: #{name}: #{str}"
|
|
||||||
}
|
|
||||||
tempfiles_leaked.each {|t| t.close! }
|
|
||||||
end
|
|
||||||
return leak_p, [count2, initial_tempfiles]
|
|
||||||
end
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Record the result of a single test. Makes it very easy to gather
|
# Record the result of a single test. Makes it very easy to gather
|
||||||
# information. Eg:
|
# information. Eg:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user