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
Jeremy Evans 08759edea8
Remove autoload for constant if the autoload fails
Previously, if an autoload failed (the file was loaded, but the
constant was not defined by the autoloaded file). Ruby will try
to autoload again if you delete the autoloaded file from
$LOADED_FEATURES.  With this change, the autoload and the
constant itself are removed as soon as it fails.

To handle cases where multiple threads are autoloading, when
deleting an autoload, handle the case where another thread
already deleted it.

Fixes [Bug #15790]
2021-10-08 14:54:26 -07:00

494 lines
14 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'
tmpfile2 = tmpdir + '/bar.rb'
a = Module.new do
autoload :X, tmpfile
autoload :Y, tmpfile2
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)
assert_equal(tmpfile, a.autoload?(:X, false))
assert_equal(tmpfile, a.autoload?(:X, nil))
assert_nil(b.autoload?(:X, false))
assert_nil(b.autoload?(:X, nil))
assert_equal(true, a.const_defined?("Y"))
assert_equal(true, b.const_defined?("Y"))
assert_equal(tmpfile2, a.autoload?("Y"))
assert_equal(tmpfile2, b.autoload?("Y"))
}
end
def test_autoload_with_unqualified_file_name # [ruby-core:69206]
Object.send(:remove_const, :A) if Object.const_defined?(:A)
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, 'test-ruby-core-69206'
end
end
END
File.write("test-ruby-core-69206.rb", '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+"/test-bug-14469.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, "test-bug-14469.rb"
end
assert_raise(NameError, bug) {AutoloadTest::ZZZ}
end;
end
end
def test_autoload_deprecate_constant
Dir.mktmpdir('autoload') do |tmpdir|
File.write(tmpdir+"/test-bug-14469.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, "test-bug-14469.rb"
end
assert_warning(/ZZZ is deprecated/, bug) {AutoloadTest::ZZZ}
end;
end
end
def test_autoload_private_constant_before_autoload
Dir.mktmpdir('autoload') do |tmpdir|
File.write(tmpdir+"/test-bug-11055.rb", "#{<<~"begin;"}\n#{<<~'end;'}")
begin;
class AutoloadTest
ZZZ = :ZZZ
end
end;
assert_separately(%W[-I #{tmpdir}], "#{<<-"begin;"}\n#{<<-'end;'}")
bug = '[Bug #11055]'
begin;
class AutoloadTest
autoload :ZZZ, "test-bug-11055.rb"
private_constant :ZZZ
ZZZ
end
assert_raise(NameError, bug) {AutoloadTest::ZZZ}
end;
assert_separately(%W[-I #{tmpdir}], "#{<<-"begin;"}\n#{<<-'end;'}")
bug = '[Bug #11055]'
begin;
class AutoloadTest
autoload :ZZZ, "test-bug-11055.rb"
private_constant :ZZZ
end
assert_raise(NameError, bug) {AutoloadTest::ZZZ}
end;
end
end
def test_autoload_deprecate_constant_before_autoload
Dir.mktmpdir('autoload') do |tmpdir|
File.write(tmpdir+"/test-bug-11055.rb", "#{<<~"begin;"}\n#{<<~'end;'}")
begin;
class AutoloadTest
ZZZ = :ZZZ
end
end;
assert_separately(%W[-I #{tmpdir}], "#{<<-"begin;"}\n#{<<-'end;'}")
bug = '[Bug #11055]'
begin;
class AutoloadTest
autoload :ZZZ, "test-bug-11055.rb"
deprecate_constant :ZZZ
end
assert_warning(/ZZZ is deprecated/, bug) {class AutoloadTest; ZZZ; end}
assert_warning(/ZZZ is deprecated/, bug) {AutoloadTest::ZZZ}
end;
assert_separately(%W[-I #{tmpdir}], "#{<<-"begin;"}\n#{<<-'end;'}")
bug = '[Bug #11055]'
begin;
class AutoloadTest
autoload :ZZZ, "test-bug-11055.rb"
deprecate_constant :ZZZ
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}/test-bug-14742.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, 'test-bug-14742'
autoload :Bar, 'test-bug-14742'
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_autoload_same_file_with_raise
Dir.mktmpdir('autoload') do |tmpdir|
File.write("#{tmpdir}/test-bug-16177.rb", "#{<<~'begin;'}\n#{<<~'end;'}")
begin;
raise '[ruby-core:95055] [Bug #16177]'
end;
assert_raise(RuntimeError, '[ruby-core:95055] [Bug #16177]') do
assert_separately(%W[-I #{tmpdir}], "#{<<-'begin;'}\n#{<<-'end;'}")
begin;
autoload :Foo, 'test-bug-16177'
autoload :Bar, 'test-bug-16177'
t1 = Thread.new do Foo end
t2 = Thread.new do Bar end
t1.join
t2.join
end;
end
end
end
def test_source_location
bug = "Bug16764"
Dir.mktmpdir('autoload') do |tmpdir|
path = "#{tmpdir}/test-#{bug}.rb"
File.write(path, "C::#{bug} = __FILE__\n")
assert_separately(%W[-I #{tmpdir}], "#{<<-"begin;"}\n#{<<-"end;"}")
begin;
class C; end
C.autoload(:Bug16764, #{path.dump})
assert_equal [__FILE__, __LINE__-1], C.const_source_location(#{bug.dump})
assert_equal #{path.dump}, C.const_get(#{bug.dump})
assert_equal [#{path.dump}, 1], C.const_source_location(#{bug.dump})
end;
end
end
def test_no_memory_leak
assert_no_memory_leak([], '', "#{<<~"begin;"}\n#{<<~'end;'}", 'many autoloads', timeout: 60)
begin;
200000.times do |i|
m = Module.new
m.instance_eval do
autoload :Foo, 'x'
autoload :Bar, i.to_s
end
end
end;
end
def test_autoload_after_failed_and_removed_from_loaded_features
Dir.mktmpdir('autoload') do |tmpdir|
autoload_path = File.join(tmpdir, "test-bug-15790.rb")
File.write(autoload_path, '')
assert_separately(%W[-I #{tmpdir}], <<-RUBY)
path = #{File.realpath(autoload_path).inspect}
autoload :X, path
assert_equal(path, Object.autoload?(:X))
assert_raise(NameError){X}
assert_nil(Object.autoload?(:X))
assert_equal(false, Object.const_defined?(:X))
$LOADED_FEATURES.delete(path)
assert_equal(false, Object.const_defined?(:X))
assert_nil(Object.autoload?(:X))
assert_raise(NameError){X}
assert_equal(false, Object.const_defined?(:X))
assert_nil(Object.autoload?(:X))
RUBY
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)} if defined? Object::AutoloadTest
TestAutoload.class_eval {remove_const(:AutoloadTest)} if defined? TestAutoload::AutoloadTest
end
end