Skip to content

Commit e7feb07

Browse files
committed
[GR-38307] Fix loading issue when multiple feature paths overlap
PullRequest: truffleruby/3313
2 parents a950317 + 3d8dd52 commit e7feb07

File tree

5 files changed

+99
-80
lines changed

5 files changed

+99
-80
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Bug fixes:
3636
* Fix `Float#round` to avoid losing precision during the rounding process (@aardvark179).
3737
* Fix `String#insert` to not call a subclassed string method (@bjfish).
3838
* Fix `rb_obj_call_init` to pass any block argument to the `initialize` method (#2675, @aardvark179).
39+
* Fix issue with feature loading not detecting a previously loaded feature (#2677, @bjfish).
3940

4041
Compatibility:
4142

spec/ruby/core/kernel/shared/require.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,25 @@
568568

569569
-> { @object.require("unicode_normalize") }.should raise_error(LoadError)
570570
end
571+
572+
ruby_version_is "3.0" do
573+
it "does not load a file earlier on the $LOAD_PATH when other similar features were already loaded" do
574+
Dir.chdir CODE_LOADING_DIR do
575+
@object.send(@method, "../code/load_fixture").should be_true
576+
end
577+
ScratchPad.recorded.should == [:loaded]
578+
579+
$LOAD_PATH.unshift "#{CODE_LOADING_DIR}/b"
580+
# This loads because the above load was not on the $LOAD_PATH
581+
@object.send(@method, "load_fixture").should be_true
582+
ScratchPad.recorded.should == [:loaded, :loaded]
583+
584+
$LOAD_PATH.unshift "#{CODE_LOADING_DIR}/c"
585+
# This does not load because the above load was on the $LOAD_PATH
586+
@object.send(@method, "load_fixture").should be_false
587+
ScratchPad.recorded.should == [:loaded, :loaded]
588+
end
589+
end
571590
end
572591

573592
describe "(shell expansion)" do
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ScratchPad << :loaded

spec/truffle/kernel/feature_loader_spec.rb

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,37 @@
1515
current_index.clear
1616

1717
Truffle::FeatureLoader.features_index_add('foo', 0)
18-
current_index[Truffle::FeatureLoader::FeatureEntry.new('foo')].should == [0]
18+
current_index['foo'][0].index.should == 0
1919

2020
Truffle::FeatureLoader.features_index_add('foo', 1)
21-
current_index[Truffle::FeatureLoader::FeatureEntry.new('foo')].should == [0, 1]
21+
current_index['foo'][1].index.should == 1
2222
current_index.clear
2323

2424
Truffle::FeatureLoader.features_index_add('foo.rb', 0)
2525
Truffle::FeatureLoader.features_index_add('foo.rb', 1)
26-
current_index[Truffle::FeatureLoader::FeatureEntry.new('foo')].should == [0, 1]
27-
current_index[Truffle::FeatureLoader::FeatureEntry.new('foo.rb')].should == [0, 1]
26+
current_index['foo'][0].index.should == 0
27+
current_index['foo'][1].index.should == 1
2828
current_index.clear
2929

3030
Truffle::FeatureLoader.features_index_add('one/two/foo.rb', 0)
3131
['foo', 'foo.rb', 'two/foo', 'two/foo.rb', 'one/two/foo.rb', 'one/two/foo'].each do |feature|
32-
current_index[Truffle::FeatureLoader::FeatureEntry.new(feature)].should == [0]
32+
current_index['foo'].any? do |fe|
33+
if fe.include?(Truffle::FeatureLoader::FeatureEntry.new(feature))
34+
fe.index.should == 0
35+
true
36+
end
37+
end.should == true
3338
end
3439
current_index.clear
3540

3641
Truffle::FeatureLoader.features_index_add('/two/foo.rb', 0)
3742
['foo', 'foo.rb', 'two/foo', 'two/foo.rb', '/two/foo.rb', '/two/foo'].each do |feature|
38-
current_index[Truffle::FeatureLoader::FeatureEntry.new(feature)].should == [0]
43+
current_index['foo'].any? do |fe|
44+
if fe.include?(Truffle::FeatureLoader::FeatureEntry.new(feature))
45+
fe.index.should == 0
46+
true
47+
end
48+
end.should == true
3949
end
4050
current_index.clear
4151
ensure
@@ -89,48 +99,35 @@
8999
it "only a nested path at the end should match" do
90100
hash = {}
91101
stored_entry = Truffle::FeatureLoader::FeatureEntry.new("path/to/feature")
92-
hash[stored_entry] = true
93-
stored_entry.part_of_index = true
102+
hash[stored_entry.base] = [stored_entry]
94103

95104
short_lookup_entry = Truffle::FeatureLoader::FeatureEntry.new("to/feature")
96-
hash[short_lookup_entry].should be_true
105+
hash['feature'].any? { |fe| fe.include?(short_lookup_entry) }.should be_true
97106

98107
exact_lookup_entry = Truffle::FeatureLoader::FeatureEntry.new("path/to/feature")
99-
hash[exact_lookup_entry].should be_true
100-
108+
hash['feature'].any? { |fe| fe.include?(exact_lookup_entry) }.should be_true
101109

102110
longer_lookup_entry = Truffle::FeatureLoader::FeatureEntry.new("long/path/to/feature")
103-
hash[longer_lookup_entry].should be_nil
111+
hash['feature'].any? { |fe| fe.include?(longer_lookup_entry) }.should be_false
104112

105113
prefix_lookup_entry = Truffle::FeatureLoader::FeatureEntry.new("path/to")
106-
hash[prefix_lookup_entry].should be_nil
114+
hash['feature'].any? { |fe| fe.include?(prefix_lookup_entry) }.should be_false
107115
end
108116

109117
describe "when stored has an extension" do
110118
it "matches a lookup with or without extension" do
111119
hash = {}
112120
stored_entry = Truffle::FeatureLoader::FeatureEntry.new("path/to/feature.so")
113-
hash[stored_entry] = true
114-
stored_entry.part_of_index = true
115-
121+
hash['feature'] = [stored_entry]
116122

117123
lookup_entry_no_ext = Truffle::FeatureLoader::FeatureEntry.new("to/feature")
118-
hash[lookup_entry_no_ext].should be_true
124+
hash['feature'].any? { |fe| fe.include?(lookup_entry_no_ext) }.should be_true
119125

120126
lookup_entry_ext = Truffle::FeatureLoader::FeatureEntry.new("to/feature.so")
121-
hash[lookup_entry_ext].should be_true
127+
hash['feature'].any? { |fe| fe.include?(lookup_entry_ext) }.should be_true
122128

123129
lookup_entry_wrong_ext = Truffle::FeatureLoader::FeatureEntry.new("to/feature.rb")
124-
hash[lookup_entry_wrong_ext].should be_nil
130+
hash['feature'].any? { |fe| fe.include?(lookup_entry_wrong_ext) }.should be_false
125131
end
126132
end
127-
128-
it "raises an error when both keys are lookup" do
129-
hash = {}
130-
stored_entry = Truffle::FeatureLoader::FeatureEntry.new("path/to/feature")
131-
hash[stored_entry] = true
132-
133-
short_lookup_entry = Truffle::FeatureLoader::FeatureEntry.new("to/feature")
134-
-> { hash[short_lookup_entry] }.should raise_error
135-
end
136133
end

src/main/ruby/truffleruby/core/truffle/feature_loader.rb

Lines changed: 53 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,24 @@ module FeatureLoader
1313

1414
DOT_DLEXT = ".#{Truffle::Platform::DLEXT}"
1515

16-
# FeatureEntry => [*index_inside_$LOADED_FEATURES]
16+
#
17+
# The following $LOADED_FEATURES:
18+
# ["/one/path.rb", "/two/path.so", "three/path" ]
19+
#
20+
# would create the @loaded_features_index:
21+
#
22+
# {
23+
# "path" => [
24+
# #<FeatureEntry @feature="/one/path.rb" @index=0>,
25+
# #<FeatureEntry @feature="/two/path.so" @index=1>,
26+
# #<FeatureEntry @feature="/three/path" @index=2>,
27+
# ]
28+
# }
29+
#
30+
# For the feature "path" or "nested/path", all three feature entries are in the same hash key and `include?`
31+
# the feature. However, for feature "path.rb", only the first entry with the same extension would be at the same
32+
# hash key and not `include?` the feature.
33+
#
1734
@loaded_features_index = {}
1835
# A snapshot of $LOADED_FEATURES, to check if the @loaded_features_index cache is up to date.
1936
@loaded_features_version = -1
@@ -33,43 +50,24 @@ def self.clear_cache
3350
end
3451

3552
class FeatureEntry
36-
attr_reader :feature, :ext, :feature_no_ext
37-
attr_accessor :part_of_index
53+
attr_accessor :index
54+
attr_reader :feature, :ext, :feature_no_ext, :base
3855

3956
def initialize(feature)
40-
@part_of_index = false
4157
@ext = Truffle::FeatureLoader.extension(feature)
4258
@feature = feature
4359
@feature_no_ext = @ext ? feature[0...(-@ext.size)] : feature
4460
@base = File.basename(@feature_no_ext)
61+
@index = nil
4562
end
4663

47-
def ==(other)
48-
# The looked up feature has to be the trailing part of an already-part_of_index entry.
49-
# We always want to check part_of_index_feature.end_with?(lookup_feature).
50-
# We compare extensions only if the lookup_feature has an extension.
51-
52-
if @part_of_index
53-
stored = self
54-
lookup = other
55-
elsif other.part_of_index
56-
stored = other
57-
lookup = self
58-
else
59-
raise 'Expected that at least one of the FeatureEntry instances is part of the index'
60-
end
61-
64+
def include?(lookup)
6265
if lookup.ext
63-
stored.feature.end_with?(lookup.feature)
66+
feature.end_with?(lookup.feature)
6467
else
65-
stored.feature_no_ext.end_with?(lookup.feature_no_ext)
68+
feature_no_ext.end_with?(lookup.feature_no_ext)
6669
end
6770
end
68-
alias_method :eql?, :==
69-
70-
def hash
71-
@base.hash
72-
end
7371
end
7472

7573
def self.find_file(feature)
@@ -161,30 +159,32 @@ def self.feature_provided?(feature, expanded)
161159
with_synchronized_features do
162160
get_loaded_features_index
163161
feature_entry = FeatureEntry.new(feature)
164-
if @loaded_features_index.key?(feature_entry)
165-
@loaded_features_index[feature_entry].each do |i|
166-
loaded_feature = $LOADED_FEATURES[i]
162+
if @loaded_features_index.key?(feature_entry.base)
163+
@loaded_features_index[feature_entry.base].each do |fe|
164+
if fe.include?(feature_entry)
165+
loaded_feature = $LOADED_FEATURES[fe.index]
167166

168-
next if loaded_feature.size < feature.size
169-
feature_path = if loaded_feature.start_with?(feature)
170-
feature
171-
else
172-
if expanded
173-
nil
167+
next if loaded_feature.size < feature.size
168+
feature_path = if loaded_feature.start_with?(feature)
169+
feature
174170
else
175-
loaded_feature_path(loaded_feature, feature, get_expanded_load_path)
171+
if expanded
172+
nil
173+
else
174+
loaded_feature_path(loaded_feature, feature, get_expanded_load_path)
175+
end
176176
end
177-
end
178-
if feature_path
179-
loaded_feature_ext = extension_symbol(loaded_feature)
180-
if !loaded_feature_ext
181-
return :unknown unless feature_ext
182-
else
183-
if (!feature_has_rb_ext || !feature_ext) && binary_ext?(loaded_feature_ext)
184-
return :so
185-
end
186-
if (feature_has_rb_ext || !feature_ext) && loaded_feature_ext == :rb
187-
return :rb
177+
if feature_path
178+
loaded_feature_ext = extension_symbol(loaded_feature)
179+
if !loaded_feature_ext
180+
return :unknown unless feature_ext
181+
else
182+
if (!feature_has_rb_ext || !feature_ext) && binary_ext?(loaded_feature_ext)
183+
return :so
184+
end
185+
if (feature_has_rb_ext || !feature_ext) && loaded_feature_ext == :rb
186+
return :rb
187+
end
188188
end
189189
end
190190
end
@@ -225,7 +225,7 @@ def self.relative_feature(expanded_path)
225225
if !load_path_entries.empty?
226226
load_path_entry = load_path_entries.max_by(&:length)
227227
before_dot_rb = expanded_path.end_with?('.rb') ? -4 : -1
228-
expanded_path[load_path_entry.size+1..before_dot_rb]
228+
expanded_path[load_path_entry.size + 1..before_dot_rb]
229229
else
230230
nil
231231
end
@@ -297,13 +297,14 @@ def self.get_loaded_features_index
297297

298298
# MRI: features_index_add
299299
# always called inside #with_synchronized_features
300+
#
300301
def self.features_index_add(feature, offset)
301302
feature_entry = FeatureEntry.new(feature)
302-
if @loaded_features_index.key?(feature_entry)
303-
@loaded_features_index[feature_entry] << offset
303+
feature_entry.index = offset
304+
if @loaded_features_index.key?(feature_entry.base)
305+
@loaded_features_index[feature_entry.base] << feature_entry
304306
else
305-
@loaded_features_index[feature_entry] = [offset]
306-
feature_entry.part_of_index = true
307+
@loaded_features_index[feature_entry.base] = [feature_entry]
307308
end
308309
end
309310

0 commit comments

Comments
 (0)