1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
|
# frozen_string_literal: true
class LeakChecker
@@try_lsof = nil # not-tried-yet
def initialize
@fd_info = find_fds
@@skip = false
@tempfile_info = find_tempfiles
@thread_info = find_threads
@env_info = find_env
@encoding_info = find_encodings
@old_verbose = $VERBOSE
@old_warning_flags = find_warning_flags
end
def check(test_name)
if /i386-solaris/ =~ RUBY_PLATFORM && /TestGem/ =~ test_name
GC.verify_internal_consistency
end
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_verbose(test_name),
check_warning_flags(test_name),
]
GC.start if leaks.any?
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
%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
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 !@@skip && !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.select! {|fd|
str = ''.dup
pos = nil
if h[fd]
str << ' :'
h[fd].map {|io, autoclose, inspect|
if ENV["LEAK_CHECKER_TRACE_OBJECT_ALLOCATION"]
pos = "#{ObjectSpace.allocation_sourcefile(io)}:#{ObjectSpace.allocation_sourceline(io)}"
end
s = ' ' + inspect
s << "(not-autoclose)" if !autoclose
s
}.sort.each {|s|
str << s
}
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
end
puts "Leaked file descriptor: #{test_name}: #{fd}#{str}"
puts " The IO was created at #{pos}" if pos
true
}
unless fd_leaked.empty?
unless @@try_lsof == false
@@try_lsof |= system(*%W[lsof -a -d #{fd_leaked.minmax.uniq.join("-")} -p #$$], out: Test::Unit::Runner.output)
end
end
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 objects for a file descriptor in: #{test_name}: #{str}"
end
}
end
@fd_info = live2
@@skip = false
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
e = ENV["_Ruby_Env_Ignorecase_"], ENV["_RUBY_ENV_IGNORECASE_"]
begin
ENV["_Ruby_Env_Ignorecase_"] = ENV["_RUBY_ENV_IGNORECASE_"] = nil
ENV["_RUBY_ENV_IGNORECASE_"] = "ENV_CASE_TEST"
ENV_IGNORECASE = ENV["_Ruby_Env_Ignorecase_"] == "ENV_CASE_TEST"
ensure
ENV["_Ruby_Env_Ignorecase_"], ENV["_RUBY_ENV_IGNORECASE_"] = e
end
if ENV_IGNORECASE
def find_env
ENV.to_h {|k, v| [k.upcase, v]}
end
else
def find_env
ENV.to_h
end
end
def check_env(test_name)
old_env = @env_info
new_env = find_env
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_internal,
'Encoding.default_external' => Encoding.default_external,
'STDIN.internal_encoding' => STDIN.internal_encoding,
'STDIN.external_encoding' => STDIN.external_encoding,
'STDOUT.internal_encoding' => STDOUT.internal_encoding,
'STDOUT.external_encoding' => STDOUT.external_encoding,
'STDERR.internal_encoding' => STDERR.internal_encoding,
'STDERR.external_encoding' => STDERR.external_encoding,
}
end
def check_encodings(test_name)
old_encoding_info = @encoding_info
@encoding_info = find_encodings
leaked = false
@encoding_info.each do |key, new_encoding|
old_encoding = old_encoding_info[key]
if new_encoding != old_encoding
leaked = true
puts "#{key} changed: #{test_name} : #{old_encoding.inspect} to #{new_encoding.inspect}"
end
end
leaked
end
WARNING_CATEGORIES = (Warning.respond_to?(:[]) ? %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
def puts(*a)
output = Test::Unit::Runner.output
if defined?(output.set_encoding)
output.set_encoding(nil, nil)
end
output.puts(*a)
end
def self.skip
@@skip = true
end
end
|