Skip to content

Commit 6581047

Browse files
Improved test coverage.
1 parent be9f242 commit 6581047

File tree

5 files changed

+228
-27
lines changed

5 files changed

+228
-27
lines changed

fixtures/async/container/supervisor/a_server.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ def remove(connection)
3737
let(:ipc_path) {File.join(@root, "supervisor.ipc")}
3838
let(:endpoint) {Async::Container::Supervisor.endpoint(ipc_path)}
3939

40-
def around(&block)
40+
def around
4141
Dir.mktmpdir do |directory|
4242
@root = directory
43-
super(&block)
43+
super
4444
end
4545
end
4646

gems.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
gem "rubocop-socketry"
3131

3232
gem "sus-fixtures-async"
33+
gem "sus-fixtures-console"
3334

3435
gem "bake-test"
3536
gem "bake-test-external"

lib/async/container/supervisor/server.rb

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,9 @@ def do_register(call)
2727
call.connection.state.merge!(call.message[:state])
2828

2929
@monitors.each do |monitor|
30-
begin
31-
monitor.register(call.connection)
32-
rescue => error
33-
Console.error(self, "Error while registering process!", monitor: monitor, exception: error)
34-
end
30+
monitor.register(call.connection)
31+
rescue => error
32+
Console.error(self, "Error while registering process!", monitor: monitor, exception: error)
3533
end
3634
ensure
3735
call.finish
@@ -59,22 +57,18 @@ def do_status(call)
5957

6058
def remove(connection)
6159
@monitors.each do |monitor|
62-
begin
63-
monitor.remove(connection)
64-
rescue => error
65-
Console.error(self, "Error while removing process!", monitor: monitor, exception: error)
66-
end
60+
monitor.remove(connection)
61+
rescue => error
62+
Console.error(self, "Error while removing process!", monitor: monitor, exception: error)
6763
end
6864
end
6965

7066
def run(parent: Task.current)
7167
parent.async do |task|
7268
@monitors.each do |monitor|
73-
begin
74-
monitor.run
75-
rescue => error
76-
Console.error(self, "Error while starting monitor!", monitor: monitor, exception: error)
77-
end
69+
monitor.run
70+
rescue => error
71+
Console.error(self, "Error while starting monitor!", monitor: monitor, exception: error)
7872
end
7973

8074
@endpoint.accept do |peer|

test/async/container/connection.rb

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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 "stringio"
8+
9+
describe Async::Container::Supervisor::Connection do
10+
let(:stream) {StringIO.new}
11+
let(:connection) {Async::Container::Supervisor::Connection.new(stream)}
12+
13+
with subject::Call do
14+
let(:test_call) {Async::Container::Supervisor::Connection::Call.new(connection, 1, {do: :test, data: "value"})}
15+
16+
it "can serialize call to JSON" do
17+
json = test_call.to_json
18+
parsed = JSON.parse(json)
19+
20+
expect(parsed).to have_keys(
21+
"do" => be == "test",
22+
"data" => be == "value"
23+
)
24+
end
25+
26+
it "can get call message via as_json" do
27+
expect(test_call.as_json).to have_keys(
28+
do: be == :test,
29+
data: be == "value"
30+
)
31+
end
32+
33+
it "can iterate over call responses with each" do
34+
# Push some responses
35+
test_call.push(status: "working")
36+
test_call.push(status: "done")
37+
test_call.close
38+
39+
responses = []
40+
test_call.each do |response|
41+
responses << response
42+
end
43+
44+
expect(responses.size).to be == 2
45+
expect(responses[0]).to have_keys(status: be == "working")
46+
expect(responses[1]).to have_keys(status: be == "done")
47+
end
48+
49+
it "can fail a call" do
50+
test_call.fail(error: "Something went wrong")
51+
52+
response = test_call.pop
53+
expect(response).to have_keys(
54+
id: be == 1,
55+
finished: be == true,
56+
failed: be == true,
57+
error: be == "Something went wrong"
58+
)
59+
60+
expect(test_call.closed?).to be == true
61+
end
62+
63+
it "can access message via []" do
64+
expect(test_call[:do]).to be == :test
65+
expect(test_call[:data]).to be == "value"
66+
end
67+
68+
it "reports closed? correctly" do
69+
expect(test_call.closed?).to be == false
70+
71+
test_call.finish
72+
73+
expect(test_call.closed?).to be == true
74+
end
75+
end
76+
77+
it "writes JSON with newline" do
78+
connection.write(id: 1, do: :test)
79+
80+
stream.rewind
81+
output = stream.read
82+
83+
# Check it's valid JSON with a newline
84+
expect(output[-1]).to be == "\n"
85+
86+
parsed = JSON.parse(output.chomp)
87+
expect(parsed).to have_keys(
88+
"id" => be == 1,
89+
"do" => be == "test"
90+
)
91+
end
92+
93+
it "parses JSON lines" do
94+
stream.string = JSON.dump({id: 1, do: "test"}) << "\n"
95+
stream.rewind
96+
97+
message = connection.read
98+
99+
# Connection.read uses symbolize_names: true (keys are symbols, values are as-is)
100+
expect(message).to have_keys(
101+
id: be == 1,
102+
do: be == "test"
103+
)
104+
end
105+
106+
it "returns nil when stream is closed" do
107+
stream.string = ""
108+
stream.rewind
109+
110+
message = connection.read
111+
112+
expect(message).to be_nil
113+
end
114+
115+
it "increments id by 2" do
116+
first_id = connection.next_id
117+
second_id = connection.next_id
118+
119+
expect(second_id).to be == first_id + 2
120+
end
121+
122+
with "#close" do
123+
it "closes all pending calls" do
124+
call1 = Async::Container::Supervisor::Connection::Call.new(connection, 1, {do: :test})
125+
call2 = Async::Container::Supervisor::Connection::Call.new(connection, 2, {do: :test})
126+
127+
connection.calls[1] = call1
128+
connection.calls[2] = call2
129+
130+
connection.close
131+
132+
expect(call1.closed?).to be == true
133+
expect(call2.closed?).to be == true
134+
expect(connection.calls).to be(:empty?)
135+
end
136+
137+
it "closes the stream" do
138+
connection.close
139+
expect(stream).to be(:closed?)
140+
end
141+
end
142+
end

