1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@67109 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
This commit is contained in:
eregon 2019-02-21 15:38:36 +00:00
parent 8b94ce988b
commit 92d3a72624
3 changed files with 141 additions and 61 deletions

View file

@ -0,0 +1,79 @@
class ConstantsLockFile
LOCK_FILE_NAME = '.mspec.constants'
def self.load
if File.exist?(LOCK_FILE_NAME)
File.readlines(LOCK_FILE_NAME).map(&:chomp)
else
[]
end
end
def self.dump(ary)
contents = ary.map(&:to_s).uniq.sort.join("\n") + "\n"
File.write(LOCK_FILE_NAME, contents)
end
end
class ConstantLeakError < StandardError
end
class ConstantsLeakCheckerAction
def initialize(save)
@save = save
@check = !save
@constants_locked = ConstantsLockFile.load
@exclude_patterns = MSpecScript.get(:toplevel_constants_excludes) || []
end
def register
MSpec.register :start, self
MSpec.register :before, self
MSpec.register :after, self
MSpec.register :finish, self
end
def start
@constants_start = constants_now
end
def before(state)
@constants_before = constants_now
end
def after(state)
constants = remove_excludes(constants_now - @constants_before - @constants_locked)
if @check && !constants.empty?
MSpec.protect 'Constants leak check' do
raise ConstantLeakError, "Top level constants leaked: #{constants.join(', ')}"
end
end
end
def finish
constants = remove_excludes(constants_now - @constants_start - @constants_locked)
if @save
ConstantsLockFile.dump(@constants_locked + constants)
end
if @check && !constants.empty?
MSpec.protect 'Global constants leak check' do
raise ConstantLeakError, "Top level constants leaked in the whole test suite: #{constants.join(', ')}"
end
end
end
private
def constants_now
Object.constants.map(&:to_s)
end
def remove_excludes(constants)
constants.reject { |name|
@exclude_patterns.any? { |pattern| pattern === name }
}
end
end

View file

