class ClientTest

Constants

WAIT_WHEN_EXPECTING_EVENT
WAIT_WHEN_NOT_EXPECTING_EVENT

Public Instance Methods

concurrently(enum) { |*x| ... } click to toggle source
# File actioncable/test/client_test.rb, line 203
def concurrently(enum)
  enum.map { |*x| Concurrent::Future.execute { yield(*x) } }.map(&:value!)
end
setup() click to toggle source
# File actioncable/test/client_test.rb, line 56
def setup
  ActionCable.instance_variable_set(:@server, nil)
  server = ActionCable.server
  server.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN }

  server.config.cable = ActiveSupport::HashWithIndifferentAccess.new(adapter: "async")

  # and now the "real" setup for our test:
  server.config.disable_request_forgery_protection = true
end
test_disappearing_client() click to toggle source
# File actioncable/test/client_test.rb, line 258
def test_disappearing_client
  with_puma_server do |port|
    c = websocket_client(port)
    assert_equal({ "type" => "welcome" }, c.read_message)  # pop the first welcome message off the stack
    c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
    assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "type" => "confirm_subscription" }, c.read_message)
    c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "delay", message: "hello")
    c.close # disappear before write

    c = websocket_client(port)
    assert_equal({ "type" => "welcome" }, c.read_message) # pop the first welcome message off the stack
    c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
    assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "type" => "confirm_subscription" }, c.read_message)
    c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "ding", message: "hello")
    assert_equal({ "identifier" => '{"channel":"ClientTest::EchoChannel"}', "message" => { "dong" => "hello" } }, c.read_message)
    c.close # disappear before read
  end
end
test_interacting_clients() click to toggle source
# File actioncable/test/client_test.rb, line 219
def test_interacting_clients
  with_puma_server do |port|
    clients = concurrently(10.times) { websocket_client(port) }

    barrier_1 = Concurrent::CyclicBarrier.new(clients.size)
    barrier_2 = Concurrent::CyclicBarrier.new(clients.size)

    concurrently(clients) do |c|
      assert_equal({ "type" => "welcome" }, c.read_message)  # pop the first welcome message off the stack
      c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
      assert_equal({ "identifier" => '{"channel":"ClientTest::EchoChannel"}', "type" => "confirm_subscription" }, c.read_message)
      c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "ding", message: "hello")
      assert_equal({ "identifier" => '{"channel":"ClientTest::EchoChannel"}', "message" => { "dong" => "hello" } }, c.read_message)
      barrier_1.wait WAIT_WHEN_EXPECTING_EVENT
      c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "bulk", message: "hello")
      barrier_2.wait WAIT_WHEN_EXPECTING_EVENT
      assert_equal clients.size, c.read_messages(clients.size).size
    end

    concurrently(clients, &:close)
  end
end
test_many_clients() click to toggle source
# File actioncable/test/client_test.rb, line 242
def test_many_clients
  with_puma_server do |port|
    clients = concurrently(100.times) { websocket_client(port) }

    concurrently(clients) do |c|
      assert_equal({ "type" => "welcome" }, c.read_message)  # pop the first welcome message off the stack
      c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
      assert_equal({ "identifier" => '{"channel":"ClientTest::EchoChannel"}', "type" => "confirm_subscription" }, c.read_message)
      c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "ding", message: "hello")
      assert_equal({ "identifier" => '{"channel":"ClientTest::EchoChannel"}', "message" => { "dong" => "hello" } }, c.read_message)
    end

    concurrently(clients, &:close)
  end
end
test_server_restart() click to toggle source
# File actioncable/test/client_test.rb, line 301
def test_server_restart
  with_puma_server do |port|
    c = websocket_client(port)
    assert_equal({ "type" => "welcome" }, c.read_message)
    c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
    assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "type" => "confirm_subscription" }, c.read_message)

    ActionCable.server.restart
    c.wait_for_close
    assert c.closed?
  end
end
test_single_client() click to toggle source
# File actioncable/test/client_test.rb, line 207
def test_single_client
  with_puma_server do |port|
    c = websocket_client(port)
    assert_equal({ "type" => "welcome" }, c.read_message)  # pop the first welcome message off the stack
    c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
    assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "type" => "confirm_subscription" }, c.read_message)
    c.send_message command: "message", identifier: JSON.generate(channel: "ClientTest::EchoChannel"), data: JSON.generate(action: "ding", message: "hello")
    assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "message" => { "dong" => "hello" } }, c.read_message)
    c.close
  end
end
test_unsubscribe_client() click to toggle source
# File actioncable/test/client_test.rb, line 277
def test_unsubscribe_client
  with_puma_server do |port|
    app = ActionCable.server
    identifier = JSON.generate(channel: "ClientTest::EchoChannel")

    c = websocket_client(port)
    assert_equal({ "type" => "welcome" }, c.read_message)
    c.send_message command: "subscribe", identifier: identifier
    assert_equal({ "identifier" => "{\"channel\":\"ClientTest::EchoChannel\"}", "type" => "confirm_subscription" }, c.read_message)
    assert_equal(1, app.connections.count)
    assert(app.remote_connections.where(identifier: identifier))

    subscriptions = app.connections.first.subscriptions.send(:subscriptions)
    assert_not_equal 0, subscriptions.size, "Missing EchoChannel subscription"
    channel = subscriptions.first[1]
    channel.expects(:unsubscribed)
    c.close
    sleep 0.1 # Data takes a moment to process

    # All data is removed: No more connection or subscription information!
    assert_equal(0, app.connections.count)
  end
end
websocket_client(port) click to toggle source
# File actioncable/test/client_test.rb, line 199
def websocket_client(port)
  SyncClient.new(port)
end
with_puma_server(rack_app = ActionCable.server, port = 3099) { |port| ... } click to toggle source
# File actioncable/test/client_test.rb, line 67
def with_puma_server(rack_app = ActionCable.server, port = 3099)
  server = ::Puma::Server.new(rack_app, ::Puma::Events.strings)
  server.add_tcp_listener "127.0.0.1", port
  server.min_threads = 1
  server.max_threads = 4

  thread = server.run

  begin
    yield port

  ensure
    server.stop

    begin
      thread.join

    rescue IOError
      # Work around https://bugs.ruby-lang.org/issues/13405
      #
      # Puma's sometimes raising while shutting down, when it closes
      # its internal pipe. We can safely ignore that, but we do need
      # to do the step skipped by the exception:
      server.binder.close

    rescue RuntimeError => ex
      # Work around https://bugs.ruby-lang.org/issues/13239
      raise unless ex.message =~ /can't modify frozen IOError/

      # Handle this as if it were the IOError: do the same as above.
      server.binder.close
    end
  end
end