diff options
Diffstat (limited to 'tool/lib/leakchecker.rb')
-rw-r--r-- | tool/lib/leakchecker.rb | 240 |
1 files changed, 240 insertions, 0 deletions
diff --git a/tool/lib/leakchecker.rb b/tool/lib/leakchecker.rb new file mode 100644 index 0000000000..af9200bf77 --- /dev/null +++ b/tool/lib/leakchecker.rb @@ -0,0 +1,240 @@ +# frozen_string_literal: true +class LeakChecker + def initialize + @fd_info = find_fds + @tempfile_info = find_tempfiles + @thread_info = find_threads + @env_info = find_env + @encoding_info = find_encodings + @old_verbose = $VERBOSE + end + + def check(test_name) + leaks = [ + check_fd_leak(test_name), + check_thread_leak(test_name), + check_tempfile_leak(test_name), + check_env(test_name), + check_encodings(test_name), + check_safe(test_name), + check_verbose(test_name), + ] + GC.start if leaks.any? + end + + def check_safe test_name + puts "#{test_name}: $SAFE == #{$SAFE}" unless $SAFE == 0 + end + + def check_verbose test_name + puts "#{test_name}: $VERBOSE == #{$VERBOSE}" unless @old_verbose == $VERBOSE + end + + def find_fds + if IO.respond_to?(:console) and (m = IO.method(:console)).arity.nonzero? + m[:close] + end + fd_dir = "/proc/self/fd" + 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 + } + 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| + inspect = io.inspect + begin + autoclose = io.autoclose? + fd = io.fileno + rescue IOError # closed IO object + next + end + (h[fd] ||= []) << [io, autoclose, inspect] + } + fd_leaked.each {|fd| + str = ''.dup + if h[fd] + str << ' :' + h[fd].map {|io, autoclose, inspect| + s = ' ' + inspect + s << "(not-autoclose)" if !autoclose + s + }.sort.each {|s| + str << s + } + end + puts "Leaked file descriptor: #{test_name}: #{fd}#{str}" + } + #system("lsof -p #$$") if !fd_leaked.empty? + h.each {|fd, list| + next if list.length <= 1 + if 1 < list.count {|io, autoclose, inspect| autoclose } + str = list.map {|io, autoclose, inspect| " #{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.instance_variable_defined?(:@tmpfile) and t.path + } + [count, tempfiles] + end + end + + def check_tempfile_leak(test_name) + return false 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 + + 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 + + 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 + + def puts(*a) + output = MiniTest::Unit.output + if defined?(output.set_encoding) + output.set_encoding(nil, nil) + end + output.puts(*a) + end +end |