@ -24,7 +24,12 @@
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
class LeakError < StandardError
end
class LeakChecker
attr_reader :leaks
def initialize
@fd_info = find_fds
@tempfile_info = find_tempfiles
@ -34,19 +39,18 @@ class LeakChecker
@encoding_info = find_encodings
end
def check(test_name)
@no_leaks = true
leaks = [
check_fd_leak(test_name),
check_tempfile_leak(test_name),
check_thread_leak(test_name),
check_process_leak(test_name),
check_env(test_name),
check_argv(test_name),
check_encodings(test_name)
]
GC.start if leaks.any?
return leaks.none?
def check(state)
@state = state
@leaks = []
check_fd_leak
check_tempfile_leak
check_thread_leak
check_process_leak
check_env
check_argv
check_encodings
GC.start if !@leaks.empty?
@leaks.empty?
end
private
@ -66,8 +70,7 @@ class LeakChecker
end
end
def check_fd_leak(test_name)
leaked = false
def check_fd_leak
live1 = @fd_info
if IO.respond_to?(:console) and (m = IO.method(:console)).arity.nonzero?
m[:close]
@ -76,12 +79,11 @@ class LeakChecker
fd_closed = live1 - live2
if !fd_closed.empty?
fd_closed.each {|fd|
puts "Closed file descriptor: #{test_name}: #{fd}"
leak "Closed file descriptor: #{fd}"
}
end
fd_leaked = live2 - live1
if !fd_leaked.empty?
leaked = true
h = {}
ObjectSpace.each_object(IO) {|io|
inspect = io.inspect
@ -105,19 +107,18 @@ class LeakChecker
str << s
}
end
puts "Leaked file descriptor: #{test_name}: #{fd}#{str}"
leak "Leaked file descriptor: #{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}"
leak "Multiple autoclose IO object for a file descriptor:#{str}"
end
}
end
@fd_info = live2
return leaked
end
def extend_tempfile_counter
@ -152,22 +153,19 @@ class LeakChecker
end
end
def check_tempfile_leak(test_name)
def check_tempfile_leak
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}"
leak "Leaked tempfile: #{str}"
}
tempfiles_leaked.each {|t| t.close! }
end
@tempfile_info = [count2, initial_tempfiles]
return leaked
end
def find_threads
@ -176,108 +174,98 @@ class LeakChecker
}
end
def check_thread_leak(test_name)
def check_thread_leak
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}"
leak "Finished thread: #{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}"
leak "Leaked thread: #{str}"
}
end
@thread_info = live2
return leaked
end
def check_process_leak(test_name)
def check_process_leak
subprocesses_leaked = Process.waitall
subprocesses_leaked.each { |pid, status|
puts "Leaked subprocess: #{pid}: #{status}"
leak "Leaked subprocess: #{pid}: #{status}"
}
return !subprocesses_leaked.empty?
end
def find_env
ENV.to_h
end
def check_env(test_name)
def check_env
old_env = @env_info
new_env = find_env
return false if old_env == new_env
return 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}"
leak "Environment variable changed : #{k.inspect} changed : #{old_env[k].inspect} -> #{new_env[k].inspect}"
end
else
puts "Environment variable changed: #{test_name} : #{k.inspect} deleted"
leak "Environment variable changed: #{k.inspect} deleted"
end
else
if new_env.has_key?(k)
puts "Environment variable changed: #{test_name} : #{k.inspect} added"
leak "Environment variable changed: #{k.inspect} added"
else
flunk "unreachable"
end
end
}
@env_info = new_env
return true
end
def find_argv
ARGV.map { |e| e.dup }
end
def check_argv(test_name)
def check_argv
old_argv = @argv_info
new_argv = find_argv
leaked = false
if new_argv != old_argv
puts "ARGV changed: #{test_name} : #{old_argv.inspect} to #{new_argv.inspect}"
leak "ARGV changed: #{old_argv.inspect} to #{new_argv.inspect}"
@argv_info = new_argv
leaked = true
end
return leaked
end
def find_encodings
[Encoding.default_internal, Encoding.default_external]
end
def check_encodings(test_name)
def check_encodings
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}"
leak "Encoding.default_internal changed: #{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}"
leak "Encoding.default_external changed: #{old_external.inspect} to #{new_external.inspect}"
end
@encoding_info = [new_internal, new_external]
return leaked
end
def puts(*args)
if @no_leaks
@no_leaks = false
print "\n"
def leak(message)
if @leaks.empty?
$stderr.puts "\n"
$stderr.puts @state.description
end
super(*args)
@leaks << message
$stderr.puts message
end
end
@ -292,9 +280,14 @@ class LeakCheckerAction
end
def after(state)
unless @checker.check(state.description)
unless @checker.check(state)
leak_messages = @checker.leaks
location = state.description
if state.example
puts state.example.source_location.join(':')
location = "#{location}\n#{state.example.source_location.join(':')}"
end
MSpec.protect(location) do
raise LeakError, leak_messages.join("\n")
end
end
end

View file

@ -1,7 +1,11 @@
require 'mspec/expectations/expectations'
require 'mspec/runner/actions/timer'
require 'mspec/runner/actions/tally'
require 'mspec/runner/actions/leakchecker' if ENV['CHECK_LEAKS']
if ENV['CHECK_LEAKS']
require 'mspec/runner/actions/leakchecker'
require 'mspec/runner/actions/constants_leak_checker'
end
class DottedFormatter
attr_reader :exceptions, :timer, :tally
@ -25,7 +29,11 @@ class DottedFormatter
def register
(@timer = TimerAction.new).register
(@tally = TallyAction.new).register
LeakCheckerAction.new.register if ENV['CHECK_LEAKS']
if ENV['CHECK_LEAKS']
save = ENV['CHECK_LEAKS'] == 'save'
LeakCheckerAction.new.register
ConstantsLeakCheckerAction.new(save).register
end
@counter = @tally.counter
MSpec.register :exception, self