Skip to content

Commit 41f9a63

Browse files
committed
[test] jruby call-info specific tests
1 parent 3babf1a commit 41f9a63

2 files changed

Lines changed: 234 additions & 0 deletions

File tree

test/jruby.index

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ jruby/test_array
88
jruby/test_autoload
99
jruby/test_backquote
1010
jruby/test_backtraces
11+
jruby/test_call_info
1112
jruby/test_base64
1213
jruby/test_binding_eval_yield
1314
jruby/test_block

test/jruby/test_call_info.rb

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
require 'test/unit'
2+
require 'tempfile'
3+
require 'stringio'
4+
5+
# Tests for ThreadContext#callInfo not leaking between method calls.
6+
#
7+
# When a method is invoked with keyword arguments, the IR sets a flag
8+
# (CALL_KEYWORD) in ThreadContext#callInfo. If the receiving method does
9+
# not properly reset callInfo, that flag "leaks" to the next call and
10+
# causes incorrect keyword handling (ArgumentError, wrong defaults, etc.).
11+
class TestCallInfo < Test::Unit::TestCase
12+
13+
class KwargsReceiver
14+
attr_reader :a, :k
15+
def initialize(a, k:)
16+
@a = a
17+
@k = k
18+
end
19+
end
20+
21+
# ── Hash#initialize ────────────────────────────────────────────────
22+
23+
def test_hash_new_capacity_keyword_recognized
24+
# Hash.new(capacity: N) should treat capacity: as a sizing hint, not as the default value.
25+
h = Hash.new(capacity: 10)
26+
assert_nil h.default, "capacity: should be recognized as keyword, not default value"
27+
28+
# If callInfo leaked, this would raise ArgumentError
29+
v = KwargsReceiver.new(1, k: 2)
30+
assert_equal [1, 2], [v.a, v.k]
31+
32+
h = Hash.new(42, capacity: 10)
33+
assert_equal 42, h.default
34+
35+
# If callInfo leaked, this would raise ArgumentError
36+
v = KwargsReceiver.new(1, k: 2)
37+
assert_equal [1, 2], [v.a, v.k]
38+
end
39+
40+
# ── Struct.new callInfo leak ───────────────────────────────────────
41+
42+
def test_struct_new_with_keyword_init_does_not_leak
43+
Struct.new(:x, keyword_init: true)
44+
# If callInfo leaked, this would raise ArgumentError
45+
v = KwargsReceiver.new(1, k: 2)
46+
assert_equal 1, v.a
47+
assert_equal 2, v.k
48+
end
49+
50+
def test_struct_new_with_empty_splat_does_not_leak
51+
opts = {}
52+
Struct.new(:y, **opts)
53+
v = KwargsReceiver.new(1, k: 2)
54+
assert_equal [1, 2], [v.a, v.k]
55+
end
56+
57+
# ── Struct#initialize callInfo leak ────────────────────────────────
58+
59+
def test_struct_initialize_1arg_empty_splat_does_not_leak
60+
s = Struct.new(:x)
61+
s.new(1, **{})
62+
v = KwargsReceiver.new(1, k: 2)
63+
assert_equal [1, 2], [v.a, v.k]
64+
end
65+
66+
def test_struct_initialize_2arg_empty_splat_does_not_leak
67+
s = Struct.new(:x, :y)
68+
s.new(1, 2, **{})
69+
v = KwargsReceiver.new(1, k: 2)
70+
assert_equal [1, 2], [v.a, v.k]
71+
end
72+
73+
def test_struct_initialize_3arg_empty_splat_does_not_leak
74+
s = Struct.new(:x, :y, :z)
75+
s.new(1, 2, 3, **{})
76+
v = KwargsReceiver.new(1, k: 2)
77+
assert_equal [1, 2], [v.a, v.k]
78+
end
79+
80+
def test_struct_initialize_keyword_init_works
81+
s = Struct.new(:a, :b, keyword_init: true)
82+
instance = s.new(a: 10, b: 20)
83+
assert_equal 10, instance.a
84+
assert_equal 20, instance.b
85+
86+
# Verify no leak after keyword_init construction
87+
v = KwargsReceiver.new(1, k: 2)
88+
assert_equal [1, 2], [v.a, v.k]
89+
end
90+
91+
# ── Regexp.linear_time? keyword handling ───────────────────────────
92+
93+
def test_regexp_linear_time_with_timeout_keyword
94+
# timeout: must be recognized as keyword, not raise ArgumentError
95+
result = Regexp.linear_time?("abc", timeout: 1.0)
96+
assert_include [true, false], result
97+
98+
# Verify no callInfo leak after keyword call
99+
v = KwargsReceiver.new(1, k: 2)
100+
assert_equal [1, 2], [v.a, v.k]
101+
end
102+
103+
def test_regexp_linear_time_with_regexp_and_timeout_keyword
104+
result = Regexp.linear_time?(/abc/, timeout: 1.0)
105+
assert_include [true, false], result
106+
107+
v = KwargsReceiver.new(1, k: 2)
108+
assert_equal [1, 2], [v.a, v.k]
109+
end
110+
111+
def test_regexp_linear_time_no_leak_without_keyword
112+
Regexp.linear_time?("abc")
113+
# Non-keyword call should not affect subsequent keyword call
114+
v = KwargsReceiver.new(1, k: 2)
115+
assert_equal [1, 2], [v.a, v.k]
116+
end
117+
118+
# ── ARGF keyword handling ──────────────────────────────────────────
119+
120+
def test_argf_readlines_chomp_keyword
121+
tmpfile = Tempfile.new('test_call_info_readlines')
122+
tmpfile.write("hello\nworld\n")
123+
tmpfile.close
124+
125+
lines = File.open(tmpfile.path) { |f| f.readlines(chomp: true) }
126+
assert_equal ["hello", "world"], lines
127+
128+
# Verify no callInfo leak
129+
v = KwargsReceiver.new(1, k: 2)
130+
assert_equal [1, 2], [v.a, v.k]
131+
ensure
132+
tmpfile.unlink if tmpfile
133+
end
134+
135+
def test_argf_gets_chomp_keyword
136+
tmpfile = Tempfile.new('test_call_info_gets')
137+
tmpfile.write("hello\nworld\n")
138+
tmpfile.close
139+
140+
line = File.open(tmpfile.path) { |f| f.gets(chomp: true) }
141+
assert_equal "hello", line
142+
143+
v = KwargsReceiver.new(1, k: 2)
144+
assert_equal [1, 2], [v.a, v.k]
145+
ensure
146+
tmpfile.unlink if tmpfile
147+
end
148+
149+
def test_argf_each_line_chomp_keyword
150+
tmpfile = Tempfile.new('test_call_info_each_line')
151+
tmpfile.write("hello\nworld\n")
152+
tmpfile.close
153+
154+
lines = []
155+
File.open(tmpfile.path) { |f| f.each_line(chomp: true) { |l| lines << l } }
156+
assert_equal ["hello", "world"], lines
157+
158+
v = KwargsReceiver.new(1, k: 2)
159+
assert_equal [1, 2], [v.a, v.k]
160+
ensure
161+
tmpfile.unlink if tmpfile
162+
end
163+
164+
# ── Kernel#open with to_open redirect ──────────────────────────────
165+
166+
class ToOpenTarget
167+
def to_open(*args)
168+
StringIO.new("hello from to_open")
169+
end
170+
end
171+
172+
def test_kernel_open_to_open_redirect_with_keywords
173+
io = open(ToOpenTarget.new, mode: "r")
174+
assert_equal "hello from to_open", io.read
175+
io.close
176+
177+
# Verify no callInfo leak after to_open redirect
178+
v = KwargsReceiver.new(1, k: 2)
179+
assert_equal [1, 2], [v.a, v.k]
180+
end
181+
182+
def test_kernel_open_to_open_redirect_without_keywords
183+
io = open(ToOpenTarget.new)
184+
assert_equal "hello from to_open", io.read
185+
io.close
186+
187+
v = KwargsReceiver.new(1, k: 2)
188+
assert_equal [1, 2], [v.a, v.k]
189+
end
190+
191+
# ── to_enum keyword propagation ────────────────────────────────────
192+
193+
class EachWithKeyword
194+
def each(k:)
195+
yield k
196+
end
197+
end
198+
199+
def test_to_enum_propagates_keywords
200+
enum = EachWithKeyword.new.to_enum(:each, k: 42)
201+
assert_equal [42], enum.to_a
202+
203+
# Verify no callInfo leak
204+
v = KwargsReceiver.new(1, k: 2)
205+
assert_equal [1, 2], [v.a, v.k]
206+
end
207+
208+
def test_to_enum_without_keywords_does_not_leak
209+
enum = [1, 2, 3].to_enum(:each)
210+
enum.to_a
211+
212+
v = KwargsReceiver.new(1, k: 2)
213+
assert_equal [1, 2], [v.a, v.k]
214+
end
215+
216+
# ── General: keywords after non-keyword calls ──────────────────────
217+
218+
def test_keywords_work_after_various_calls
219+
# Exercise a sequence of calls mixing keyword and non-keyword usage
220+
# to ensure callInfo is properly reset between calls.
221+
Hash.new(capacity: 10)
222+
v1 = KwargsReceiver.new(1, k: :a)
223+
assert_equal :a, v1.k
224+
225+
[1, 2, 3].size # non-keyword call
226+
v2 = KwargsReceiver.new(2, k: :b)
227+
assert_equal :b, v2.k
228+
229+
Struct.new(:tmp, keyword_init: true)
230+
v3 = KwargsReceiver.new(3, k: :c)
231+
assert_equal :c, v3.k
232+
end
233+
end

0 commit comments

Comments
 (0)