From 6c6f9b19b4bc6256b4605b35f25fd9b4b530e0ba Mon Sep 17 00:00:00 2001 From: akr Date: Sat, 31 May 2014 13:31:32 +0000 Subject: * 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 --- test/lib/leakchecker.rb | 159 ++++++++++++++++++++++++++++++++++++++++++++ test/lib/minitest/unit.rb | 163 +--------------------------------------------- 2 files changed, 162 insertions(+), 160 deletions(-) create mode 100644 test/lib/leakchecker.rb (limited to 'test/lib') diff --git a/test/lib/leakchecker.rb b/test/lib/leakchecker.rb new file mode 100644 index 0000000000..02fffb2e42 --- /dev/null +++ b/test/lib/leakchecker.rb @@ -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 diff --git a/test/lib/minitest/unit.rb b/test/lib/minitest/unit.rb index 3b7312359b..52d757b7f7 100644 --- a/test/lib/minitest/unit.rb +++ b/test/lib/minitest/unit.rb @@ -2,6 +2,7 @@ require "optparse" require "rbconfig" +require "leakchecker" ## # Minimal (mostly drop-in) replacement for test-unit. @@ -933,7 +934,7 @@ module MiniTest filter === m || filter === "#{suite}##{m}" } - leak_info = leak_check_init + leakchecker = LeakChecker.new assertions = filtered_test_methods.map { |method| inst = suite.new method @@ -948,7 +949,7 @@ module MiniTest print result puts if @verbose - leak_info = leak_check(inst, leak_info) + leakchecker.check("#{inst.class}\##{inst.__name__}") inst._assertions } @@ -956,164 +957,6 @@ module MiniTest return assertions.size, assertions.inject(0) { |sum, n| sum + n } 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 # information. Eg: -- cgit v1.2.3