123456789_123456789_123456789_123456789_123456789_

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:

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:

All methods that process the response have the following optional keyword parameters:

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 with syswrite, returns the socket

Constant Summary

Instance Method Summary

Instance Method Details

#after_teardown

Closes all io’s in @ios_to_close, also deletes them if they are files

[ GitHub ]

  
# 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.

Parameters:

  • &blk (Block)

    Passed the SSLContext.

Yields:

  • (OpenSSL::SSL::SSLContext)

Returns:

  • (OpenSSL::SSL::SSLContext)

    The new socket

[ GitHub ]

  
# 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

Parameters:

  • req (String, GET_11)

    request path

Returns:

  • (OpenSSL::SSL::SSLSocket, TCPSocket, UNIXSocket)

    the created socket

[ GitHub ]

  
# 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.

Parameters:

  • skts (Array<Sockets])

    an array of sockets that have already had requests sent

Returns:

  • (Array<String, Class>)

    an array matching the order of the parameter skts, contains the response or the error class generated by the socket.

[ GitHub ]

  
# 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

Parameters:

  • req (String, nil) (defaults to: GET_11)

    The request stirng.

  • req (String, GET_11) (defaults to: GET_11)

    request path

  • host: (String) (defaults to: nil)

    tcp/ssl host

  • port: (Integer/String) (defaults to: nil)

    tcp/ssl port

  • path: (String) (defaults to: nil)

    unix socket, full path

  • ctx: (OpenSSL::SSL::SSLContext) (defaults to: nil)

    ssl context

  • session: (OpenSSL::SSL::Session) (defaults to: nil)

    ssl session

Returns:

  • (OpenSSL::SSL::SSLSocket, TCPSocket, UNIXSocket)

    the created socket

[ GitHub ]

  
# 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

Parameters:

  • req (String) (defaults to: GET_11)

    the request

  • len (Integer)

    the number of requests to send

[ GitHub ]

  
# 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

Parameters:

  • req (String, GET_11) (defaults to: GET_11)

    request path

  • host: (String) (defaults to: nil)

    tcp/ssl host

  • port: (Integer/String) (defaults to: nil)

    tcp/ssl port

  • path: (String) (defaults to: nil)

    unix socket, full path

  • ctx: (OpenSSL::SSL::SSLContext) (defaults to: nil)

    ssl context

  • session: (OpenSSL::SSL::Session) (defaults to: nil)

    ssl session

Returns:

  • (String)

    socket read string

[ GitHub ]

  
# 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.

Parameters:

  • req (String, GET_11) (defaults to: GET_11)

    request path

  • host: (String) (defaults to: nil)

    tcp/ssl host

  • port: (Integer/String) (defaults to: nil)

    tcp/ssl port

  • path: (String) (defaults to: nil)

    unix socket, full path

  • ctx: (OpenSSL::SSL::SSLContext) (defaults to: nil)

    ssl context

  • session: (OpenSSL::SSL::Session) (defaults to: nil)

    ssl session

  • timeout: (Float, nil) (defaults to: nil)

    total socket read timeout, defaults to RESP_READ_TIMEOUT

  • len: (Integer, nil) (defaults to: nil)

    the read_nonblock maxlen, defaults to RESP_READ_LEN

Returns:

  • (Response)

    the body portion of the HTTP response

[ GitHub ]

  
# 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.

Parameters:

  • req (String, GET_11) (defaults to: GET_11)

    request path

  • host: (String) (defaults to: nil)

    tcp/ssl host

  • port: (Integer/String) (defaults to: nil)

    tcp/ssl port

  • path: (String) (defaults to: nil)

    unix socket, full path

  • ctx: (OpenSSL::SSL::SSLContext) (defaults to: nil)

    ssl context

  • session: (OpenSSL::SSL::Session) (defaults to: nil)

    ssl session

  • timeout: (Float, nil) (defaults to: nil)

    total socket read timeout, defaults to RESP_READ_TIMEOUT

  • len: (Integer, nil) (defaults to: nil)

    the read_nonblock maxlen, defaults to RESP_READ_LEN

Returns:

  • (Array<String>)

    array of header lines in the response

[ GitHub ]

  
# 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

Parameters:

  • req (String, GET_11) (defaults to: GET_11)

    request path

  • host: (String) (defaults to: nil)

    tcp/ssl host

  • port: (Integer/String) (defaults to: nil)

    tcp/ssl port

  • path: (String) (defaults to: nil)

    unix socket, full path

  • ctx: (OpenSSL::SSL::SSLContext) (defaults to: nil)

    ssl context

  • session: (OpenSSL::SSL::Session) (defaults to: nil)

    ssl session

  • timeout: (Float, nil) (defaults to: nil)

    total socket read timeout, defaults to RESP_READ_TIMEOUT

  • len: (Integer, nil) (defaults to: nil)

    the read_nonblock maxlen, defaults to RESP_READ_LEN

Returns:

[ GitHub ]

  
# 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

Parameters:

  • socket (OpenSSL::SSL::SSLSocket, TCPSocket, UNIXSocket)

Returns:

  • (Boolean)

    true if closed by server, false is indeterminate, as it may not be writable

[ GitHub ]

  
# 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