1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00
ruby--ruby/test/ruby/test_autoload.rb
normal ec959fbb4b variable.c: fix multiple autoload with identical file (again)
We need to ensure autoload declarations pointing to the same
feature (aka "file") can wait on each other to avoid deadlock
situations.

So, reorganize autoload data structures to maintain a
feature => autoload_data_i mapping, and have module constant
tables point to the new autoload_const struct instead of
directly to autoload_data_i.  This allows multiple
autoload_const structs to refer to the SAME autoload_data_i
struct, and with it, the on-stack autoload_state.waitq.

The end result is different constants can share the same waitq
(tied to the feature name), and not deadlock each other during
loading.

Thanks to Eugene Kenny for the bug report and reproducible test case.

Reported-by: Eugene Kenny <elkenny@gmail.com>

* variable.c (autoload_featuremap): new global
  (struct autoload_const): new per-const struct
  (struct autoload_state): reference autoload_const instead of autoload_data_i
  (struct autoload_data_i): remove per-const
  (autoload_i_mark): delete from autoload_featuremap if unreferenced
  (autoload_c_mark): new dmark callback
  (autoload_c_free): new dfree callback
  (autoload_c_memsize): new memsize callback
  (autoload_const_type): new data type
  (get_autoload_data): set autoload_const as well
  (rb_autoload_str): use new data structures
  (autoload_delete): cleanup from autoload_featuremap
  (check_autoload_required): adjust for new internals
  (rb_autoloading_value): ditto
  (struct autoload_const_set_args): remove, redundant with autoload_const
  (const_tbl_update): adjust for new internals
  (autoload_const_set): ditto
  (autoload_require): ditto
  (autoload_reset): ditto
  (rb_autoload_load): ditto
  (rb_const_set): ditto
  (current_autoload_data): ditto
  (set_const_visibility): ditto
