socketry / async-websocket

Asynchronous WebSocket client and server, supporting HTTP/1 and HTTP/2 for Ruby.
MIT License
166 stars 18 forks source link

Example for Cucumber step definitions #66

Closed schmijos closed 7 months ago

schmijos commented 7 months ago

I've got the following Cucumber step definitions implemented:

Given(/^the client is connected to the relay$/) do
  Async do |task|
    @endpoint = Async::HTTP::Endpoint.parse("ws://127.0.0.1:3000")
    @connection = Async::WebSocket::Client.connect(@endpoint)
    puts "1"
  end
  puts "2"
end

When(/^the client sends$/) do |message|
  @connection.write(message)
end

Then(/^the server responds with ([A-Z]+)$/) do |type|
  message = @connection.read
  assert_equal type, JSON.parse(message).first
end

Running them returns the following stdout:

1
 0.48s     warn: Async::Pool::Controller: Async::Pool::Controller Gardener [oid=0x10cc] [ec=0x10e0] [pid=70992] [2024-01-08 13:20:33 +0100]
               | Closing resource while still in use!
               | {"resource":"#<Async::HTTP::Protocol::HTTP1::Client:0x000000011eacdfe0>","usage":1}
2

The assertion passes. Everything works as expected. But I don't like this warning. Where does this come from?

schmijos commented 7 months ago

Using an around hook instead of a single step works correctly without warning.

Around("@connected") do |scenario, block|
  Async do
    @endpoint = Async::HTTP::Endpoint.parse("ws://127.0.0.1:3000")
    Async::WebSocket::Client.connect(@endpoint) do |connection|
      @connection = connection
      block.call
    end
  end
end

I guess this is because connect ensures connection closing itself if called with a block?

The following works as well

Around("@connected") do |scenario, block|
  Async do
    @endpoint = Async::HTTP::Endpoint.parse("ws://127.0.0.1:3000")
    @connection = Async::WebSocket::Client.connect(@endpoint)
    block.call
  ensure
    @connection.close
  end
end

And this more generic version works as well. But it bugs me that I cannot find out how to generically delay the Async::Stop until all the work is done.

Given(/^the client is connected to the relay$/) do
  @endpoint = Async::HTTP::Endpoint.parse("ws://127.0.0.1:3000")
  @connection = Async::WebSocket::Client.connect(@endpoint)
end

Around do |scenario, block|
  Async do
    block.call
  ensure
    @connection&.close
  end
end
ioquatix commented 7 months ago

As you figured out, the connection is closed automatically when it goes out of scope. But the connection is still in use by the WebSocket instance. So, a warning is issued that we are closing the HTTP connection while it still appears to be in use (i.e. not checked back into the connection pool).

As you implemented, calling @connection.close is the correct solution, as this closes the WebSocket correctly and returns the HTTP connection to the connection pool.

Async::Stop is used at the end of the top level Async{} block to shut down all tasks. There is no way to persist a task outside of the Async{} block by design. However, the Fiber.scheduler interface does not enforce this. Generally speaking, you don't want to leak resources between tests, so this should be the right behaviour.

Does that answer your question?

schmijos commented 7 months ago

Yes, thank you very much! I ended up with the following "idiomatic" solution to Cucumber testing with Async:

  1. "Configure" Async support for Cucumber:

    # features/support/env.rb
    Around do |_scenario, block|
    Async { block.call }
    end
  2. Use Async-dependent tools like Async::WebSocket in step definitions

    # features/step_definitions/connection.rb
    Given(/^the client is connected to the relay$/) do
    @endpoint = Async::HTTP::Endpoint.parse("ws://127.0.0.1:3000")
    @connection = Async::WebSocket::Client.connect(@endpoint)
    end
    
    After do
    @connection&.close
    end