Class: JRPC::Transport::Test

Inherits:
Base
  • Object
show all
Defined in:
lib/jrpc/transport/test.rb

Defined Under Namespace

Classes: UnexpectedRequest

Constant Summary collapse

ConnectionError =

Resolve ‘raise ConnectionError`-style names to the transport hierarchy the clients rescue, mirroring Tcp (otherwise constant lookup finds JRPC::* v1).

Base::ConnectionError
Timeout =
Base::Timeout
MalformedFrame =
Base::MalformedFrame

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(server = 'test', **options) ⇒ Test

Returns a new instance of Test.



65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/jrpc/transport/test.rb', line 65

def initialize(server = 'test', **options)
  super
  @strict = options.fetch(:strict, true)
  @mon = Monitor.new
  @handlers = {}
  @inbound = []       # FIFO of [:frame, String] | [:raise, Exception]
  @sent = []          # raw payload strings exactly as the client wrote them
  @requests = []      # parsed request envelopes (Hash) in write order
  @notifications = [] # parsed notification envelopes (Hash) in write order
  @open = false
  @io = nil
  @signal = nil
  @fail_connect = nil
end

Class Method Details

.finalizer(io, signal) ⇒ Object

GC backstop: release the socketpair FDs if a transport is dropped without an explicit #shutdown. Returns a proc that captures only the two IOs, never self.



55
56
57
58
59
60
61
62
63
# File 'lib/jrpc/transport/test.rb', line 55

def self.finalizer(io, signal)
  proc do
    [io, signal].each do |sock|
      sock.close
    rescue StandardError
      nil
    end
  end
end

Instance Method Details

#closeObject



210
211
212
213
214
215
216
217
218
# File 'lib/jrpc/transport/test.rb', line 210

def close
  @mon.synchronize do
    @open = false
    @inbound.clear
    # Wake a loop blocked in IO.select so it re-checks closed? promptly.
    signal_readable
    true
  end
end

#closed?Boolean

closed? tracks the logical open flag, not the FD: #close keeps the socketpair alive (so a concurrent IO.select in SharedClient’s loop is never yanked) and only flips the flag, mirroring the proven spec helper.

Returns:

  • (Boolean)


163
164
165
# File 'lib/jrpc/transport/test.rb', line 163

def closed?
  @mon.synchronize { !@open }
end

#connectObject

— Transport interface (called from the client / SharedClient loop) ——-



151
152
153
154
155
156
157
158
# File 'lib/jrpc/transport/test.rb', line 151

def connect
  @mon.synchronize do
    raise @fail_connect if @fail_connect

    open_socketpair if @io.nil?
    @open = true
  end
end

#fail_connect(error = ConnectionError.new('connect failed')) ⇒ Object

Arm #connect to raise error on every attempt until cleared by #reset. Defaults to a ConnectionError so SharedClient’s loop treats it as a normal connect failure (drained), rather than a crash.



110
111
112
113
# File 'lib/jrpc/transport/test.rb', line 110

def fail_connect(error = ConnectionError.new('connect failed'))
  @mon.synchronize { @fail_connect = error }
  self
end

#last_requestObject



132
# File 'lib/jrpc/transport/test.rb', line 132

def last_request  = @mon.synchronize { @requests.last }

#notificationsObject



131
# File 'lib/jrpc/transport/test.rb', line 131

def notifications = @mon.synchronize { @notifications.dup }

#on(method, &block) ⇒ Object

Register a handler for method. The block receives the request params (Array, Hash, or nil) and its return value becomes the JSON-RPC result.

Raises:

  • (ArgumentError)


84
85
86
87
88
89
# File 'lib/jrpc/transport/test.rb', line 84

def on(method, &block)
  raise ArgumentError, 'on requires a block' unless block

  @mon.synchronize { @handlers[method.to_s] = block }
  self
end

#push_raise(error) ⇒ Object

Enqueue an error to be raised on the client’s next read, simulating a socket-level failure mid-stream. Pass a transport error for realistic behavior (e.g. JRPC::Transport::Base::ConnectionError.new(‘reset’)).



102
103
104
105
# File 'lib/jrpc/transport/test.rb', line 102

def push_raise(error)
  enqueue([:raise, error])
  self
end

#push_response(frame) ⇒ Object

Enqueue a literal inbound frame. Accepts a JSON String (used verbatim, so it may be intentionally malformed) or a Hash (serialized with JSON.generate).



93
94
95
96
97
# File 'lib/jrpc/transport/test.rb', line 93

def push_response(frame)
  payload = frame.is_a?(String) ? frame : JSON.generate(frame)
  enqueue([:frame, payload])
  self
end

#read_frameObject



185
186
187
188
189
190
191
192
193
194
# File 'lib/jrpc/transport/test.rb', line 185

def read_frame(**)
  @mon.synchronize do
    raise ConnectionError, 'transport closed' if closed_unlocked?

    entry = pop_inbound
    raise Timeout, 'read_frame: no scripted response available' if entry.nil?

    deliver(entry)
  end
end

#requestsObject



130
# File 'lib/jrpc/transport/test.rb', line 130

def requests      = @mon.synchronize { @requests.dup }

#resetObject

Clear recordings, the inbound queue, and any armed connect failure. Keeps registered handlers so a transport can be reused across examples.



117
118
119
120
121
122
123
124
125
126
127
# File 'lib/jrpc/transport/test.rb', line 117

def reset
  @mon.synchronize do
    @inbound.clear
    @sent.clear
    @requests.clear
    @notifications.clear
    @fail_connect = nil
    drain_signal
  end
  self
end

#sentObject



129
# File 'lib/jrpc/transport/test.rb', line 129

def sent          = @mon.synchronize { @sent.dup }

#shutdownObject

Close the socketpair FDs. Idempotent. Call from an after-hook for deterministic FD cleanup; otherwise the GC finalizer reclaims them.



136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/jrpc/transport/test.rb', line 136

def shutdown
  @mon.synchronize do
    @open = false
    [@io, @signal].each do |sock|
      sock&.close
    rescue StandardError
      nil
    end
    @io = nil
    @signal = nil
  end
end

#socketObject



167
168
169
# File 'lib/jrpc/transport/test.rb', line 167

def socket
  @mon.synchronize { @open ? @io : nil }
end

#try_read_frameObject



196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/jrpc/transport/test.rb', line 196

def try_read_frame
  @mon.synchronize do
    raise ConnectionError, 'transport closed' if closed_unlocked?

    entry = pop_inbound
    if entry.nil?
      drain_signal
      return :wait
    end

    deliver(entry)
  end
end

#write_frame(bytes) ⇒ Object



171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/jrpc/transport/test.rb', line 171

def write_frame(bytes, **)
  @mon.synchronize do
    raise ConnectionError, 'transport closed' if closed_unlocked?

    @sent << bytes
    envelope = JSON.parse(bytes)
    if envelope.key?('id')
      handle_request(envelope)
    else
      handle_notification(envelope)
    end
  end
end