PSPDFKit-labs / bypass

Bypass provides a quick way to create a custom plug that can be put in place instead of an actual HTTP server to return prebaked responses to client requests.
https://hex.pm/packages/bypass
MIT License
972 stars 113 forks source link

Bypass hangs in Bypass.do_verify_expectations/2 when using hackney inside a task with a timeout #75

Open silicium14 opened 5 years ago

silicium14 commented 5 years ago

I want to test that I'm able to abort a request that takes too long for any reason. I decided to use a task to wrap my request, the task is shut down after a timeout. I use bypass in my test to simulate a request that takes longer than the timeout value.

When using httpc to make the request the test succeeds. When using hackney to make the request, the test hangs in Bypass.do_verify_expectations/2.

I use {:hackney, "~> 1.15"} and {:bypass, "~> 1.0.0", only: :test}. Below is the test runner output and the code for the two tests.

1) test bypass with hackney (HackneyBypassTest)
     test/peertube_index/hackney_bypass_test.exs:5
     ** (ExUnit.TimeoutError) on_exit callback timed out after 2000ms. You can change the timeout:

       1. per test by setting "@tag timeout: x"
       2. per case by setting "@moduletag timeout: x"
       3. globally via "ExUnit.start(timeout: x)" configuration
       4. or set it to infinity per run by calling "mix test --trace"
          (useful when using IEx.pry)

     Timeouts are given as integers in milliseconds.

     stacktrace:
       (stdlib) gen.erl:169: :gen.do_call/4
       (elixir) lib/gen_server.ex:921: GenServer.call/3
       (bypass) lib/bypass.ex:71: Bypass.do_verify_expectations/2
       (ex_unit) lib/ex_unit/on_exit_handler.ex:140: ExUnit.OnExitHandler.exec_callback/1
       (ex_unit) lib/ex_unit/on_exit_handler.ex:126: ExUnit.OnExitHandler.on_exit_runner_loop/0
defmodule HackneyBypassTest do
  use ExUnit.Case

  @tag timeout: 2000
  test "bypass with hackney" do
    bypass = Bypass.open
    reponse_delay = 600
    timeout = reponse_delay - 100

    Bypass.expect(bypass, "GET", "/", fn conn ->
      Process.sleep(reponse_delay)
      Plug.Conn.resp(conn, 200, "response data")
    end)

    request = Task.async(fn ->
      :hackney.get("http://localhost:#{bypass.port}", [], "", [follow_redirect: true, with_body: true])
    end)
    result =
    case Task.yield(request, timeout) || Task.shutdown(request) do
      {:ok, result} ->
        {:ok, result}
      nil ->
        {:error, :timeout}
    end

    assert result == {:error, :timeout}
    IO.puts "DEBUG: End of test reached"
  end

  test "bypass with httpc" do
    bypass = Bypass.open
    reponse_delay = 600
    timeout = reponse_delay - 100

    Bypass.expect(bypass, "GET", "/", fn conn ->
      Process.sleep(reponse_delay)
      Plug.Conn.resp(conn, 200, "response data")
    end)

    request = Task.async(fn ->
      :httpc.request(:get, {String.to_charlist("http://localhost:#{bypass.port}"), []}, [], body_format: :binary)
    end)
    result =
    case Task.yield(request, timeout) || Task.shutdown(request) do
      {:ok, result} ->
        {:ok, result}
      nil ->
        {:error, :timeout}
    end

    assert result == {:error, :timeout}
    IO.puts "DEBUG: End of test reached"
  end
end
dalefukami commented 5 years ago

I had a similar issue and managed to resolve it by adding a Bypass.pass(bypass) call just prior to my assertion (or just before the end of the test). It's kind of a work around, I think, but it solved my issue. So, using your example, maybe this would work:

  @tag timeout: 2000
  test "bypass with hackney" do
    ...

    assert result == {:error, :timeout}

    # Clear any expectation of bypassed requests completing
    Bypass.pass(bypass)

    IO.puts "DEBUG: End of test reached"
  end