Skip to content

Commit 0e8c588

Browse files
committed
fix infinite loop from inifinite self-returning as_json objects
1 parent 7e7f7e6 commit 0e8c588

File tree

3 files changed

+312
-2
lines changed

3 files changed

+312
-2
lines changed

lib/async/container/supervisor/message_wrapper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def normalize(obj)
4747
when Array
4848
obj.map{|v| normalize(v)}
4949
else
50-
if obj.respond_to?(:as_json)
50+
if obj.respond_to?(:as_json) && (as_json = obj.as_json) && as_json != obj
5151
normalize(obj.as_json)
5252
else
5353
obj

test/async/container/connection.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
require "async/container/supervisor/connection"
77
require "sus/fixtures/async/scheduler_context"
88
require "stringio"
9-
require "msgpack"
109

1110
class TestTarget
1211
def initialize(&block)
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
require "async/container/supervisor/connection"
7+
require "sus/fixtures/async/scheduler_context"
8+
require "stringio"
9+
require "msgpack"
10+
11+
class TrueObject
12+
def as_json
13+
true
14+
end
15+
end
16+
17+
describe Async::Container::Supervisor::MessageWrapper do
18+
let(:stream) {StringIO.new}
19+
let(:message_wrapper) {Async::Container::Supervisor::MessageWrapper.new(stream)}
20+
21+
def write_message(message)
22+
message_wrapper.write(message)
23+
stream.rewind
24+
end
25+
26+
def read_message
27+
message_wrapper.read
28+
end
29+
30+
with "write and read" do
31+
it "normalizes without infinite loop" do
32+
Integer.define_method(:as_json) do
33+
self
34+
end
35+
36+
write_message({id: 1, do: :test})
37+
38+
message = read_message
39+
expect(message[:id]).to be == 1
40+
ensure
41+
Integer.send(:remove_method, :as_json)
42+
end
43+
44+
it "handles simple strings" do
45+
write_message({message: "hello world"})
46+
47+
result = read_message
48+
expect(result[:message]).to be == "hello world"
49+
end
50+
51+
it "handles integers" do
52+
write_message({count: 42, negative: -10})
53+
54+
result = read_message
55+
expect(result[:count]).to be == 42
56+
expect(result[:negative]).to be == -10
57+
end
58+
59+
it "handles floats" do
60+
write_message({pi: 3.14159, negative: -2.5})
61+
62+
result = read_message
63+
expect(result[:pi]).to be == 3.14159
64+
expect(result[:negative]).to be == -2.5
65+
end
66+
67+
it "handles boolean values" do
68+
write_message({success: true, failed: false})
69+
70+
result = read_message
71+
expect(result[:success]).to be == true
72+
expect(result[:failed]).to be == false
73+
end
74+
75+
it "handles nil values" do
76+
write_message({empty: nil})
77+
78+
result = read_message
79+
expect(result[:empty]).to be_nil
80+
end
81+
82+
it "handles arrays" do
83+
write_message({items: [1, 2, 3, "four", true]})
84+
85+
result = read_message
86+
expect(result[:items]).to be == [1, 2, 3, "four", true]
87+
end
88+
89+
it "handles nested hashes" do
90+
write_message({
91+
user: {
92+
name: "Alice",
93+
details: {
94+
age: 30,
95+
active: true
96+
}
97+
}
98+
})
99+
100+
result = read_message
101+
expect(result[:user][:name]).to be == "Alice"
102+
expect(result[:user][:details][:age]).to be == 30
103+
expect(result[:user][:details][:active]).to be == true
104+
end
105+
106+
it "handles nested arrays" do
107+
write_message({matrix: [[1, 2], [3, 4], [5, 6]]})
108+
109+
result = read_message
110+
expect(result[:matrix]).to be == [[1, 2], [3, 4], [5, 6]]
111+
end
112+
113+
it "handles symbols" do
114+
write_message({action: :start, status: :success})
115+
116+
result = read_message
117+
expect(result[:action]).to be == :start
118+
expect(result[:status]).to be == :success
119+
end
120+
121+
it "handles empty hash" do
122+
write_message({})
123+
124+
result = read_message
125+
expect(result).to be == {}
126+
end
127+
128+
it "handles empty array" do
129+
write_message({items: []})
130+
131+
result = read_message
132+
expect(result[:items]).to be == []
133+
end
134+
end
135+
136+
with "Time handling" do
137+
it "serializes and deserializes Time objects" do
138+
time = Time.now
139+
write_message({timestamp: time})
140+
141+
result = read_message
142+
expect(result[:timestamp]).to be_within(0.001).of(time)
143+
end
144+
145+
it "handles Time in nested structures" do
146+
time = Time.now
147+
write_message({
148+
event: {
149+
name: "test",
150+
occurred_at: time
151+
}
152+
})
153+
154+
result = read_message
155+
expect(result[:event][:occurred_at]).to be_within(0.001).of(time)
156+
end
157+
end
158+
159+
with "Class handling" do
160+
it "serializes class names" do
161+
write_message({type: String})
162+
163+
result = read_message
164+
expect(result[:type]).to be == "String"
165+
end
166+
167+
it "handles multiple classes" do
168+
write_message({types: [String, Integer, Array]})
169+
170+
result = read_message
171+
expect(result[:types]).to be == ["String", "Integer", "Array"]
172+
end
173+
end
174+
175+
with "Exception handling" do
176+
it "serializes and deserializes RuntimeError" do
177+
error = RuntimeError.new("Something went wrong")
178+
error.set_backtrace(["line1", "line2"])
179+
180+
write_message({error: error})
181+
182+
result = read_message
183+
expect(result[:error]).to be_a(RuntimeError)
184+
expect(result[:error].message).to be == "Something went wrong"
185+
expect(result[:error].backtrace).to be == ["line1", "line2"]
186+
end
187+
188+
it "handles StandardError" do
189+
error = StandardError.new("Standard error")
190+
write_message({error: error})
191+
192+
result = read_message
193+
expect(result[:error]).to be_a(StandardError)
194+
expect(result[:error].message).to be == "Standard error"
195+
end
196+
197+
it "handles ArgumentError" do
198+
error = ArgumentError.new("Invalid argument")
199+
write_message({error: error})
200+
201+
result = read_message
202+
expect(result[:error]).to be_a(ArgumentError)
203+
expect(result[:error].message).to be == "Invalid argument"
204+
end
205+
end
206+
207+
with "normalize method" do
208+
it "normalizes objects with as_json method" do
209+
obj = TrueObject.new
210+
write_message({custom: obj})
211+
212+
result = read_message
213+
expect(result[:custom]).to be == true
214+
end
215+
216+
it "normalizes arrays of objects with as_json" do
217+
write_message({items: [TrueObject.new, TrueObject.new]})
218+
219+
result = read_message
220+
expect(result[:items]).to be == [true, true]
221+
end
222+
223+
it "normalizes nested objects with as_json" do
224+
write_message({
225+
data: {
226+
flag: TrueObject.new,
227+
nested: {
228+
another: TrueObject.new
229+
}
230+
}
231+
})
232+
233+
result = read_message
234+
expect(result[:data][:flag]).to be == true
235+
expect(result[:data][:nested][:another]).to be == true
236+
end
237+
end
238+
239+
with "complex messages" do
240+
it "handles complex nested structures" do
241+
write_message({
242+
id: 123,
243+
action: :process,
244+
data: {
245+
items: [1, 2, 3],
246+
metadata: {
247+
timestamp: Time.now,
248+
type: String
249+
}
250+
},
251+
flags: {
252+
active: true,
253+
debug: false
254+
}
255+
})
256+
257+
result = read_message
258+
expect(result[:id]).to be == 123
259+
expect(result[:action]).to be == :process
260+
expect(result[:data][:items]).to be == [1, 2, 3]
261+
expect(result[:flags][:active]).to be == true
262+
end
263+
264+
it "handles large arrays" do
265+
large_array = (1..1000).to_a
266+
write_message({data: large_array})
267+
268+
result = read_message
269+
expect(result[:data]).to be == large_array
270+
end
271+
272+
it "handles deeply nested structures" do
273+
deep = {level1: {level2: {level3: {level4: {level5: "deep"}}}}}
274+
write_message(deep)
275+
276+
result = read_message
277+
expect(result[:level1][:level2][:level3][:level4][:level5]).to be == "deep"
278+
end
279+
end
280+
281+
with "edge cases" do
282+
it "handles empty string" do
283+
write_message({text: ""})
284+
285+
result = read_message
286+
expect(result[:text]).to be == ""
287+
end
288+
289+
it "handles zero values" do
290+
write_message({count: 0, amount: 0.0})
291+
292+
result = read_message
293+
expect(result[:count]).to be == 0
294+
expect(result[:amount]).to be == 0.0
295+
end
296+
297+
it "handles unicode strings" do
298+
write_message({text: "Hello 世界 🌍"})
299+
300+
result = read_message
301+
expect(result[:text]).to be == "Hello 世界 🌍"
302+
end
303+
304+
it "handles special characters" do
305+
write_message({text: "Line1\nLine2\tTabbed"})
306+
307+
result = read_message
308+
expect(result[:text]).to be == "Line1\nLine2\tTabbed"
309+
end
310+
end
311+
end

0 commit comments

Comments
 (0)