Hi all
I've recently become aware that `expect { … }.to_not raise_error(SomeError)`
has been deprecated. I've found a few cases where this has been frustrating,
but I've hit one I'm completely stumped by.
I'm writing a Celluloid actor which invokes some user-provided procs with
(separately) user-provided values. As an example of something that might go
wrong, the object providing a value may provide the wrong number of arguments
for a given callback, and so an ArgumentError will be raised. Here's a cut-down
version of the code:
class Result
include Celluloid
# … lots of stuff omitted …
def on(handlers)
# … more stuff omitted …
handlers.fetch(@message_type).call(*@message_args)
end
end
Normally, if an error is raised in an actor method, that actor is killed. As
you can see, `call` is wide open to an ArgumentError. However, as this is the
client object's fault, not ours, I don't want the actor to crash. Celluloid
provides an `abort` method[1] for this purpose. My problem is in specifying the
behaviour. Naively, you might try to check if the actor is still alive:
it "doesn't crash the actor" do
expect {
actor.method_that_raises_an_error
}.to_not change { actor.alive? }.to(false)
end
Unfortunately, as the actor is running asynchronously, the example checks the
actor's alive state before it completely dies. One way to approach this is to
use Celluloid's actor guarantees and send it another message, which will be
handled synchronously after the first is processed, and will raise a
DeadActorError:
it "doesn't crash the actor" do
actor.method_that_raises_an_error rescue nil
expect {
actor.method_that_raises_an_error
}.to_not raise_error(Celluloid::DeadActorError)
end
Unfortunately, this prints a deprecation warning into the spec output, and
presumably this will actually fail in a future version of RSpec.
Now, it's obvious from the code that changing the expectation to just `to_not
raise_error()` is wrong, as we are expecting the method to raise an error – we
just want to make sure it's not a Celluloid::DeadActorError. I could rescue
specific errors inside the expect block, but now I'm duplicating knowledge from
other examples just to make this one run.
So another way to approach this is my waiting on the actor's thread. Again,
naively this is a starting point:
it "doesn't crash the actor" do
expect {
actor.method_that_raises_an_error rescue nil
Celluloid::Actor.join(actor)
}.to_not change { actor.alive? }.to(false)
end
There are two problems with this. The first is that it sets up a race
condition, because any delay in the RSpec thread, eg:
it "doesn't crash the actor" do
expect {
actor.method_that_raises_an_error rescue nil
sleep 0.1
Celluloid::Actor.join(actor)
}.to_not change { actor.alive? }.to(false)
end
Produces this error in Actor.join:
An error occurred in an after hook
Celluloid::DeadActorError: actor already terminated
Since the purpose of Celluloid / actors is to avoid this sort of problem, I
don't consider it an acceptable approach.
The second (much bigger) problem, is that it only helps describe the failure
case, once the actor is fixed to use `abort` rather than `raise`, it doesn't
crash, and so Actor.join blocks the RSpec thread indefinitely.
The only option remaining I can think of is to re-implement the negative error
expectation with begin/rescue, which is what I've done in other places I find
this pattern useful.
Does anybody have any suggestions that may help? I've exhausted my own ideas.
Thanks
Ash
[1]
https://github.com/celluloid/celluloid/wiki/Frequently-Asked-Questions#q-how-can-i-raise-an-exception-in-the-caller-without-crashing-the-receiver
--
http://www.patchspace.co.uk/
http://www.linkedin.com/in/ashmoran
smime.p7s
Description: S/MIME cryptographic signature
_______________________________________________ rspec-users mailing list [email protected] http://rubyforge.org/mailman/listinfo/rspec-users
