Skip to content

Commit

Permalink
Merge pull request #3639 from rubygems/require_perf_regression
Browse files Browse the repository at this point in the history
Fix correctness and performance regression in `require`
  • Loading branch information
deivid-rodriguez committed May 24, 2020
2 parents cbbc533 + db872c7 commit 54a4cdb
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 40 deletions.
21 changes: 12 additions & 9 deletions lib/rubygems.rb
Original file line number Diff line number Diff line change
Expand Up @@ -590,22 +590,25 @@ def self.load_path_insert_index

index = $LOAD_PATH.index RbConfig::CONFIG['sitelibdir']

index
index || 0
end

##
# The number of paths in the `$LOAD_PATH` from activated gems. Used to
# prioritize `-I` and `ENV['RUBYLIB`]` entries during `require`.

def self.activated_gem_paths
@activated_gem_paths ||= 0
end

##
# Add a list of paths to the $LOAD_PATH at the proper place.

def self.add_to_load_path(*paths)
insert_index = load_path_insert_index
@activated_gem_paths = activated_gem_paths + paths.size

if insert_index
# gem directories must come after -I and ENV['RUBYLIB']
$LOAD_PATH.insert(insert_index, *paths)
else
# we are probably testing in core, -I and RUBYLIB don't apply
$LOAD_PATH.unshift(*paths)
end
# gem directories must come after -I and ENV['RUBYLIB']
$LOAD_PATH.insert(Gem.load_path_insert_index, *paths)
end

@yaml_loaded = false
Expand Down
54 changes: 23 additions & 31 deletions lib/rubygems/core_ext/kernel_require.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,48 +39,40 @@ def require(path)

path = path.to_path if path.respond_to? :to_path

# Ensure -I beats a default gem
# https://github.com/rubygems/rubygems/pull/1868
resolved_path = begin
rp = nil
Gem.suffixes.each do |s|
load_path_insert_index = Gem.load_path_insert_index
break unless load_path_insert_index

$LOAD_PATH[0...load_path_insert_index].each do |lp|
safe_lp = lp.dup.tap(&Gem::UNTAINT)
begin
if File.symlink? safe_lp # for backward compatibility
next
if spec = Gem.find_unresolved_default_spec(path)
# Ensure -I beats a default gem
resolved_path = begin
rp = nil
load_path_check_index = Gem.load_path_insert_index - Gem.activated_gem_paths
Gem.suffixes.each do |s|
$LOAD_PATH[0...load_path_check_index].each do |lp|
safe_lp = lp.dup.tap(&Gem::UNTAINT)
begin
if File.symlink? safe_lp # for backward compatibility
next
end
rescue SecurityError
RUBYGEMS_ACTIVATION_MONITOR.exit
raise
end
rescue SecurityError
RUBYGEMS_ACTIVATION_MONITOR.exit
raise
end

full_path = File.expand_path(File.join(safe_lp, "#{path}#{s}"))
if File.file?(full_path)
rp = full_path
break
full_path = File.expand_path(File.join(safe_lp, "#{path}#{s}"))
if File.file?(full_path)
rp = full_path
break
end
end
break if rp
end
break if rp
rp
end
rp
end

if resolved_path
RUBYGEMS_ACTIVATION_MONITOR.exit
return gem_original_require(resolved_path)
end

if spec = Gem.find_unresolved_default_spec(path)
begin
Kernel.send(:gem, spec.name, Gem::Requirement.default_prerelease)
rescue Exception
RUBYGEMS_ACTIVATION_MONITOR.exit
raise
end
end unless resolved_path
end

# If there are no unresolved deps, then we can use just try
Expand Down
1 change: 1 addition & 0 deletions lib/rubygems/test_case.rb
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ def setup
Gem::Security.reset

Gem.loaded_specs.clear
Gem.instance_variable_set(:@activated_gem_paths, 0)
Gem.clear_default_specs
Bundler.reset!

Expand Down
40 changes: 40 additions & 0 deletions test/rubygems/test_require.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,35 @@ def refute_require(path)
refute require(path), "'#{path}' was not yet required"
end

def test_respect_loaded_features_caching_like_standard_require
dir = Dir.mktmpdir("test_require", @tempdir)

lp1 = File.join dir, 'foo1'
foo1 = File.join lp1, 'foo.rb'

FileUtils.mkdir_p lp1
File.open(foo1, 'w') { |f| f.write "class Object; HELLO = 'foo1' end" }

lp = $LOAD_PATH.dup

$LOAD_PATH.unshift lp1
assert_require 'foo'
assert_equal "foo1", ::Object::HELLO

lp2 = File.join dir, 'foo2'
foo2 = File.join lp2, 'foo.rb'

FileUtils.mkdir_p lp2
File.open(foo2, 'w') { |f| f.write "class Object; HELLO = 'foo2' end" }

$LOAD_PATH.unshift lp2
refute_require 'foo'
assert_equal "foo1", ::Object::HELLO
ensure
$LOAD_PATH.replace lp
Object.send :remove_const, :HELLO if Object.const_defined? :HELLO
end

# Providing -I on the commandline should always beat gems
def test_dash_i_beats_gems
a1 = util_spec "a", "1", {"b" => "= 1"}, "lib/test_gem_require_a.rb"
Expand Down Expand Up @@ -420,6 +449,17 @@ def test_default_gem_require_activates_just_once
assert_equal 0, times_called
end

def test_second_gem_require_does_not_resolve_path_manually_before_going_through_standard_require
a1 = util_spec "a", "1", nil, "lib/test_gem_require_a.rb"
install_gem a1

assert_require "test_gem_require_a"

stub(:gem_original_require, ->(path) { assert_equal "test_gem_require_a", path }) do
require "test_gem_require_a"
end
end

def test_realworld_default_gem
testing_ruby_repo = !ENV["GEM_COMMAND"].nil?
skip "this test can't work under ruby-core setup" if testing_ruby_repo || java_platform?
Expand Down

0 comments on commit 54a4cdb

Please sign in to comment.