* test/ruby/test_autoload.rb (test_autoload_same_file): new test
  (test_no_leak): new test
  [ruby-core:86935] [Bug #14742]

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@63392 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2018-05-10 08:54:26 +00:00

359 lines
9.2 KiB
Ruby

# frozen_string_literal: false
require 'test/unit'
require 'tempfile'
class TestAutoload < Test::Unit::TestCase
def test_autoload_so
# Date is always available, unless excluded intentionally.
assert_in_out_err([], <<-INPUT, [], [])
autoload :Date, "date"
begin Date; rescue LoadError; end
INPUT
end
def test_non_realpath_in_loadpath
require 'tmpdir'
tmpdir = Dir.mktmpdir('autoload')
tmpdirs = [tmpdir]
tmpdirs.unshift(tmpdir + '/foo')
Dir.mkdir(tmpdirs[0])
tmpfiles = [tmpdir + '/foo.rb', tmpdir + '/foo/bar.rb']
open(tmpfiles[0] , 'w') do |f|
f.puts <<-INPUT
$:.unshift(File.expand_path('..', __FILE__)+'/./foo')
module Foo
autoload :Bar, 'bar'
end
p Foo::Bar
INPUT
end
open(tmpfiles[1], 'w') do |f|
f.puts 'class Foo::Bar; end'
end
assert_in_out_err([tmpfiles[0]], "", ["Foo::Bar"], [])
ensure
File.unlink(*tmpfiles) rescue nil if tmpfiles
tmpdirs.each {|dir| Dir.rmdir(dir)}
end
def test_autoload_p
bug4565 = '[ruby-core:35679]'
require 'tmpdir'
Dir.mktmpdir('autoload') {|tmpdir|
tmpfile = tmpdir + '/foo.rb'
a = Module.new do
autoload :X, tmpfile
end
b = Module.new do
include a
end
assert_equal(true, a.const_defined?(:X))
assert_equal(true, b.const_defined?(:X))
assert_equal(tmpfile, a.autoload?(:X), bug4565)
assert_equal(tmpfile, b.autoload?(:X), bug4565)
}
end
def test_autoload_with_unqualified_file_name # [ruby-core:69206]
lp = $LOAD_PATH.dup
lf = $LOADED_FEATURES.dup
Dir.mktmpdir('autoload') { |tmpdir|
$LOAD_PATH << tmpdir
Dir.chdir(tmpdir) do
eval <<-END
class ::Object
module A
autoload :C, 'b'
end
end
END
File.open('b.rb', 'w') {|file| file.puts 'module A; class C; end; end'}
assert_kind_of Class, ::A::C
end
}
ensure
$LOAD_PATH.replace lp
$LOADED_FEATURES.replace lf
Object.send(:remove_const, :A) if Object.const_defined?(:A)
end
def test_require_explicit
Tempfile.create(['autoload', '.rb']) {|file|
file.puts 'class Object; AutoloadTest = 1; end'
file.close
add_autoload(file.path)
begin
assert_nothing_raised do
assert(require file.path)
assert_equal(1, ::AutoloadTest)
end
ensure
remove_autoload_constant
end
}
end
def test_threaded_accessing_constant
# Suppress "warning: loading in progress, circular require considered harmful"
EnvUtil.default_warning {
Tempfile.create(['autoload', '.rb']) {|file|
file.puts 'sleep 0.5; class AutoloadTest; X = 1; end'
file.close
add_autoload(file.path)
begin
assert_nothing_raised do
t1 = Thread.new { ::AutoloadTest::X }
t2 = Thread.new { ::AutoloadTest::X }
[t1, t2].each(&:join)
end
ensure
remove_autoload_constant
end
}
}
end
def test_threaded_accessing_inner_constant
# Suppress "warning: loading in progress, circular require considered harmful"
EnvUtil.default_warning {
Tempfile.create(['autoload', '.rb']) {|file|
file.puts 'class AutoloadTest; sleep 0.5; X = 1; end'
file.close
add_autoload(file.path)
begin
assert_nothing_raised do
t1 = Thread.new { ::AutoloadTest::X }
t2 = Thread.new { ::AutoloadTest::X }
[t1, t2].each(&:join)
end
ensure
remove_autoload_constant
end
}
}
end
def test_nameerror_when_autoload_did_not_define_the_constant
Tempfile.create(['autoload', '.rb']) {|file|
file.puts ''
file.close
add_autoload(file.path)
begin
assert_raise(NameError) do
AutoloadTest
end
ensure
remove_autoload_constant
end
}
end
def test_override_autoload
Tempfile.create(['autoload', '.rb']) {|file|
file.puts ''
file.close
add_autoload(file.path)
begin
eval %q(class AutoloadTest; end)
assert_equal(Class, AutoloadTest.class)
ensure
remove_autoload_constant
end
}
end
def test_override_while_autoloading
Tempfile.create(['autoload', '.rb']) {|file|
file.puts 'class AutoloadTest; sleep 0.5; end'
file.close
add_autoload(file.path)
begin
# while autoloading...
t = Thread.new { AutoloadTest }
sleep 0.1
# override it
EnvUtil.suppress_warning {
eval %q(AutoloadTest = 1)
}
t.join
assert_equal(1, AutoloadTest)
ensure
remove_autoload_constant
end
}
end
def ruby_impl_require
Kernel.module_eval do
alias old_require require
end
called_with = []
Kernel.send :define_method, :require do |path|
called_with << path
old_require path
end
yield called_with
ensure
Kernel.module_eval do
undef require
alias require old_require
undef old_require
end
end
def test_require_implemented_in_ruby_is_called
ruby_impl_require do |called_with|
Tempfile.create(['autoload', '.rb']) {|file|
file.puts 'class AutoloadTest; end'
file.close
add_autoload(file.path)
begin
assert(Object::AutoloadTest)
ensure
remove_autoload_constant
end
assert_equal [file.path], called_with
}
end
end
def test_autoload_while_autoloading
ruby_impl_require do |called_with|
Tempfile.create(%w(a .rb)) do |a|
Tempfile.create(%w(b .rb)) do |b|
a.puts "require '#{b.path}'; class AutoloadTest; end"
b.puts "class AutoloadTest; module B; end; end"
[a, b].each(&:flush)
add_autoload(a.path)
begin
assert(Object::AutoloadTest)
ensure
remove_autoload_constant
end
assert_equal [a.path, b.path], called_with
end
end
end
end
def test_bug_13526
script = File.join(__dir__, 'bug-13526.rb')
assert_ruby_status([script], '', '[ruby-core:81016] [Bug #13526]')
end
def test_autoload_private_constant
Dir.mktmpdir('autoload') do |tmpdir|
File.write(tmpdir+"/zzz.rb", "#{<<~"begin;"}\n#{<<~'end;'}")
begin;
class AutoloadTest
ZZZ = :ZZZ
private_constant :ZZZ
end
end;
assert_separately(%W[-I #{tmpdir}], "#{<<-"begin;"}\n#{<<-'end;'}")
bug = '[ruby-core:85516] [Bug #14469]'
begin;
class AutoloadTest
autoload :ZZZ, "zzz.rb"
end
assert_raise(NameError, bug) {AutoloadTest::ZZZ}
end;
end
end
def test_autoload_deprecate_constant
Dir.mktmpdir('autoload') do |tmpdir|
File.write(tmpdir+"/zzz.rb", "#{<<~"begin;"}\n#{<<~'end;'}")
begin;
class AutoloadTest
ZZZ = :ZZZ
deprecate_constant :ZZZ
end
end;
assert_separately(%W[-I #{tmpdir}], "#{<<-"begin;"}\n#{<<-'end;'}")
bug = '[ruby-core:85516] [Bug #14469]'
begin;
class AutoloadTest
autoload :ZZZ, "zzz.rb"
end
assert_warning(/ZZZ is deprecated/, bug) {AutoloadTest::ZZZ}
end;
end
end
def test_autoload_fork
EnvUtil.default_warning do
Tempfile.create(['autoload', '.rb']) {|file|
file.puts 'sleep 0.3; class AutoloadTest; end'
file.close
add_autoload(file.path)
begin
thrs = []
3.times do
thrs << Thread.new { AutoloadTest; nil }
thrs << Thread.new { fork { AutoloadTest } }
end
thrs.each(&:join)
thrs.each do |th|
pid = th.value or next
_, status = Process.waitpid2(pid)
assert_predicate status, :success?
end
ensure
remove_autoload_constant
assert_nil $!, '[ruby-core:86410] [Bug #14634]'
end
}
end
end if Process.respond_to?(:fork)
def test_autoload_same_file
Dir.mktmpdir('autoload') do |tmpdir|
File.write("#{tmpdir}/b.rb", "#{<<~'begin;'}\n#{<<~'end;'}")
begin;
module Foo; end
module Bar; end
end;
3.times do # timing-dependent, needs a few times to hit [Bug #14742]
assert_separately(%W[-I #{tmpdir}], "#{<<-'begin;'}\n#{<<-'end;'}")
begin;
autoload :Foo, 'b'
autoload :Bar, 'b'
t1 = Thread.new do Foo end
t2 = Thread.new do Bar end
t1.join
t2.join
bug = '[ruby-core:86935] [Bug #14742]'
assert_instance_of Module, t1.value, bug
assert_instance_of Module, t2.value, bug
end;
end
end
end
def test_no_leak
assert_no_memory_leak([], '', <<~'end;', 'many autoloads', timeout: 30)
200000.times do |i|
m = Module.new
m.instance_eval do
autoload :Foo, 'x'
autoload :Bar, i.to_s
end
end
end;
end
def add_autoload(path)
(@autoload_paths ||= []) << path
::Object.class_eval {autoload(:AutoloadTest, path)}
end
def remove_autoload_constant
$".replace($" - @autoload_paths)
::Object.class_eval {remove_const(:AutoloadTest)}
end
end