2017-06-20 07:10:37 -04:00
|
|
|
# frozen_string_literal: true
|
2014-05-31 09:31:32 -04:00
|
|
|
class LeakChecker
|
|
|
|
def initialize
|
|
|
|
@fd_info = find_fds
|
|
|
|
@tempfile_info = find_tempfiles
|
|
|
|
@thread_info = find_threads
|
2015-03-12 10:01:00 -04:00
|
|
|
@env_info = find_env
|
2017-12-12 16:32:13 -05:00
|
|
|
@encoding_info = find_encodings
|
2018-03-30 22:29:19 -04:00
|
|
|
@old_verbose = $VERBOSE
|
2019-12-26 23:06:31 -05:00
|
|
|
@old_warning_flags = find_warning_flags
|
2014-05-31 09:31:32 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def check(test_name)
|
2020-03-03 21:25:43 -05:00
|
|
|
if /i386-solaris/ =~ RUBY_PLATFORM && /TestGem/ =~ test_name
|
|
|
|
GC.verify_internal_consistency
|
|
|
|
end
|
|
|
|
|
2015-06-13 15:42:03 -04:00
|
|
|
leaks = [
|
|
|
|
check_fd_leak(test_name),
|
|
|
|
check_thread_leak(test_name),
|
|
|
|
check_tempfile_leak(test_name),
|
2017-12-12 16:32:13 -05:00
|
|
|
check_env(test_name),
|
|
|
|
check_encodings(test_name),
|
2018-03-30 22:29:19 -04:00
|
|
|
check_verbose(test_name),
|
2019-12-26 23:06:31 -05:00
|
|
|
check_warning_flags(test_name),
|
2015-06-13 15:42:03 -04:00
|
|
|
]
|
|
|
|
GC.start if leaks.any?
|
2014-05-31 09:31:32 -04:00
|
|
|
end
|
|
|
|
|
2018-03-30 22:29:19 -04:00
|
|
|
def check_verbose test_name
|
|
|
|
puts "#{test_name}: $VERBOSE == #{$VERBOSE}" unless @old_verbose == $VERBOSE
|
|
|
|
end
|
|
|
|
|
2014-05-31 09:31:32 -04:00
|
|
|
def find_fds
|
2015-07-08 07:35:59 -04:00
|
|
|
if IO.respond_to?(:console) and (m = IO.method(:console)).arity.nonzero?
|
|
|
|
m[:close]
|
|
|
|
end
|
2020-05-05 21:09:29 -04:00
|
|
|
%w"/proc/self/fd /dev/fd".each do |fd_dir|
|
|
|
|
if File.directory?(fd_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
|
|
|
|
}
|
|
|
|
return fds.sort
|
|
|
|
end
|
2014-05-31 09:31:32 -04:00
|
|
|
end
|
2020-05-05 21:09:29 -04:00
|
|
|
[]
|
2014-05-31 09:31:32 -04:00
|
|
|
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|
|
2014-06-28 08:51:48 -04:00
|
|
|
inspect = io.inspect
|
2014-05-31 09:31:32 -04:00
|
|
|
begin
|
|
|
|
autoclose = io.autoclose?
|
|
|
|
fd = io.fileno
|
|
|
|
rescue IOError # closed IO object
|
|
|
|
next
|
|
|
|
end
|
2014-06-28 08:51:48 -04:00
|
|
|
(h[fd] ||= []) << [io, autoclose, inspect]
|
2014-05-31 09:31:32 -04:00
|
|
|
}
|
|
|
|
fd_leaked.each {|fd|
|
2017-06-20 07:43:05 -04:00
|
|
|
str = ''.dup
|
2019-11-11 11:30:42 -05:00
|
|
|
pos = nil
|
2014-05-31 09:31:32 -04:00
|
|
|
if h[fd]
|
|
|
|
str << ' :'
|
2014-06-28 08:51:48 -04:00
|
|
|
h[fd].map {|io, autoclose, inspect|
|
2019-11-11 11:30:42 -05:00
|
|
|
if ENV["LEAK_CHECKER_TRACE_OBJECT_ALLOCATION"]
|
|
|
|
pos = "#{ObjectSpace.allocation_sourcefile(io)}:#{ObjectSpace.allocation_sourceline(io)}"
|
|
|
|
end
|
2014-06-28 08:51:48 -04:00
|
|
|
s = ' ' + inspect
|
2014-05-31 09:31:32 -04:00
|
|
|
s << "(not-autoclose)" if !autoclose
|
|
|
|
s
|
|
|
|
}.sort.each {|s|
|
|
|
|
str << s
|
|
|
|
}
|
2020-05-06 00:13:10 -04:00
|
|
|
else
|
|
|
|
begin
|
|
|
|
io = IO.for_fd(fd, autoclose: false)
|
|
|
|
s = io.stat
|
|
|
|
rescue Errno::EBADF
|
|
|
|
# something un-stat-able
|
|
|
|
next
|
|
|
|
else
|
|
|
|
next if /darwin/ =~ RUBY_PLATFORM and [0, -1].include?(s.dev)
|
|
|
|
str << ' ' << s.inspect
|
|
|
|
ensure
|
|
|
|
io.close
|
|
|
|
end
|
2014-05-31 09:31:32 -04:00
|
|
|
end
|
|
|
|
puts "Leaked file descriptor: #{test_name}: #{fd}#{str}"
|
2019-11-11 11:30:42 -05:00
|
|
|
puts " The IO was created at #{pos}" if pos
|
2014-05-31 09:31:32 -04:00
|
|
|
}
|
2014-11-01 10:12:11 -04:00
|
|
|
#system("lsof -p #$$") if !fd_leaked.empty?
|
2014-05-31 09:31:32 -04:00
|
|
|
h.each {|fd, list|
|
|
|
|
next if list.length <= 1
|
2014-06-28 08:51:48 -04:00
|
|
|
if 1 < list.count {|io, autoclose, inspect| autoclose }
|
|
|
|
str = list.map {|io, autoclose, inspect| " #{inspect}" + (autoclose ? "(autoclose)" : "") }.sort.join
|
2014-05-31 09:31:32 -04:00
|
|
|
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
|
2017-01-27 00:01:18 -05:00
|
|
|
tempfiles = ObjectSpace.each_object(Tempfile).find_all {|t|
|
2018-08-14 18:59:57 -04:00
|
|
|
t.instance_variable_defined?(:@tmpfile) and t.path
|
2017-01-27 00:01:18 -05:00
|
|
|
}
|
2014-05-31 09:31:32 -04:00
|
|
|
[count, tempfiles]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def check_tempfile_leak(test_name)
|
2015-06-13 15:41:25 -04:00
|
|
|
return false unless defined? Tempfile
|
2014-05-31 09:31:32 -04:00
|
|
|
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|
|
2016-01-05 01:09:17 -05:00
|
|
|
t != Thread.current && t.alive?
|
2014-05-31 09:31:32 -04:00
|
|
|
}
|
|
|
|
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
|
2014-07-02 03:59:20 -04:00
|
|
|
|
2015-03-12 10:01:00 -04:00
|
|
|
def find_env
|
|
|
|
ENV.to_h
|
|
|
|
end
|
|
|
|
|
|
|
|
def check_env(test_name)
|
|
|
|
old_env = @env_info
|
|
|
|
new_env = ENV.to_h
|
|
|
|
return false if old_env == new_env
|
|
|
|
(old_env.keys | new_env.keys).sort.each {|k|
|
|
|
|
if old_env.has_key?(k)
|
|
|
|
if new_env.has_key?(k)
|
|
|
|
if old_env[k] != new_env[k]
|
|
|
|
puts "Environment variable changed: #{test_name} : #{k.inspect} changed : #{old_env[k].inspect} -> #{new_env[k].inspect}"
|
|
|
|
end
|
|
|
|
else
|
|
|
|
puts "Environment variable changed: #{test_name} : #{k.inspect} deleted"
|
|
|
|
end
|
|
|
|
else
|
|
|
|
if new_env.has_key?(k)
|
|
|
|
puts "Environment variable changed: #{test_name} : #{k.inspect} added"
|
|
|
|
else
|
|
|
|
flunk "unreachable"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
}
|
|
|
|
@env_info = new_env
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
2017-12-12 16:32:13 -05:00
|
|
|
def find_encodings
|
|
|
|
[Encoding.default_internal, Encoding.default_external]
|
|
|
|
end
|
|
|
|
|
|
|
|
def check_encodings(test_name)
|
|
|
|
old_internal, old_external = @encoding_info
|
|
|
|
new_internal, new_external = find_encodings
|
|
|
|
leaked = false
|
|
|
|
if new_internal != old_internal
|
|
|
|
leaked = true
|
|
|
|
puts "Encoding.default_internal changed: #{test_name} : #{old_internal.inspect} to #{new_internal.inspect}"
|
|
|
|
end
|
|
|
|
if new_external != old_external
|
|
|
|
leaked = true
|
|
|
|
puts "Encoding.default_external changed: #{test_name} : #{old_external.inspect} to #{new_external.inspect}"
|
|
|
|
end
|
|
|
|
@encoding_info = [new_internal, new_external]
|
|
|
|
return leaked
|
|
|
|
end
|
|
|
|
|
2019-12-26 23:06:31 -05:00
|
|
|
WARNING_CATEGORIES = %i[deprecated experimental].freeze
|
|
|
|
|
|
|
|
def find_warning_flags
|
|
|
|
WARNING_CATEGORIES.to_h do |category|
|
|
|
|
[category, Warning[category]]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def check_warning_flags(test_name)
|
|
|
|
new_warning_flags = find_warning_flags
|
|
|
|
leaked = false
|
|
|
|
WARNING_CATEGORIES.each do |category|
|
|
|
|
if new_warning_flags[category] != @old_warning_flags[category]
|
|
|
|
leaked = true
|
|
|
|
puts "Warning[#{category.inspect}] changed: #{test_name} : #{@old_warning_flags[category]} to #{new_warning_flags[category]}"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return leaked
|
|
|
|
end
|
|
|
|
|
2014-07-02 03:59:20 -04:00
|
|
|
def puts(*a)
|
2017-11-25 21:11:24 -05:00
|
|
|
output = MiniTest::Unit.output
|
|
|
|
if defined?(output.set_encoding)
|
|
|
|
output.set_encoding(nil, nil)
|
|
|
|
end
|
|
|
|
output.puts(*a)
|
2014-07-02 03:59:20 -04:00
|
|
|
end
|
2014-05-31 09:31:32 -04:00
|
|
|
end
|