summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog5
-rw-r--r--test/lib/leakchecker.rb159
-rw-r--r--test/lib/minitest/unit.rb163
3 files changed, 167 insertions, 160 deletions
diff --git a/ChangeLog b/ChangeLog
index 8381daa200..ea54cd0fae 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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>
* thread.c (rb_thread_atfork_internal): My compiler complains
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: