codegourmet

savory code and other culinary highlights

Integration Tests With Celluloid::IO, Part 2

| Comments

In the first post, we wrote an integration test setup for a Celluloid::IO application.

One remaining problem is that the tests are “absorbing” all exceptions that occur during worker execution, or are raising them some time later. This is because the workers can just crash internally and/or the worker thread is still running when the test is done.

Catching actor exceptions

A first solution is to store all occuring worker exceptions inside a variable in the test thread and then re-raise them when we’re done.

But first, our hack:

test_helper.rb:

1
2
3
4
# ...

WORKER_EXCEPTIONS = []
Celluloid.exception_handler {|ex| WORKER_EXCEPTIONS << ex }

request_response_test.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class RequestResponseTest < MiniTest::Test

  def test_responds_correctly
    tcp_server = TCPServer.new

    tcp_server.start

    client = TCPClient.new
    client.send('hello_message')
    sleep 0.05
    response = client.read

    assert_equal 'hello_response', response

    cleanup_worker(tcp_server)
  end

  protected

    def cleanup_worker(worker)
      Thread.new { worker.terminate if worker.alive? }.join

      unless WORKER_EXCEPTIONS.empty?
        ex = WORKER_EXCEPTIONS.first
        WORKER_EXCEPTIONS.clear
        raise ex
      end
    end

end

Above code terminates the worker if it’s still running and then raises the first worker exception that has been collected via Celluloid’s global exception handler.

Refactoring into test_helper

Here’s the refactored code from the first post, with the exception handling moved into the test helper:

test_helper.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
require 'celluloid/test'

WORKER_EXCEPTIONS = []
Celluloid.exception_handler {|ex| WORKER_EXCEPTIONS << ex }

Celluloid.boot

module CelluloidIOTest
  def before_setup
    Celluloid.shutdown
    Celluloid.boot
  end

  def with_tcp_server(&block)
    tcp_server = TCPServer.new # server init and startup

    begin
      tcp_server.start
    ensure
      cleanup_worker(tcp_server)
    end
  end

  def cleanup_worker(worker)
    Thread.new { worker.terminate if worker.alive? }.join

    unless WORKER_EXCEPTIONS.empty?
      ex = WORKER_EXCEPTIONS.first
      WORKER_EXCEPTIONS.clear
      raise ex
    end
  end

  # ...
end

class MiniTest::Test
  include CelluloidIOTest
end

The code for the test stays exactly the same as in the previous post, except that now worker exceptions are actually raised after test execution.

The global exception hack above looks like it’s not threadsafe, so we might experiment around with a more sophisticated setup in the future (scheduler, exception collecting actor). But as a hotfix it’s proved to be pretty reliable in our test runs.

If you know a better solution, feel invited to post it in the comments!

Happy Coding! – codegourmet

Comments