Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions class.c
Original file line number Diff line number Diff line change
Expand Up @@ -1843,6 +1843,10 @@ ensure_origin(VALUE klass)
{
VALUE origin = RCLASS_ORIGIN(klass);
if (origin == klass) {
/* Create the box-local classext before reading m_tbl, so that the
* origin shares the m_tbl with the box-local iclasses of klass,
* as rb_prepend_module relies on that identity. */
rb_class_ensure_writable(klass);
origin = class_alloc(T_ICLASS, klass);
RCLASS_SET_M_TBL(origin, RCLASS_M_TBL(klass));
rb_class_set_super(origin, RCLASS_SUPER(klass));
Expand Down
2 changes: 1 addition & 1 deletion lib/timeout.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Gem::Specification.new do |spec|

spec.files = Dir.chdir(__dir__) do
`git ls-files -z`.split("\x0").reject do |f|
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features|rakelib)/|\.(?:git|travis|circleci)|appveyor|Rakefile)})
(f == File.basename(__FILE__)) || f.match(%r{\A(?:(?:bin|test|spec|features|rakelib)/|\.(?:git|travis|circleci)|appveyor|Rakefile)})
end
end
spec.require_paths = ["lib"]
Expand Down
67 changes: 67 additions & 0 deletions test/ruby/test_box.rb
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,58 @@ def test_autoload_in_box
assert_raise(NameError) { BOX_B }
end

def test_autoload_dispatches_prepended_require
# Autoload must go through the `Kernel#require` decorations (Zeitwerk, RubyGems, etc.)
# of the box that registered the autoload, not `Ruby::Box#require` directly.
assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
begin;
FEATURE = "/nonexistent/virtual_feature"
module Decor
def require(path)
if path == FEATURE
Object.const_set(:AutoloadedFromDecorator, Module.new)
return true
end
super
end
end
Kernel.prepend(Decor)
Object.autoload(:AutoloadedFromDecorator, FEATURE)
assert_kind_of Module, AutoloadedFromDecorator
end;
end

def test_autoload_dispatches_prepended_require_of_the_registered_box
# Even when the autoload is triggered from outside, it must be dispatched to the
# (decorated) `Kernel#require` of the box that registered the autoload.
# --enable=gems because Kernel.prepend in a box without RubyGems has a separate
# ancestry ordering problem, and assert_separately runs with --disable=gems.
assert_in_out_err([ENV_ENABLE_BOX, "--enable=gems"], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error|
begin;
box = Ruby::Box.new
box.eval(<<~RUBY)
FEATURE = "/nonexistent/box_virtual_feature"
module BoxDecor
def require(path)
if path == FEATURE
Holder.const_set(:Virtual, "decorated in \#{Ruby::Box.current.inspect}")
return true
end
super
end
end
Kernel.prepend(BoxDecor)
module Holder
autoload :Virtual, FEATURE
end
RUBY
puts box::Holder::Virtual
end;
assert_equal 1, output.size
assert_match(/\Adecorated in #<Ruby::Box:\d+,user/, output.first)
end
end

def test_continuous_top_level_method_in_a_box
setup_box

Expand Down Expand Up @@ -372,6 +424,21 @@ def test_descendants_follow_ancestors_of_the_current_box
assert_include @box::BoxedString.ancestors, String
assert_include String.descendants, @box::BoxedString
end

def test_prepend_to_builtin_module_in_box
# Use --disable-gems to keep Kernel untouched in the new box, so that
# the prepend below is the first copy-on-write of Kernel's classext
assert_separately([ENV_ENABLE_BOX, '--disable-gems'], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
begin;
box = Ruby::Box.new
box.eval('module BoxDecor; def itself; :decorated; end; end; Kernel.prepend(BoxDecor)')
ancestors = box.eval('Object.ancestors')
assert_operator ancestors.index(box::BoxDecor), :<, ancestors.index(Kernel)
assert_equal :decorated, box.eval('Object.new.itself')
assert_not_include Object.ancestors, box::BoxDecor
assert_equal 42, 42.itself
end;
end
end

class TestBoxDescendantsMain
Expand Down
5 changes: 5 additions & 0 deletions test/rubygems/test_gem_rdoc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
require_relative "helper"
require "rubygems/rdoc"

# RDoc resolves its own dependencies lazily, on require. Load them here, while
# the real gem paths are still in effect, because Gem::TestCase#setup points
# GEM_HOME at an empty temporary directory where they cannot be found.
Gem::RDoc.load_rdoc if defined?(Gem::RDoc)

class TestGemRDoc < Gem::TestCase
def setup
super
Expand Down
33 changes: 27 additions & 6 deletions variable.c
Original file line number Diff line number Diff line change
Expand Up @@ -3164,10 +3164,16 @@ autoload_apply_constants(VALUE _arguments)
}

static VALUE
autoload_feature_require(VALUE _arguments)
autoload_feature_require_in_box(VALUE receiver, VALUE feature)
{
VALUE receiver = rb_vm_top_self();
rb_vm_frame_flag_set_box_require(GET_EC());

return rb_funcall(receiver, rb_intern("require"), 1, feature);
}

static VALUE
autoload_feature_require(VALUE _arguments)
{
struct autoload_load_arguments *arguments = (struct autoload_load_arguments*)_arguments;

struct autoload_const *autoload_const = arguments->autoload_const;
Expand All @@ -3176,9 +3182,6 @@ autoload_feature_require(VALUE _arguments)
// We save this for later use in autoload_apply_constants:
arguments->autoload_data = rb_check_typeddata(autoload_const->autoload_data_value, &autoload_data_type);

if (rb_box_available() && BOX_OBJ_P(autoload_box_value))
receiver = autoload_box_value;

/*
* Clear the global cc cache table because the require method can be different from the current
* box's one and it may cause inconsistent cc-cme states.
Expand All @@ -3187,7 +3190,25 @@ autoload_feature_require(VALUE _arguments)
*/
rb_gccct_clear_table();

VALUE result = rb_funcall(receiver, rb_intern("require"), 1, arguments->autoload_data->feature);
VALUE feature = arguments->autoload_data->feature;
rb_box_t *box = NULL;
if (rb_box_available() && BOX_OBJ_P(autoload_box_value)) {
box = rb_get_box_t(autoload_box_value);
}

VALUE result;
if (box && box->top_self) {
/*
* Call `require` on the top self of the box that registered the autoload, in a frame
* running in that box, so that `Kernel#require` decorations in the box (RubyGems,
* Zeitwerk, etc.) are dispatched and the feature is loaded into that box.
*/
result = rb_vm_call_cfunc_in_box(box->top_self, autoload_feature_require_in_box,
box->top_self, feature, feature, box);
}
else {
result = rb_funcall(rb_vm_top_self(), rb_intern("require"), 1, feature);
}

if (RTEST(result)) {
return rb_mutex_synchronize(autoload_mutex, autoload_apply_constants, _arguments);
Expand Down