test/async/container/server.rb

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
# Copyright, 2025, by Samuel Williams.
55

66
require "async/container/supervisor/a_server"
7-
require "sus/fixtures/console/null_logger"
7+
require "sus/fixtures/console/captured_logger"
88

99
describe Async::Container::Supervisor::Server do
10-
include Sus::Fixtures::Console::NullLogger
1110
include Async::Container::Supervisor::AServer
11+
include Sus::Fixtures::Console::CapturedLogger
1212

1313
it "can handle unexpected failures" do
1414
# First, send invalid JSON to trigger the error:
@@ -36,6 +36,10 @@
3636
)
3737

3838
stream.close
39+
40+
# Verify error was logged about the parsing failure
41+
error_logs = console_capture.select {|log| log[:severity] == :warn}
42+
expect(error_logs).not.to be(:empty?)
3943
end
4044

4145
with "failing monitor" do
@@ -45,9 +49,11 @@ def run
4549
end
4650

4751
def register(connection)
52+
raise "Monitor failed to register!"
4853
end
4954

5055
def remove(connection)
56+
raise "Monitor failed to remove!"
5157
end
5258

5359
def status(call)
@@ -58,6 +64,30 @@ def status(call)
5864

5965
let(:monitors) {[failing_monitor]}
6066

67+
it "can handle monitor registration failures" do
68+
# Send a register message:
69+
stream = endpoint.connect
70+
71+
message = {id: 1, do: :register, state: {process_id: ::Process.pid}}
72+
stream.puts(JSON.dump(message))
73+
stream.flush
74+
75+
# Read the response:
76+
response = JSON.parse(stream.gets, symbolize_names: true)
77+
78+
# The server should still finish despite the monitor error:
79+
expect(response).to have_keys(
80+
id: be == 1,
81+
finished: be == true
82+
)
83+
84+
# Verify error was logged about the monitor failure:
85+
error_log = console_capture.find {|log| log[:severity] == :error && log[:message] =~ /Error while registering process/}
86+
expect(error_log).to be_truthy
87+
88+
stream.close
89+
end
90+
6191
it "can handle monitor status failures" do
6292
# Send a status request:
6393
stream = endpoint.connect
@@ -71,14 +101,41 @@ def status(call)
71101

72102
# The server should still respond with a finished message despite the monitor error:
73103
expect(response).to have_keys(
74-
id: be == 1,
75-
finished: be == true,
76-
error: have_keys(
77-
class: be == "RuntimeError",
78-
message: be == "Monitor failed to get status!",
79-
backtrace: be_a(Array)
80-
)
104+
id: be == 1,
105+
finished: be == true,
106+
error: have_keys(
107+
class: be == "RuntimeError",
108+
message: be == "Monitor failed to get status!",
109+
backtrace: be_a(Array)
81110
)
111+
)
112+
113+
stream.close
114+
end
115+
116+
it "can handle monitor removal failures" do
117+
# Connect then disconnect to trigger removal:
118+
stream = endpoint.connect
119+
stream.close
120+
121+
# Give time for removal to process
122+
reactor.sleep(0.01)
123+
124+
# Verify error was logged about the monitor removal failure:
125+
error_log = console_capture.find {|log| log[:severity] == :error && log[:message] =~ /Error while removing process/}
126+
expect(error_log).to be_truthy
127+
128+
# Verify server is still working by sending a new request:
129+
stream = endpoint.connect
130+
message = {id: 1, do: :status}
131+
stream.puts(JSON.dump(message))
132+
stream.flush
133+
134+
response = JSON.parse(stream.gets, symbolize_names: true)
135+
expect(response).to have_keys(
136+
id: be == 1,
137+
finished: be == true
138+
)
82139

83140
stream.close
84141
end
@@ -98,6 +155,13 @@ def status(call)
98155
stream.puts(JSON.dump(message))
99156
stream.flush
100157

158+
# Wait for the message to be processed
159+
reactor.sleep(0.01)
160+
161+
# Verify a debug warning was logged about ignoring the message:
162+
debug_log = console_capture.find {|log| log[:severity] == :debug && log[:message] =~ /Ignoring message/}
163+
expect(debug_log).to be_truthy
164+
101165
# Send a valid message to confirm the server is still working:
102166
valid_message = {id: 3, do: :register, state: {process_id: ::Process.pid}}
103167
stream.puts(JSON.dump(valid_message))
@@ -145,4 +209,4 @@ def status(call)
145209

146210
stream.close
147211
end
148-
end
212+
end

0 commit comments

Comments
 (0)