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 rspec-users@rubyforge.org http://rubyforge.org/mailman/listinfo/rspec-users