Module: TestPuma::PumaSocket
Relationships & Source Files | |
Defined in: | test/helpers/test_puma/puma_socket.rb |
Overview
This module is included in CI test files, and provides methods to create client sockets. Normally, the socket parameters are defined by the code creating the ::Puma
server (in-process or spawned), so they do not need to be specified. Regardless, many of the less frequently used parameters still have keyword arguments and they can be set to whatever is required.
This module closes all sockets and performs all reads non-blocking and all writes using syswrite. These are helpful for reliable tests. Please do not use native Ruby sockets except if absolutely necessary.
Methods that return a socket or sockets:
-
#new_socket - Opens a socket
-
#send_http - Opens a socket and sends a request, which defaults to GET_11
-
#send_http_array - Creates an array of sockets. It opens each and sends a request on each
All methods that create a socket have the following optional keyword parameters:
-
host:
- tcp/ssl host (String
) -
port:
- tcp/ssl port (Integer
,String
) -
path:
- unix socket, full path (String
) -
ctx:
- ssl context (OpenSSL::SSL::SSLContext
) -
session:
- ssl session (OpenSSL::SSL::Session
)
Methods that process the response:
-
#send_http_read_response - sends a request and returns the whole response
-
#send_http_read_resp_body - sends a request and returns the response body
-
#send_http_read_resp_headers - sends a request and returns the response with the body removed as an array of lines
All methods that process the response have the following optional keyword parameters:
-
timeout:
- total socket read timeout, defaults to RESP_READ_TIMEOUT (Float
) -
len:
- theread_nonblock
maxlen, defaults to RESP_READ_LEN (Integer
)
Methods added to socket instances:
-
read_response
- reads the response and returns it, uses READ_RESPONSE -
read_body
- reads the response and returns the body, uses READ_BODY -
<<
- overrides the standard method, writes to the socket withsyswrite
, returns the socket
Constant Summary
-
EMPTY_200 =
# File 'test/helpers/test_puma/puma_socket.rb', line 69[200, {}, ['']]
-
GET_10 =
# File 'test/helpers/test_puma/puma_socket.rb', line 60"GET / HTTP/1.0\r\n\r\n"
-
GET_11 =
# File 'test/helpers/test_puma/puma_socket.rb', line 61"GET / HTTP/1.1\r\n\r\n"
-
HELLO_11 =
# File 'test/helpers/test_puma/puma_socket.rb', line 63"HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\n" \ "Content-Length: 11\r\n\r\nHello World"
-
NO_ENTITY_BODY =
# File 'test/helpers/test_puma/puma_socket.rb', line 68Puma::STATUS_WITH_NO_ENTITY_BODY
-
READ_BODY =
# File 'test/helpers/test_puma/puma_socket.rb', line 206#=> (timeout: nil, len: nil) { self.read_response(timeout: nil, len: nil) .split(RESP_SPLIT, 2).last }
-
READ_RESPONSE =
# File 'test/helpers/test_puma/puma_socket.rb', line 211#=> (timeout: nil, len: nil) do content_length = nil chunked = nil status = nil no_body = nil response = Response.new read_len = len || RESP_READ_LEN timeout ||= RESP_READ_TIMEOUT time_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) time_end = time_start + timeout times = [] time_read = nil loop do begin self.to_io.wait_readable timeout time_read ||= Process.clock_gettime(Process::CLOCK_MONOTONIC) part = self.read_nonblock(read_len, exception: false) case part when String times << (Process.clock_gettime(Process::CLOCK_MONOTONIC) - time_read).round(4) status ||= part[/\AHTTP\/1\.[01] (\d{3})/, 1] if status no_body ||= NO_ENTITY_BODY.key? status.to_i || status.to_i < 200 end if no_body && part.end_with?(RESP_SPLIT) response.times = times return response << part end unless content_length || chunked chunked ||= part.downcase.include? "\r\ntransfer-encoding: chunked\r\n" content_length = (t = part[/^Content-Length: (\d+)/i , 1]) ? t.to_i : nil end response << part hdrs, body = response.split RESP_SPLIT, 2 unless body.nil? # below could be simplified, but allows for debugging... finished = if content_length body.bytesize == content_length elsif chunked body.end_with? "0\r\n\r\n" elsif !hdrs.empty? && !body.empty? true else false end response.times = times return response if finished end sleep 0.000_1 when :wait_readable # continue loop when :wait_writable # :wait_writable for ssl to = time_end - Process.clock_gettime(Process::CLOCK_MONOTONIC) self.to_io.wait_writable to when nil if response.empty? raise EOFError else response.times = times return response end end timeout = time_end - Process.clock_gettime(Process::CLOCK_MONOTONIC) if timeout <= 0 raise Timeout::Error, 'Client Read Timeout' end end end end
-
REQ_WRITE =
TODO:
verify whole string is written
#=> (str) { self.syswrite str; self }
-
RESP_READ_LEN =
# File 'test/helpers/test_puma/puma_socket.rb', line 6665_536
-
RESP_READ_TIMEOUT =
# File 'test/helpers/test_puma/puma_socket.rb', line 6710
-
SET_TCP_NODELAY =
# File 'test/helpers/test_puma/puma_socket.rb', line 73Socket.const_defined?(:IPPROTO_TCP) && ::Socket.const_defined?(:TCP_NODELAY)
-
UTF8 =
# File 'test/helpers/test_puma/puma_socket.rb', line 71::Encoding::UTF_8
Instance Method Summary
-
#after_teardown
Closes all io’s in
@ios_to_close
, also deletes them if they are files. - #before_setup
-
#new_ctx {|OpenSSL::SSL::SSLContext| ... } ⇒ OpenSSL::SSL::SSLContext
Helper for creating an
OpenSSL::SSL::SSLContext
. -
#new_socket(host: nil, port: nil, path: nil, ctx: nil, session: nil) ⇒ OpenSSL::SSL::SSLSocket, ...
Creates a new client socket.
-
#read_response_array(skts, resp_count: nil, body_only: nil) ⇒ Array<String, Class>
Reads an array of sockets that have already had requests sent.
-
#send_http(req = GET_11, host: nil, port: nil, path: nil, ctx: nil, session: nil) ⇒ OpenSSL::SSL::SSLSocket, ...
Sends a request and returns the socket.
-
#send_http_array(req = GET_11, len, dly: 0.000_1, max_retries: 5) ⇒ Array<OpenSSL::SSL::SSLSocket, TCPSocket, UNIXSocket>
Creates an array of sockets, sending a request on each.
-
#send_http_read_all(req = GET_11, host: nil, port: nil, path: nil, ctx: nil, session: nil, len: nil, timeout: nil) ⇒ String
Sends a request and returns whatever can be read.
-
#send_http_read_resp_body(req = GET_11, host: nil, port: nil, path: nil, ctx: nil, session: nil, len: nil, timeout: nil) ⇒ Response
Sends a request and returns the HTTP response body.
-
#send_http_read_resp_headers(req = GET_11, host: nil, port: nil, path: nil, ctx: nil, session: nil, len: nil, timeout: nil) ⇒ Array<String>
Sends a request and returns the response header lines as an array of strings.
-
#send_http_read_response(req = GET_11, host: nil, port: nil, path: nil, ctx: nil, session: nil, len: nil, timeout: nil) ⇒ Response
Sends a request and returns the HTTP response.
-
#skt_closed_by_server(socket) ⇒ Boolean
Determines whether the socket has been closed by the server.
Instance Method Details
#after_teardown
Closes all io’s in @ios_to_close
, also deletes them if they are files
# File 'test/helpers/test_puma/puma_socket.rb', line 85
def after_teardown return if skipped? super # Errno::EBADF raised on macOS @ios_to_close.each do |io| begin if io.respond_to? :sysclose io.sync_close = true io.sysclose unless io.closed? else io.close if io.respond_to?(:close) && !io.closed? if io.is_a?(File) && (path = io&.path) && File.exist?(path) File.unlink path end end rescue Errno::EBADF, Errno::ENOENT, IOError ensure io = nil end end # not sure about below, may help with gc... @ios_to_close.clear @ios_to_close = nil end
#before_setup
[ GitHub ]# File 'test/helpers/test_puma/puma_socket.rb', line 75
def before_setup @ios_to_close ||= [] @bind_port = nil @bind_path = nil @control_port = nil @control_path = nil super end
#new_ctx {|OpenSSL::SSL::SSLContext| ... } ⇒ OpenSSL::SSL::SSLContext
Helper for creating an OpenSSL::SSL::SSLContext
.
# File 'test/helpers/test_puma/puma_socket.rb', line 292
def new_ctx(&blk) ctx = OpenSSL::SSL::SSLContext.new if blk yield ctx else ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE end ctx end
#new_socket(host: nil, port: nil, path: nil, ctx: nil, session: nil) ⇒ OpenSSL::SSL::SSLSocket
, ...
Creates a new client socket. TCP, SSL, and UNIX are supported
# File 'test/helpers/test_puma/puma_socket.rb', line 306
def new_socket(host: nil, port: nil, path: nil, ctx: nil, session: nil) port ||= @bind_port path ||= @bind_path ip ||= (host || HOST.ip).gsub RE_HOST_TO_IP, '' # in case a URI style IPv6 is passed skt = if path && !port && !ctx UNIXSocket.new path.sub(/\A@/, "\0") # sub is for abstract elsif port # && !path tcp = TCPSocket.new ip, port.to_i tcp.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if SET_TCP_NODELAY if ctx ::OpenSSL::SSL::SSLSocket.new tcp, ctx else tcp end else raise 'port or path must be set!' end skt.define_singleton_method :read_response, READ_RESPONSE skt.define_singleton_method :read_body, READ_BODY skt.define_singleton_method :<<, REQ_WRITE skt.define_singleton_method :req_write, REQ_WRITE # used for chaining @ios_to_close << skt if ctx @ios_to_close << tcp skt.session = session if session skt.sync_close = true skt.connect end skt end
#read_response_array(skts, resp_count: nil, body_only: nil) ⇒ Array
<String
, Class
>
Reads an array of sockets that have already had requests sent.
# File 'test/helpers/test_puma/puma_socket.rb', line 369
def read_response_array(skts, resp_count: nil, body_only: nil) results = Array.new skts.length Thread.new do until skts.compact.empty? skts.each_with_index do |skt, idx| next if skt.nil? begin next unless skt.wait_readable 0.000_5 if resp_count resp = skt.read_response.dup cntr = 0 until resp.split(RESP_SPLIT).length == resp_count + 1 || cntr > 20 cntr += 1 Thread.pass if skt.wait_readable 0.001 begin resp << skt.read_response rescue EOFError break end end end results[idx] = resp else results[idx] = body_only ? skt.read_body : skt.read_response end rescue StandardError => e results[idx] = e.class end begin skt.close unless skt.closed? # skt.close may return Errno::EBADF rescue StandardError => e results[idx] ||= e.class end skts[idx] = nil end end end.join 15 results end
#send_http(req = GET_11, host: nil, port: nil, path: nil, ctx: nil, session: nil) ⇒ OpenSSL::SSL::SSLSocket
, ...
Sends a request and returns the socket
# File 'test/helpers/test_puma/puma_socket.rb', line 179
def send_http(req = GET_11, host: nil, port: nil, path: nil, ctx: nil, session: nil) skt = new_socket host: host, port: port, path: path, ctx: ctx, session: session skt.syswrite req skt end
#send_http_array(req = GET_11, len, dly: 0.000_1, max_retries: 5) ⇒ Array
<OpenSSL::SSL::SSLSocket
, TCPSocket
, UNIXSocket
>
Creates an array of sockets, sending a request on each
# File 'test/helpers/test_puma/puma_socket.rb', line 345
def send_http_array(req = GET_11, len, dly: 0.000_1, max_retries: 5) Array.new(len) { retries = 0 begin skt = send_http req sleep dly skt rescue Errno::ECONNREFUSED retries += 1 if retries < max_retries retry else flunk 'Generate requests failed from Errno::ECONNREFUSED' end end } end
#send_http_read_all(req = GET_11, host: nil, port: nil, path: nil, ctx: nil, session: nil, len: nil, timeout: nil) ⇒ String
Sends a request and returns whatever can be read. Use when multiple responses are sent by the server
# File 'test/helpers/test_puma/puma_socket.rb', line 141
def send_http_read_all(req = GET_11, host: nil, port: nil, path: nil, ctx: nil, session: nil, len: nil, timeout: nil) skt = send_http req, host: host, port: port, path: path, ctx: ctx, session: session read = String.new # rubocop: disable Performance/UnfreezeString counter = 0 prev_size = 0 loop do raise(Timeout::Error, 'Client Read Timeout') if counter > 5 if skt.wait_readable 1 read << skt.sysread(RESP_READ_LEN) end ttl_read = read.bytesize return read if prev_size == ttl_read && !ttl_read.zero? prev_size = ttl_read counter += 1 end rescue EOFError return read rescue => e raise e end
#send_http_read_resp_body(req = GET_11, host: nil, port: nil, path: nil, ctx: nil, session: nil, len: nil, timeout: nil) ⇒ Response
Sends a request and returns the HTTP response body.
# File 'test/helpers/test_puma/puma_socket.rb', line 130
def send_http_read_resp_body(req = GET_11, host: nil, port: nil, path: nil, ctx: nil, session: nil, len: nil, timeout: nil) skt = send_http req, host: host, port: port, path: path, ctx: ctx, session: session skt.read_body timeout: timeout, len: len end
#send_http_read_resp_headers(req = GET_11, host: nil, port: nil, path: nil, ctx: nil, session: nil, len: nil, timeout: nil) ⇒ Array
<String
>
Sends a request and returns the response header lines as an array of strings. Includes the status line.
# File 'test/helpers/test_puma/puma_socket.rb', line 118
def send_http_read_resp_headers(req = GET_11, host: nil, port: nil, path: nil, ctx: nil, session: nil, len: nil, timeout: nil) skt = send_http req, host: host, port: port, path: path, ctx: ctx, session: session resp = skt.read_response timeout: timeout, len: len resp.split(RESP_SPLIT, 2).first.split "\r\n" end
#send_http_read_response(req = GET_11, host: nil, port: nil, path: nil, ctx: nil, session: nil, len: nil, timeout: nil) ⇒ Response
Sends a request and returns the HTTP response. Assumes one response is sent
# File 'test/helpers/test_puma/puma_socket.rb', line 168
def send_http_read_response(req = GET_11, host: nil, port: nil, path: nil, ctx: nil, session: nil, len: nil, timeout: nil) skt = send_http req, host: host, port: port, path: path, ctx: ctx, session: session skt.read_response timeout: timeout, len: len end
#skt_closed_by_server(socket) ⇒ Boolean
Determines whether the socket has been closed by the server. Only works when Socket::TCP_INFO is defined
, linux/Ubuntu
# File 'test/helpers/test_puma/puma_socket.rb', line 191
def skt_closed_by_server(socket) skt = socket.to_io return false unless skt.kind_of?(TCPSocket) begin tcp_info = skt.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_INFO) rescue IOError, SystemCallError false else state = tcp_info.unpack('C')[0] # TIME_WAIT: 6, CLOSE: 7, CLOSE_WAIT: 8, LAST_ACK: 9, CLOSING: 11 (state >= 6 && state <= 9) || state == 11 end end