diff --git a/akka-actor/src/main/scala/akka/event/Logging.scala b/akka-actor/src/main/scala/akka/event/Logging.scala index 0777d9aef1..111d5d5dd8 100644 --- a/akka-actor/src/main/scala/akka/event/Logging.scala +++ b/akka-actor/src/main/scala/akka/event/Logging.scala @@ -586,6 +586,7 @@ object Logging { /** Null Object used for errors without cause Throwable */ object NoCause extends NoStackTrace } + def noCause = Error.NoCause /** * For WARNING Logging diff --git a/akka-actor/src/main/scala/akka/japi/JavaAPI.scala b/akka-actor/src/main/scala/akka/japi/JavaAPI.scala index bc8c3c6ff9..d3123153da 100644 --- a/akka-actor/src/main/scala/akka/japi/JavaAPI.scala +++ b/akka-actor/src/main/scala/akka/japi/JavaAPI.scala @@ -46,7 +46,9 @@ trait Creator[T] { } object PurePartialFunction { - case object NoMatch extends RuntimeException with NoStackTrace + sealed abstract class NoMatchException extends RuntimeException with NoStackTrace + case object NoMatch extends NoMatchException + final def noMatch(): RuntimeException = NoMatch } /** @@ -86,7 +88,16 @@ abstract class PurePartialFunction[A, B] extends scala.runtime.AbstractFunction1 final def isDefinedAt(x: A): Boolean = try { apply(x, true); true } catch { case NoMatch ⇒ false } final def apply(x: A): B = try apply(x, false) catch { case NoMatch ⇒ throw new MatchError } - final def noMatch(): RuntimeException = NoMatch +} + +abstract class CachingPartialFunction[A, B <: AnyRef] extends scala.runtime.AbstractFunction1[A, B] with PartialFunction[A, B] { + import PurePartialFunction._ + + def `match`(x: A): B + + var cache: B = _ + final def isDefinedAt(x: A): Boolean = try { cache = `match`(x); true } catch { case NoMatch ⇒ cache = null.asInstanceOf[B]; false } + final def apply(x: A): B = cache } /** @@ -164,4 +175,6 @@ object Util { def manifest[T](clazz: Class[T]): Manifest[T] = Manifest.classType(clazz) def arrayToSeq[T](arr: Array[T]): Seq[T] = arr.toSeq + + def arrayToSeq(classes: Array[Class[_]]): Seq[Class[_]] = classes.toSeq } diff --git a/akka-docs/java/code/docs/testkit/TestKitDocTest.java b/akka-docs/java/code/docs/testkit/TestKitDocTest.java index d71f086104..a5f85019ea 100644 --- a/akka-docs/java/code/docs/testkit/TestKitDocTest.java +++ b/akka-docs/java/code/docs/testkit/TestKitDocTest.java @@ -1,22 +1,31 @@ -/* - * +/** + * Copyright (C) 2009-2012 Typesafe Inc. */ package docs.testkit; import static org.junit.Assert.*; import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.Config; + +import akka.actor.ActorKilledException; +import akka.actor.ActorRef; import akka.actor.ActorSystem; +import akka.actor.Kill; import akka.actor.Props; import akka.actor.UntypedActor; +import akka.actor.UntypedActorFactory; import akka.dispatch.Await; import akka.dispatch.Future; -import akka.japi.JAPI; -import akka.japi.PurePartialFunction; +import akka.testkit.CallingThreadDispatcher; +import akka.testkit.TestActor; +import akka.testkit.TestActor.AutoPilot; import akka.testkit.TestActorRef; -import akka.testkit.TestKit; +import akka.testkit.JavaTestKit; import akka.util.Duration; public class TestKitDocTest { @@ -37,8 +46,11 @@ public class TestKitDocTest { private static ActorSystem system; - public TestKitDocTest() { - system = ActorSystem.create(); + @BeforeClass + public static void setup() { + final Config config = ConfigFactory.parseString( + "akka.event-handlers = [akka.testkit.TestEventListener]"); + system = ActorSystem.create("demoSystem", config); } @AfterClass @@ -50,82 +62,338 @@ public class TestKitDocTest { @Test public void demonstrateTestActorRef() { final Props props = new Props(MyActor.class); - final TestActorRef ref = TestActorRef.apply(props, system); + final TestActorRef ref = TestActorRef.create(system, props, "testA"); final MyActor actor = ref.underlyingActor(); assertTrue(actor.testMe()); } //#test-actor-ref - //#test-behavior @Test public void demonstrateAsk() throws Exception { + //#test-behavior final Props props = new Props(MyActor.class); - final TestActorRef ref = TestActorRef.apply(props, system); + final TestActorRef ref = TestActorRef.create(system, props, "testB"); final Future future = akka.pattern.Patterns.ask(ref, "say42", 3000); assertTrue(future.isCompleted()); assertEquals(42, Await.result(future, Duration.Zero())); + //#test-behavior } - //#test-behavior - //#test-expecting-exceptions @Test public void demonstrateExceptions() { + //#test-expecting-exceptions final Props props = new Props(MyActor.class); - final TestActorRef ref = TestActorRef.apply(props, system); + final TestActorRef ref = TestActorRef.create(system, props, "myActor"); try { ref.receive(new Exception("expected")); fail("expected an exception to be thrown"); } catch (Exception e) { assertEquals("expected", e.getMessage()); } + //#test-expecting-exceptions } - //#test-expecting-exceptions - //#test-within @Test public void demonstrateWithin() { - new TestKit(system) {{ - testActor().tell(42); - new Within(Duration.parse("1 second")) { + //#test-within + new JavaTestKit(system) {{ + getRef().tell(42); + new Within(Duration.Zero(), Duration.parse("1 second")) { // do not put code outside this method, will run afterwards public void run() { assertEquals((Integer) 42, expectMsgClass(Integer.class)); } }; }}; + //#test-within } - //#test-within @Test - public void demonstrateExpectMsgPF() { - new TestKit(system) {{ - testActor().tell(42); - //#test-expect-pf - final String out = expectMsgPF(Duration.parse("1 second"), "fourty-two", - new PurePartialFunction() { - public String apply(Object in, boolean isCheck) { - if (Integer.valueOf(42).equals(in)) { + public void demonstrateExpectMsg() { + //#test-expectmsg + new JavaTestKit(system) {{ + getRef().tell(42); + final String out = new ExpectMsg("match hint") { + // do not put code outside this method, will run afterwards + protected String match(Object in) { + if (in instanceof Integer) { return "match"; } else { throw noMatch(); } } - } - ); + }.get(); // this extracts the received message assertEquals("match", out); - //#test-expect-pf - testActor().tell("world"); - //#test-expect-anyof - final String any = expectMsgAnyOf(remaining(), JAPI.seq("hello", "world")); - //#test-expect-anyof - assertEquals("world", any); - testActor().tell("world"); - //#test-expect-anyclassof - @SuppressWarnings("unchecked") - final String anyClass = expectMsgAnyClassOf(remaining(), JAPI.>seq(String.class)); - //#test-expect-anyclassof - assertEquals("world", any); + }}; + //#test-expectmsg + } + + @Test + public void demonstrateReceiveWhile() { + //#test-receivewhile + new JavaTestKit(system) {{ + getRef().tell(42); + getRef().tell(43); + getRef().tell("hello"); + final String[] out = + new ReceiveWhile(String.class, duration("1 second")) { + // do not put code outside this method, will run afterwards + protected String match(Object in) { + if (in instanceof Integer) { + return in.toString(); + } else { + throw noMatch(); + } + } + }.get(); // this extracts the received messages + assertArrayEquals(new String[] {"42", "43"}, out); + expectMsgEquals("hello"); + }}; + //#test-receivewhile + new JavaTestKit(system) {{ + //#test-receivewhile-full + new ReceiveWhile( // type of array to be created must match ... + String.class, // ... this class which is needed to that end + duration("100 millis"), // maximum collect time + duration("50 millis"), // maximum time between messages + 12 // maximum number of messages to collect + ) { + //#match-elided + protected String match(Object in) { + throw noMatch(); + } + //#match-elided + }; + //#test-receivewhile-full }}; } + @Test + public void demonstrateAwaitCond() { + //#test-awaitCond + new JavaTestKit(system) {{ + getRef().tell(42); + new AwaitCond( + duration("1 second"), // maximum wait time + duration("100 millis") // interval at which to check the condition + ) { + // do not put code outside this method, will run afterwards + protected boolean cond() { + // typically used to wait for something to start up + return msgAvailable(); + } + }; + }}; + //#test-awaitCond + } + + @Test + @SuppressWarnings("unchecked") // due to generic varargs + public void demonstrateExpect() { + new JavaTestKit(system) {{ + getRef().tell("hello"); + getRef().tell("hello"); + getRef().tell("hello"); + getRef().tell("world"); + getRef().tell(42); + getRef().tell(42); + //#test-expect + final String hello = expectMsgEquals("hello"); + final Object any = expectMsgAnyOf("hello", "world"); + final Object[] all = expectMsgAllOf("hello", "world"); + final int i = expectMsgClass(Integer.class); + final Number j = expectMsgAnyClassOf(Integer.class, Long.class); + expectNoMsg(); + //#test-expect + assertEquals("hello", hello); + assertEquals("hello", any); + assertEquals(42, i); + assertEquals(42, j); + assertArrayEquals(new String[] {"hello", "world"}, all); + }}; + } + + @Test + public void demonstrateIgnoreMsg() { + //#test-ignoreMsg + new JavaTestKit(system) {{ + // ignore all Strings + new IgnoreMsg() { + protected boolean ignore(Object msg) { + return msg instanceof String; + } + }; + getRef().tell("hello"); + getRef().tell(42); + expectMsgEquals(42); + // remove message filter + ignoreNoMsg(); + getRef().tell("hello"); + expectMsgEquals("hello"); + }}; + //#test-ignoreMsg + } + + @Test + public void demonstrateDilated() { + //#duration-dilation + new JavaTestKit(system) {{ + final Duration original = duration("1 second"); + final Duration stretched = dilated(original); + assertTrue("dilated", stretched.gteq(original)); + }}; + //#duration-dilation + } + + @Test + public void demonstrateProbe() { + //#test-probe + // simple actor which just forwards messages + class Forwarder extends UntypedActor { + final ActorRef target; + public Forwarder(ActorRef target) { + this.target = target; + } + public void onReceive(Object msg) { + target.forward(msg, getContext()); + } + } + + new JavaTestKit(system) {{ + // create a test probe + final JavaTestKit probe = new JavaTestKit(system); + + // create a forwarder, injecting the probe’s testActor + final Props props = new Props(new UntypedActorFactory() { + private static final long serialVersionUID = 8927158735963950216L; + public UntypedActor create() { + return new Forwarder(probe.getRef()); + } + }); + final ActorRef forwarder = system.actorOf(props, "forwarder"); + + // verify correct forwarding + forwarder.tell(42, getRef()); + probe.expectMsgEquals(42); + assertEquals(getRef(), probe.getLastSender()); + }}; + //#test-probe + } + + @Test + public void demonstrateSpecialProbe() { + //#test-special-probe + new JavaTestKit(system) {{ + class MyProbe extends JavaTestKit { + public MyProbe() { + super(system); + } + public void assertHello() { + expectMsgEquals("hello"); + } + } + + final MyProbe probe = new MyProbe(); + probe.getRef().tell("hello"); + probe.assertHello(); + }}; + //#test-special-probe + } + + @Test + public void demonstrateReply() { + //#test-probe-reply + new JavaTestKit(system) {{ + final JavaTestKit probe = new JavaTestKit(system); + probe.getRef().tell("hello", getRef()); + probe.expectMsgEquals("hello"); + probe.reply("world"); + expectMsgEquals("world"); + assertEquals(probe.getRef(), getLastSender()); + }}; + //#test-probe-reply + } + + @Test + public void demonstrateForward() { + //#test-probe-forward + new JavaTestKit(system) {{ + final JavaTestKit probe = new JavaTestKit(system); + probe.getRef().tell("hello", getRef()); + probe.expectMsgEquals("hello"); + probe.forward(getRef()); + expectMsgEquals("hello"); + assertEquals(getRef(), getLastSender()); + }}; + //#test-probe-forward + } + + @Test + public void demonstrateWithinProbe() { + try { + //#test-within-probe + new JavaTestKit(system) {{ + final JavaTestKit probe = new JavaTestKit(system); + new Within(duration("1 second")) { + public void run() { + probe.expectMsgEquals("hello"); + } + }; + }}; + //#test-within-probe + } catch (AssertionError e) { + // expected to fail + } + } + + @Test + public void demonstrateAutoPilot() { + //#test-auto-pilot + new JavaTestKit(system) {{ + final JavaTestKit probe = new JavaTestKit(system); + // install auto-pilot + probe.setAutoPilot(new TestActor.AutoPilot() { + public AutoPilot run(ActorRef sender, Object msg) { + sender.tell(msg); + return noAutoPilot(); + } + }); + // first one is replied to directly ... + probe.getRef().tell("hello", getRef()); + expectMsgEquals("hello"); + // ... but then the auto-pilot switched itself off + probe.getRef().tell("world", getRef()); + expectNoMsg(); + }}; + //#test-auto-pilot + } + + // only compilation + public void demonstrateCTD() { + //#calling-thread-dispatcher + system.actorOf( + new Props(MyActor.class) + .withDispatcher(CallingThreadDispatcher.Id())); + //#calling-thread-dispatcher + } + + @Test + public void demonstrateEventFilter() { + //#test-event-filter + new JavaTestKit(system) {{ + assertEquals("demoSystem", system.name()); + final ActorRef victim = system.actorOf(Props.empty(), "victim"); + + final int result = new EventFilter(ActorKilledException.class) { + protected Integer run() { + victim.tell(Kill.getInstance()); + return 42; + } + }.from("akka://demoSystem/user/victim").occurrences(1).exec(); + + assertEquals(42, result); + }}; + //#test-event-filter + } + } diff --git a/akka-docs/java/code/docs/testkit/TestKitSampleTest.java b/akka-docs/java/code/docs/testkit/TestKitSampleTest.java new file mode 100644 index 0000000000..ba235fad15 --- /dev/null +++ b/akka-docs/java/code/docs/testkit/TestKitSampleTest.java @@ -0,0 +1,95 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ +package docs.testkit; + +//#fullsample +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import akka.actor.ActorRef; +import akka.actor.ActorSystem; +import akka.actor.Props; +import akka.actor.UntypedActor; +import akka.testkit.JavaTestKit; +import akka.util.Duration; + +public class TestKitSampleTest { + + public static class SomeActor extends UntypedActor { + ActorRef target = null; + + public void onReceive(Object msg) { + + if (msg.equals("hello")) { + getSender().tell("world"); + if (target != null) target.forward(msg, getContext()); + + } else if (msg instanceof ActorRef) { + target = (ActorRef) msg; + getSender().tell("done"); + } + } + } + + static ActorSystem system; + + @BeforeClass + public static void setup() { + system = ActorSystem.create(); + } + + @AfterClass + public static void teardown() { + system.shutdown(); + } + + @Test + public void testIt() { + /* + * Wrap the whole test procedure within a testkit constructor + * if you want to receive actor replies or use Within(), etc. + */ + new JavaTestKit(system) {{ + final Props props = new Props(SomeActor.class); + final ActorRef subject = system.actorOf(props); + + // can also use JavaTestKit “from the outside” + final JavaTestKit probe = new JavaTestKit(system); + // “inject” the probe by passing it to the test subject + // like a real resource would be passed in production + subject.tell(probe.getRef(), getRef()); + // await the correct response + expectMsgEquals(duration("1 second"), "done"); + + // the run() method needs to finish within 3 seconds + new Within(duration("3 seconds")) { + protected void run() { + + subject.tell("hello", getRef()); + + // This is a demo: would normally use expectMsgEquals(). + // Wait time is bounded by 3-second deadline above. + new AwaitCond() { + protected boolean cond() { + return probe.msgAvailable(); + } + }; + + // response must have been enqueued to us before probe + expectMsgEquals(Duration.Zero(), "world"); + // check that the probe we injected earlier got the msg + probe.expectMsgEquals(Duration.Zero(), "hello"); + Assert.assertEquals(getRef(), probe.getLastSender()); + + // Will wait for the rest of the 3 seconds + expectNoMsg(); + } + }; + }}; + } + +} +//#fullsample diff --git a/akka-docs/java/dispatchers.rst b/akka-docs/java/dispatchers.rst index 2723883e9c..023424e687 100644 --- a/akka-docs/java/dispatchers.rst +++ b/akka-docs/java/dispatchers.rst @@ -92,7 +92,7 @@ There are 4 different types of message dispatchers: * CallingThreadDispatcher - This dispatcher runs invocations on the current thread only. This dispatcher does not create any new threads, - but it can be used from different threads concurrently for the same actor. See :ref:`TestCallingThreadDispatcherRef` + but it can be used from different threads concurrently for the same actor. See :ref:`Java-CallingThreadDispatcher` for details and restrictions. - Sharability: Unlimited diff --git a/akka-docs/java/testing.rst b/akka-docs/java/testing.rst index 6aa31ff633..bab663b355 100644 --- a/akka-docs/java/testing.rst +++ b/akka-docs/java/testing.rst @@ -141,170 +141,152 @@ Feel free to experiment with the possibilities, and if you find useful patterns, don't hesitate to let the Akka forums know about them! Who knows, common operations might even be worked into nice DSLs. -Integration Testing with :class:`TestKit` -========================================= +Integration Testing with :class:`JavaTestKit` +============================================= When you are reasonably sure that your actor's business logic is correct, the -next step is verifying that it works correctly within its intended environment -(if the individual actors are simple enough, possibly because they use the -:mod:`FSM` module, this might also be the first step). The definition of the -environment depends of course very much on the problem at hand and the level at -which you intend to test, ranging for functional/integration tests to full -system tests. The minimal setup consists of the test procedure, which provides -the desired stimuli, the actor under test, and an actor receiving replies. -Bigger systems replace the actor under test with a network of actors, apply -stimuli at varying injection points and arrange results to be sent from -different emission points, but the basic principle stays the same in that a -single procedure drives the test. +next step is verifying that it works correctly within its intended environment. +The definition of the environment depends of course very much on the problem at +hand and the level at which you intend to test, ranging for +functional/integration tests to full system tests. The minimal setup consists +of the test procedure, which provides the desired stimuli, the actor under +test, and an actor receiving replies. Bigger systems replace the actor under +test with a network of actors, apply stimuli at varying injection points and +arrange results to be sent from different emission points, but the basic +principle stays the same in that a single procedure drives the test. -The :class:`TestKit` class contains a collection of tools which makes this +The :class:`JavaTestKit` class contains a collection of tools which makes this common task easy. -.. includecode:: code/docs/testkit/PlainWordTest.java#plain-spec +.. includecode:: code/docs/testkit/TestKitSampleTest.java#fullsample -The :class:`TestKit` contains an actor named :obj:`testActor` which is the +The :class:`JavaTestKit` contains an actor named :obj:`testActor` which is the entry point for messages to be examined with the various ``expectMsg...`` -assertions detailed below. When mixing in the trait ``ImplicitSender`` this -test actor is implicitly used as sender reference when dispatching messages -from the test procedure. The :obj:`testActor` may also be passed to -other actors as usual, usually subscribing it as notification listener. There -is a whole set of examination methods, e.g. receiving all consecutive messages -matching certain criteria, receiving a whole sequence of fixed messages or -classes, receiving nothing for some time, etc. +assertions detailed below. The test actor’s reference is obtained using the +:meth:`getRef()` method as demonstrated above. The :obj:`testActor` may also +be passed to other actors as usual, usually subscribing it as notification +listener. There is a whole set of examination methods, e.g. receiving all +consecutive messages matching certain criteria, receiving a whole sequence of +fixed messages or classes, receiving nothing for some time, etc. -The ActorSystem passed in to the constructor of TestKit is accessible via the -:meth:`system()` method. Remember to shut down the actor system after the test -is finished (also in case of failure) so that all actors—including the test -actor—are stopped. +The ActorSystem passed in to the constructor of JavaTestKit is accessible via the +:meth:`getSystem()` method. + +.. note:: + + Remember to shut down the actor system after the test is finished (also in + case of failure) so that all actors—including the test actor—are stopped. Built-In Assertions ------------------- -The above mentioned :meth:`expectMsg` is not the only method for formulating -assertions concerning received messages. Here is the full list: +The above mentioned :meth:`expectMsgEquals` is not the only method for +formulating assertions concerning received messages, the full set is this: - * :meth:` T expectMsg(Duration d, T msg): T` +.. includecode:: code/docs/testkit/TestKitDocTest.java#test-expect + +In these examples, the maximum durations you will find mentioned below are left +out, in which case they use the default value from configuration item +``akka.test.single-expect-default`` which itself defaults to 3 seconds (or they +obey the innermost enclosing :class:`Within` as detailed :ref:`below +`). The full signatures are: + + * :meth:`public  T expectMsgEquals(Duration max, T msg)` The given message object must be received within the specified time; the object will be returned. - * :meth:` T expectMsgPF(Duration d, PartialFunction pf)` + * :meth:`public Object expectMsgAnyOf(Duration max, Object... msg)` - Within the given time period, a message must be received and the given - partial function must be defined for that message; the result from applying - the partial function to the received message is returned. + An object must be received within the given time, and it must be equal + (compared with ``equals()``) to at least one of the passed reference + objects; the received object will be returned. - .. includecode:: code/docs/testkit/TestKitDocTest.java#test-expect-pf - - * :meth:` T expectMsgClass(Duration d, Class c)` - - An object which is an instance of the given :class:`Class` must be received - within the allotted time frame; the object will be returned. Note that this - does a conformance check; if you need the class to be equal, have a look at - :meth:`expectMsgAllClassOf` with a single given class argument. - - * :meth:` T expectMsgAnyOf(Duration d, Seq obj)` - - An object must be received within the given time, and it must be equal ( - compared with ``equals()``) to at least one of the passed reference objects; the - received object will be returned. - - .. includecode:: code/docs/testkit/TestKitDocTest.java#test-expect-anyof - - * :meth:` T expectMsgAnyClassOf(Duration d, Seq> classes)` - - An object must be received within the given time, and it must be an - instance of at least one of the supplied :class:`Class` objects; the - received object will be returned. - - .. includecode:: code/docs/testkit/TestKitDocTest.java#test-expect-anyclassof - - * :meth:`expectMsgAllOf[T](d: Duration, obj: T*): Seq[T]` + * :meth:`public Object[] expectMsgAllOf(Duration max, Object... msg)` A number of objects matching the size of the supplied object array must be received within the given time, and for each of the given objects there - must exist at least one among the received ones which equals (compared with - ``==``) it. The full sequence of received objects is returned. + must exist at least one among the received ones which equals it (compared + with ``equals()``). The full sequence of received objects is returned in + the order received. - * :meth:`expectMsgAllClassOf[T](d: Duration, c: Class[_ <: T]*): Seq[T]` + * :meth:`public  T expectMsgClass(Duration max, Class c)` - A number of objects matching the size of the supplied :class:`Class` array - must be received within the given time, and for each of the given classes - there must exist at least one among the received objects whose class equals - (compared with ``==``) it (this is *not* a conformance check). The full - sequence of received objects is returned. + An object which is an instance of the given :class:`Class` must be received + within the allotted time frame; the object will be returned. Note that this + does a conformance check, if you need the class to be equal you need to + verify that afterwards. - * :meth:`expectMsgAllConformingOf[T](d: Duration, c: Class[_ <: T]*): Seq[T]` + * :meth:`public  T expectMsgAnyClassOf(Duration max, Class... c)` - A number of objects matching the size of the supplied :class:`Class` array - must be received within the given time, and for each of the given classes - there must exist at least one among the received objects which is an - instance of this class. The full sequence of received objects is returned. + An object must be received within the given time, and it must be an + instance of at least one of the supplied :class:`Class` objects; the + received object will be returned. Note that this does a conformance check, + if you need the class to be equal you need to verify that afterwards. - * :meth:`expectNoMsg(d: Duration)` + .. note:: + + Because of a limitation in Java’s type system it may be necessary to add + ``@SuppressWarnings("unchecked")`` when using this method. + + * :meth:`public void expectNoMsg(Duration max)` No message must be received within the given time. This also fails if a message has been received before calling this method which has not been removed from the queue using one of the other methods. - * :meth:`receiveN(n: Int, d: Duration): Seq[AnyRef]` +For cases which require more refined conditions there are constructs which take +code blocks: - ``n`` messages must be received within the given time; the received - messages are returned. + * **ExpectMsg** - * :meth:`fishForMessage(max: Duration, hint: String)(pf: PartialFunction[Any, Boolean]): Any` + .. includecode:: code/docs/testkit/TestKitDocTest.java#test-expectmsg - Keep receiving messages as long as the time is not used up and the partial - function matches and returns ``false``. Returns the message received for - which it returned ``true`` or throws an exception, which will include the - provided hint for easier debugging. + The :meth:`match(Object in)` method will be evaluated once a message has + been received within the allotted time (which may be given as constructor + argument). If it throws ``noMatch()`` (where it is sufficient to call that + method; the ``throw`` keyword is only needed in cases where the compiler + would otherwise complain about wrong return types—Java is lacking Scala’s + notion of a type which signifies “will not ever return normally”), then the + expectation fails with an :class:`AssertionError`, otherwise the matched + and possibly transformed object is stored for retrieval using the + :meth:`get()` method. -In addition to message reception assertions there are also methods which help -with message flows: + * **ReceiveWhile** - * :meth:`receiveOne(d: Duration): AnyRef` + .. includecode:: code/docs/testkit/TestKitDocTest.java#test-receivewhile - Tries to receive one message for at most the given time interval and - returns ``null`` in case of failure. If the given Duration is zero, the - call is non-blocking (polling mode). + This construct works like ExpectMsg, but it continually collects messages + as long as they match the criteria, and it does not fail when a + non-matching one is encountered. Collecting messages also ends when the + time is up, when too much time passes between messages or when enough + messages have been received. - * :meth:`receiveWhile[T](max: Duration, idle: Duration, messages: Int)(pf: PartialFunction[Any, T]): Seq[T]` + .. includecode:: code/docs/testkit/TestKitDocTest.java#test-receivewhile-full + :exclude: match-elided - Collect messages as long as + The need to specify the ``String`` result type twice results from the need + to create a correctly typed array and Java’s inability to infer the class’s + type argument. - * they are matching the given partial function - * the given time interval is not used up - * the next message is received within the idle timeout - * the number of messages has not yet reached the maximum + * **AwaitCond** - All collected messages are returned. The maximum duration defaults to the - time remaining in the innermost enclosing :ref:`within ` - block and the idle duration defaults to infinity (thereby disabling the - idle timeout feature). The number of expected messages defaults to - ``Int.MaxValue``, which effectively disables this limit. + .. includecode:: code/docs/testkit/TestKitDocTest.java#test-awaitCond - * :meth:`awaitCond(p: => Boolean, max: Duration, interval: Duration)` + This general construct is not connected with the test kit’s message + reception, the embedded condition can compute the boolean result from + anything in scope. - Poll the given condition every :obj:`interval` until it returns ``true`` or - the :obj:`max` duration is used up. The interval defaults to 100 ms and the - maximum defaults to the time remaining in the innermost enclosing - :ref:`within ` block. +There are also cases where not all messages sent to the test kit are actually +relevant to the test, but removing them would mean altering the actors under +test. For this purpose it is possible to ignore certain messages: - * :meth:`ignoreMsg(pf: PartialFunction[AnyRef, Boolean])` + * **IgnoreMsg** - :meth:`ignoreNoMsg` + .. includecode:: code/docs/testkit/TestKitDocTest.java#test-ignoreMsg - The internal :obj:`testActor` contains a partial function for ignoring - messages: it will only enqueue messages which do not match the function or - for which the function returns ``false``. This function can be set and - reset using the methods given above; each invocation replaces the previous - function, they are not composed. - - This feature is useful e.g. when testing a logging system, where you want - to ignore regular messages and are only interested in your specific ones. - -Expecting Exceptions --------------------- +Expecting Log Messages +---------------------- Since an integration test does not allow to the internal processing of the participating actors, verifying expected exceptions cannot be done directly. @@ -313,9 +295,23 @@ handler with the :class:`TestEventListener` and using an :class:`EventFilter` allows assertions on log messages, including those which are generated by exceptions: -.. includecode:: code/docs/testkit/TestKitDocTest.java#event-filter +.. includecode:: code/docs/testkit/TestKitDocTest.java#test-event-filter -.. _TestKit.within: +If a number of occurrences is specific—as demonstrated above—then ``exec()`` +will block until that number of matching messages have been received or the +timeout configured in ``akka.test.filter-leeway`` is used up (time starts +counting after the ``run()`` method returns). In case of a timeout the test +fails. + +.. note:: + + Be sure to exchange the default event handler with the + :class:`TestEventListener` in your ``application.conf`` to enable this + function:: + + akka.event-handlers = [akka.testkit.TestEventListener] + +.. _JavaTestKit.within: Timing Assertions ----------------- @@ -327,17 +323,13 @@ the positive or negative result must be obtained. Lower time limits need to be checked external to the examination, which is facilitated by a new construct for managing time constraints: -.. code-block:: scala +.. includecode:: code/docs/testkit/TestKitDocTest.java#test-within - within([min, ]max) { - ... - } - -The block given to :meth:`within` must complete after a :ref:`Duration` which +The block in :meth:`Within.run()` must complete after a :ref:`Duration` which is between :obj:`min` and :obj:`max`, where the former defaults to zero. The deadline calculated by adding the :obj:`max` parameter to the block's start time is implicitly available within the block to all examination methods, if -you do not specify it, is is inherited from the innermost enclosing +you do not specify it, it is inherited from the innermost enclosing :meth:`within` block. It should be noted that if the last message-receiving assertion of the block is @@ -346,16 +338,10 @@ It should be noted that if the last message-receiving assertion of the block is latencies. This means that while individual contained assertions still use the maximum time bound, the overall block may take arbitrarily longer in this case. -.. includecode:: code/docs/testkit/TestKitDocTest.java#test-within - .. note:: All times are measured using ``System.nanoTime``, meaning that they describe - wall time, not CPU time. - -Ray Roestenburg has written a great article on using the TestKit: -``_. -His full example is also available :ref:`here `. + wall time, not CPU time or system time. Accounting for Slow Test Systems ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -371,33 +357,22 @@ in ``akka.testkit`` package object to add dilated function to :class:`Duration`. .. includecode:: code/docs/testkit/TestKitDocTest.java#duration-dilation -Resolving Conflicts with Implicit ActorRef ------------------------------------------- - -If you want the sender of messages inside your TestKit-based tests to be the ``testActor`` -simply mix in ``ÌmplicitSender`` into your test. - -.. includecode:: code/docs/testkit/PlainWordSpec.scala#implicit-sender - Using Multiple Probe Actors --------------------------- When the actors under test are supposed to send various messages to different destinations, it may be difficult distinguishing the message streams arriving -at the :obj:`testActor` when using the :class:`TestKit` as a mixin. Another -approach is to use it for creation of simple probe actors to be inserted in the -message flows. To make this more powerful and convenient, there is a concrete -implementation called :class:`TestProbe`. The functionality is best explained -using a small example: +at the :obj:`testActor` when using the :class:`JavaTestKit` as shown until now. +Another approach is to use it for creation of simple probe actors to be +inserted in the message flows. The functionality is best explained using a +small example: -.. includecode:: code/docs/testkit/TestKitDocTest.java - :include: imports-test-probe,my-double-echo,test-probe +.. includecode:: code/docs/testkit/TestKitDocTest.java#test-probe -Here a the system under test is simulated by :class:`MyDoubleEcho`, which is -supposed to mirror its input to two outputs. Attaching two test probes enables -verification of the (simplistic) behavior. Another example would be two actors -A and B which collaborate by A sending messages to B. In order to verify this -message flow, a :class:`TestProbe` could be inserted as target of A, using the +This simple test verifies an equally simple Forwarder actor by injecting a +probe as the forwarder’s target. Another example would be two actors A and B +which collaborate by A sending messages to B. In order to verify this message +flow, a :class:`TestProbe` could be inserted as target of A, using the forwarding capabilities or auto-pilot described below to include a real B in the test setup. @@ -407,33 +382,28 @@ more concise and clear: .. includecode:: code/docs/testkit/TestKitDocTest.java :include: test-special-probe -You have complete flexibility here in mixing and matching the :class:`TestKit` -facilities with your own checks and choosing an intuitive name for it. In real -life your code will probably be a bit more complicated than the example given -above; just use the power! +You have complete flexibility here in mixing and matching the +:class:`JavaTestKit` facilities with your own checks and choosing an intuitive +name for it. In real life your code will probably be a bit more complicated +than the example given above; just use the power! Replying to Messages Received by Probes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The probes keep track of the communications channel for replies, if possible, -so they can also reply: +The probe stores the sender of the last dequeued message (i.e. after its +``expectMsg*`` reception), which may be retrieved using the +:meth:`getLastSender()` method. This information can also implicitly be used +for having the probe reply to the last received message: .. includecode:: code/docs/testkit/TestKitDocTest.java#test-probe-reply Forwarding Messages Received by Probes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Given a destination actor ``dest`` which in the nominal actor network would -receive a message from actor ``source``. If you arrange for the message to be -sent to a :class:`TestProbe` ``probe`` instead, you can make assertions -concerning volume and timing of the message flow while still keeping the -network functioning: +The probe can also forward a received message (i.e. after its ``expectMsg*`` +reception), retaining the original sender: -.. includecode:: code/docs/testkit/TestKitDocTest.java - :include: test-probe-forward-actors,test-probe-forward - -The ``dest`` actor will receive the same message invocation as if no test probe -had intervened. +.. includecode:: code/docs/testkit/TestKitDocTest.java#test-probe-forward Auto-Pilot ^^^^^^^^^^ @@ -445,7 +415,7 @@ keep a test running and verify traces later you can also install an This code can be used to forward messages, e.g. in a chain ``A --> Probe --> B``, as long as a certain protocol is obeyed. -.. includecode:: ../../akka-testkit/src/test/scala/akka/testkit/TestProbeSpec.scala#autopilot +.. includecode:: code/docs/testkit/TestKitDocTest.java#test-auto-pilot The :meth:`run` method must return the auto-pilot for the next message, wrapped in an :class:`Option`; setting it to :obj:`None` terminates the auto-pilot. @@ -455,25 +425,15 @@ Caution about Timing Assertions The behavior of :meth:`within` blocks when using test probes might be perceived as counter-intuitive: you need to remember that the nicely scoped deadline as -described :ref:`above ` is local to each probe. Hence, probes +described :ref:`above ` is local to each probe. Hence, probes do not react to each other's deadlines or to the deadline set in an enclosing -:class:`TestKit` instance:: +:class:`JavaTestKit` instance: - class SomeTest extends TestKit(_system: ActorSystem) with ImplicitSender { +.. includecode:: code/docs/testkit/TestKitDocTest.java#test-within-probe - val probe = TestProbe() +Here, the ``expectMsgEquals`` call will use the default timeout. - within(100 millis) { - probe.expectMsg("hallo") // Will hang forever! - } - } - -This test will hang indefinitely, because the :meth:`expectMsg` call does not -see any deadline. Currently, the only option is to use ``probe.within`` in the -above code to make it work; later versions may include lexically scoped -deadlines using implicit arguments. - -.. _TestCallingThreadDispatcherRef: +.. _Java-CallingThreadDispatcher: CallingThreadDispatcher ======================= @@ -572,7 +532,7 @@ has to offer: exception stack traces - Exclusion of certain classes of dead-lock scenarios -.. _actor.logging: +.. _actor.logging-java: Tracing Actor Invocations ========================= @@ -588,24 +548,6 @@ options: This is always on; in contrast to the other logging mechanisms, this logs at ``ERROR`` level. -* *Logging of message invocations on certain actors* - - This is enabled by a setting in the :ref:`configuration` — namely - ``akka.actor.debug.receive`` — which enables the :meth:`loggable` - statement to be applied to an actor’s :meth:`receive` function: - -.. includecode:: code/docs/testkit/TestKitDocTest.java#logging-receive - -. - If the abovementioned setting is not given in the :ref:`configuration`, this method will - pass through the given :class:`Receive` function unmodified, meaning that - there is no runtime cost unless actually enabled. - - The logging feature is coupled to this specific local mark-up because - enabling it uniformly on all actors is not usually what you need, and it - would lead to endless loops if it were applied to :class:`EventHandler` - listeners. - * *Logging of special messages* Actors handle certain special messages automatically, e.g. :obj:`Kill`, @@ -626,74 +568,10 @@ full logging of actor activities using this configuration fragment:: loglevel = DEBUG actor { debug { - receive = on autoreceive = on lifecycle = on } } } -Different Testing Frameworks -============================ - -Akka’s own test suite is written using `ScalaTest`_, -which also shines through in documentation examples. However, the TestKit and -its facilities do not depend on that framework, you can essentially use -whichever suits your development style best. - -This section contains a collection of known gotchas with some other frameworks, -which is by no means exhaustive and does not imply endorsement or special -support. - -When you need it to be a trait ------------------------------- - -If for some reason it is a problem to inherit from :class:`TestKit` due to it -being a concrete class instead of a trait, there’s :class:`TestKitBase`: - -.. includecode:: code/docs/testkit/TestKitDocTest.java - :include: test-kit-base - :exclude: put-your-test-code-here - -The ``implicit lazy val system`` must be declared exactly like that (you can of -course pass arguments to the actor system factory as needed) because trait -:class:`TestKitBase` needs the system during its construction. - -.. warning:: - - Use of the trait is discouraged because of potential issues with binary - backwards compatibility in the future, use at own risk. - -Specs2 ------- - -Some `Specs2`_ users have contributed examples of how to work around some clashes which may arise: - -* Mixing TestKit into :class:`org.specs2.mutable.Specification` results in a - name clash involving the ``end`` method (which is a private variable in - TestKit and an abstract method in Specification); if mixing in TestKit first, - the code may compile but might then fail at runtime. The work-around—which is - actually beneficial also for the third point—is to apply the TestKit together - with :class:`org.specs2.specification.Scope`. -* The Specification traits provide a :class:`Duration` DSL which uses partly - the same method names as :class:`akka.util.Duration`, resulting in ambiguous - implicits if ``akka.util.duration._`` is imported. There are two work-arounds: - - * either use the Specification variant of Duration and supply an implicit - conversion to the Akka Duration. This conversion is not supplied with the - Akka distribution because that would mean that our JAR files would dependon - Specs2, which is not justified by this little feature. - - * or mix :class:`org.specs2.time.NoTimeConversions` into the Specification. - -* Specifications are by default executed concurrently, which requires some care - when writing the tests or alternatively the ``sequential`` keyword. - -You can use the following two examples as guidelines: - -.. includecode:: code/docs/testkit/Specs2DemoSpec.scala - -.. includecode:: code/docs/testkit/Specs2DemoAcceptance.scala - - diff --git a/akka-docs/scala/code/docs/testkit/TestkitDocSpec.scala b/akka-docs/scala/code/docs/testkit/TestkitDocSpec.scala index fca85cf85f..1e42b2e8ac 100644 --- a/akka-docs/scala/code/docs/testkit/TestkitDocSpec.scala +++ b/akka-docs/scala/code/docs/testkit/TestkitDocSpec.scala @@ -275,4 +275,15 @@ class TestkitDocSpec extends AkkaSpec with DefaultTimeout with ImplicitSender { //#test-kit-base } + "demonstrate within() nesting" in { + intercept[AssertionError] { + //#test-within-probe + val probe = TestProbe() + within(1 second) { + probe.expectMsg("hello") + } + //#test-within-probe + } + } + } diff --git a/akka-docs/scala/dispatchers.rst b/akka-docs/scala/dispatchers.rst index cea9ee6e0a..f4ff26b573 100644 --- a/akka-docs/scala/dispatchers.rst +++ b/akka-docs/scala/dispatchers.rst @@ -93,7 +93,7 @@ There are 4 different types of message dispatchers: * CallingThreadDispatcher - This dispatcher runs invocations on the current thread only. This dispatcher does not create any new threads, - but it can be used from different threads concurrently for the same actor. See :ref:`TestCallingThreadDispatcherRef` + but it can be used from different threads concurrently for the same actor. See :ref:`Scala-CallingThreadDispatcher` for details and restrictions. - Sharability: Unlimited diff --git a/akka-docs/scala/fsm.rst b/akka-docs/scala/fsm.rst index e47fdaa055..b8fac5a6e3 100644 --- a/akka-docs/scala/fsm.rst +++ b/akka-docs/scala/fsm.rst @@ -424,7 +424,7 @@ This FSM will log at DEBUG level: * all state transitions Life cycle changes and special messages can be logged as described for -:ref:`Actors `. +:ref:`Actors `. Rolling Event Log ----------------- diff --git a/akka-docs/scala/testing.rst b/akka-docs/scala/testing.rst index 7a6415492d..ba05207975 100644 --- a/akka-docs/scala/testing.rst +++ b/akka-docs/scala/testing.rst @@ -317,8 +317,8 @@ with message flows: This feature is useful e.g. when testing a logging system, where you want to ignore regular messages and are only interested in your specific ones. -Expecting Exceptions --------------------- +Expecting Log Messages +---------------------- Since an integration test does not allow to the internal processing of the participating actors, verifying expected exceptions cannot be done directly. @@ -329,6 +329,20 @@ exceptions: .. includecode:: code/docs/testkit/TestkitDocSpec.scala#event-filter +If a number of occurrences is specific—as demonstrated above—then ``intercept`` +will block until that number of matching messages have been received or the +timeout configured in ``akka.test.filter-leeway`` is used up (time starts +counting after the passed-in block of code returns). In case of a timeout the +test fails. + +.. note:: + + Be sure to exchange the default event handler with the + :class:`TestEventListener` in your ``application.conf`` to enable this + function:: + + akka.event-handlers = [akka.testkit.TestEventListener] + .. _TestKit.within: Timing Assertions @@ -351,7 +365,7 @@ The block given to :meth:`within` must complete after a :ref:`Duration` which is between :obj:`min` and :obj:`max`, where the former defaults to zero. The deadline calculated by adding the :obj:`max` parameter to the block's start time is implicitly available within the block to all examination methods, if -you do not specify it, is is inherited from the innermost enclosing +you do not specify it, it is inherited from the innermost enclosing :meth:`within` block. It should be noted that if the last message-receiving assertion of the block is @@ -461,8 +475,9 @@ B``, as long as a certain protocol is obeyed. .. includecode:: ../../akka-testkit/src/test/scala/akka/testkit/TestProbeSpec.scala#autopilot -The :meth:`run` method must return the auto-pilot for the next message, wrapped -in an :class:`Option`; setting it to :obj:`None` terminates the auto-pilot. +The :meth:`run` method must return the auto-pilot for the next message, which +may be :class:`KeepRunning` to retain the current one or :class:`NoAutoPilot` +to switch it off. Caution about Timing Assertions ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -471,23 +486,13 @@ The behavior of :meth:`within` blocks when using test probes might be perceived as counter-intuitive: you need to remember that the nicely scoped deadline as described :ref:`above ` is local to each probe. Hence, probes do not react to each other's deadlines or to the deadline set in an enclosing -:class:`TestKit` instance:: +:class:`TestKit` instance: - class SomeTest extends TestKit(_system: ActorSystem) with ImplicitSender { +.. includecode:: code/docs/testkit/TestkitDocSpec.scala#test-within-probe - val probe = TestProbe() +Here, the ``expectMsg`` call will use the default timeout. - within(100 millis) { - probe.expectMsg("hallo") // Will hang forever! - } - } - -This test will hang indefinitely, because the :meth:`expectMsg` call does not -see any deadline. Currently, the only option is to use ``probe.within`` in the -above code to make it work; later versions may include lexically scoped -deadlines using implicit arguments. - -.. _TestCallingThreadDispatcherRef: +.. _Scala-CallingThreadDispatcher: CallingThreadDispatcher ======================= @@ -586,7 +591,7 @@ has to offer: exception stack traces - Exclusion of certain classes of dead-lock scenarios -.. _actor.logging: +.. _actor.logging-scala: Tracing Actor Invocations ========================= diff --git a/akka-testkit/src/main/java/akka/testkit/JavaTestKit.java b/akka-testkit/src/main/java/akka/testkit/JavaTestKit.java new file mode 100644 index 0000000000..08846a4ad4 --- /dev/null +++ b/akka-testkit/src/main/java/akka/testkit/JavaTestKit.java @@ -0,0 +1,329 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ +package akka.testkit; + +import scala.runtime.AbstractFunction0; +import akka.actor.ActorRef; +import akka.actor.ActorSystem; +import akka.event.Logging; +import akka.event.Logging.LogEvent; +import akka.japi.PurePartialFunction; +import akka.japi.CachingPartialFunction; +import akka.japi.Util; +import akka.util.Duration; + +/** + * Java API for the TestProbe. Proper JavaDocs to come once JavaDoccing is implemented. + */ +public class JavaTestKit { + private final TestProbe p; + + public JavaTestKit(ActorSystem system) { + p = new TestProbe(system); + } + + public ActorRef getRef() { + return p.ref(); + } + + public ActorSystem getSystem() { + return p.system(); + } + + static public Duration duration(String s) { + return Duration.parse(s); + } + + public Duration dilated(Duration d) { + return d.mul(TestKitExtension.get(p.system()).TestTimeFactor()); + } + + public boolean msgAvailable() { + return p.msgAvailable(); + } + + public ActorRef getLastSender() { + return p.lastMessage().sender(); + } + + public void send(ActorRef actor, Object msg) { + actor.tell(msg, p.ref()); + } + + public void forward(ActorRef actor) { + actor.tell(p.lastMessage().msg(), p.lastMessage().sender()); + } + + public void reply(Object msg) { + p.lastMessage().sender().tell(msg, p.ref()); + } + + public Duration getRemainingTime() { + return p.remaining(); + } + + public Duration getRemainingTimeOr(Duration def) { + return p.remainingOr(def); + } + + public ActorRef watch(ActorRef ref) { + return p.watch(ref); + } + + public ActorRef unwatch(ActorRef ref) { + return p.unwatch(ref); + } + + public abstract class IgnoreMsg { + abstract protected boolean ignore(Object msg); + + public IgnoreMsg() { + p.ignoreMsg(new PurePartialFunction() { + public Boolean apply(Object in, boolean isCheck) { + return ignore(in); + } + }); + } + } + + public void ignoreNoMsg() { + p.ignoreNoMsg(); + } + + public void setAutoPilot(TestActor.AutoPilot pilot) { + p.setAutoPilot(pilot); + } + + public abstract class Within { + protected abstract void run(); + + public Within(Duration max) { + p.within(max, new AbstractFunction0() { + public Object apply() { + run(); + return null; + } + }); + } + + public Within(Duration min, Duration max) { + p.within(min, max, new AbstractFunction0() { + public Object apply() { + run(); + return null; + } + }); + } + } + + public abstract class AwaitCond { + protected abstract boolean cond(); + + public AwaitCond() { + this(Duration.Undefined(), p.awaitCond$default$3()); + } + + public AwaitCond(Duration max) { + this(max, p.awaitCond$default$3()); + } + + public AwaitCond(Duration max, Duration interval) { + p.awaitCond(new AbstractFunction0() { + public Object apply() { + return cond(); + } + }, max, interval); + } + } + + public abstract class ExpectMsg { + private final T result; + + public ExpectMsg(String hint) { + this(Duration.Undefined(), hint); + } + + public ExpectMsg(Duration max, String hint) { + final Object received = p.receiveOne(max); + try { + result = match(received); + } catch (PurePartialFunction.NoMatchException ex) { + throw new AssertionError("while expecting '" + hint + + "' received unexpected: " + received); + } + } + + abstract protected T match(Object msg); + + protected RuntimeException noMatch() { + throw PurePartialFunction.noMatch(); + } + + public T get() { + return result; + } + } + + public T expectMsgEquals(T msg) { + return p.expectMsg(msg); + } + + public T expectMsgEquals(Duration max, T msg) { + return p.expectMsg(max, msg); + } + + public T expectMsgClass(Class clazz) { + return p.expectMsgClass(clazz); + } + + public T expectMsgClass(Duration max, Class clazz) { + return p.expectMsgClass(max, clazz); + } + + public Object expectMsgAnyOf(Object... msgs) { + return p.expectMsgAnyOf(Util.arrayToSeq(msgs)); + } + + public Object expectMsgAnyOf(Duration max, Object... msgs) { + return p.expectMsgAnyOf(max, Util.arrayToSeq(msgs)); + } + + public Object[] expectMsgAllOf(Object... msgs) { + return (Object[]) p.expectMsgAllOf(Util.arrayToSeq(msgs)).toArray( + Util.manifest(Object.class)); + } + + public Object[] expectMsgAllOf(Duration max, Object... msgs) { + return (Object[]) p.expectMsgAllOf(max, Util.arrayToSeq(msgs)).toArray( + Util.manifest(Object.class)); + } + + @SuppressWarnings("unchecked") + public T expectMsgAnyClassOf(Class... classes) { + final Object result = p.expectMsgAnyClassOf(Util.arrayToSeq(classes)); + return (T) result; + } + + public Object expectMsgAnyClassOf(Duration max, Class... classes) { + return p.expectMsgAnyClassOf(max, Util.arrayToSeq(classes)); + } + + public void expectNoMsg() { + p.expectNoMsg(); + } + + public void expectNoMsg(Duration max) { + p.expectNoMsg(max); + } + + public abstract class ReceiveWhile { + abstract protected T match(Object msg); + + private Object results; + + public ReceiveWhile(Class clazz) { + this(clazz, Duration.Undefined()); + } + + public ReceiveWhile(Class clazz, Duration max) { + this(clazz, max, Duration.Inf(), Integer.MAX_VALUE); + } + + public ReceiveWhile(Class clazz, Duration max, int messages) { + this(clazz, max, Duration.Inf(), messages); + } + + @SuppressWarnings("unchecked") + public ReceiveWhile(Class clazz, Duration max, Duration idle, int messages) { + results = p.receiveWhile(max, idle, messages, + new CachingPartialFunction() { + public T match(Object msg) { + return ReceiveWhile.this.match(msg); + } + }).toArray(Util.manifest(clazz)); + } + + protected RuntimeException noMatch() { + throw PurePartialFunction.noMatch(); + } + + @SuppressWarnings("unchecked") + public T[] get() { + return (T[]) results; + } + } + + public abstract class EventFilter { + abstract protected T run(); + + private final Class clazz; + + private String source = null; + private String message = null; + private boolean pattern = false; + private boolean complete = false; + private int occurrences = Integer.MAX_VALUE; + private Class exceptionType = null; + + @SuppressWarnings("unchecked") + public EventFilter(Class clazz) { + if (Throwable.class.isAssignableFrom(clazz)) { + this.clazz = Logging.Error.class; + exceptionType = (Class) clazz; + } else if (Logging.LogEvent.class.isAssignableFrom(clazz)) { + this.clazz = (Class) clazz; + } else throw new IllegalArgumentException("supplied class must either be LogEvent or Throwable"); + } + + public T exec() { + akka.testkit.EventFilter filter; + if (clazz == Logging.Error.class) { + if (exceptionType == null) exceptionType = Logging.noCause().getClass(); + filter = new ErrorFilter(exceptionType, source, message, pattern, complete, occurrences); + } else if (clazz == Logging.Warning.class) { + filter = new WarningFilter(source, message, pattern, complete, occurrences); + } else if (clazz == Logging.Info.class) { + filter = new InfoFilter(source, message, pattern, complete, occurrences); + } else if (clazz == Logging.Debug.class) { + filter = new DebugFilter(source, message, pattern, complete, occurrences); + } else throw new IllegalArgumentException("unknown LogLevel " + clazz); + return filter.intercept(new AbstractFunction0() { + public T apply() { + return run(); + } + }, p.system()); + } + + public EventFilter message(String msg) { + message = msg; + pattern = false; + complete = true; + return this; + } + + public EventFilter startsWith(String msg) { + message = msg; + pattern = false; + complete = false; + return this; + } + + public EventFilter matches(String regex) { + message = regex; + pattern = true; + return this; + } + + public EventFilter from(String source) { + this.source = source; + return this; + } + + public EventFilter occurrences(int number) { + occurrences = number; + return this; + } + } + +} diff --git a/akka-testkit/src/main/scala/akka/testkit/TestActorRef.scala b/akka-testkit/src/main/scala/akka/testkit/TestActorRef.scala index b65d836f22..6c33f5f60b 100644 --- a/akka-testkit/src/main/scala/akka/testkit/TestActorRef.scala +++ b/akka-testkit/src/main/scala/akka/testkit/TestActorRef.scala @@ -136,5 +136,5 @@ object TestActorRef { /** * Java API */ - def create(props: Props, name: String, system: ActorSystem) = apply(props, name)(system) + def create[T <: Actor](system: ActorSystem, props: Props, name: String): TestActorRef[T] = apply(props, name)(system) } diff --git a/akka-testkit/src/main/scala/akka/testkit/TestKit.scala b/akka-testkit/src/main/scala/akka/testkit/TestKit.scala index c292cc238a..ed9d6c5415 100644 --- a/akka-testkit/src/main/scala/akka/testkit/TestKit.scala +++ b/akka-testkit/src/main/scala/akka/testkit/TestKit.scala @@ -14,12 +14,23 @@ import akka.actor.ActorSystem import akka.util.Timeout import akka.util.BoxedType import scala.annotation.varargs +import akka.japi.PurePartialFunction object TestActor { type Ignore = Option[PartialFunction[AnyRef, Boolean]] - trait AutoPilot { - def run(sender: ActorRef, msg: Any): Option[AutoPilot] + abstract class AutoPilot { + def run(sender: ActorRef, msg: Any): AutoPilot + def noAutoPilot: AutoPilot = NoAutoPilot + def keepRunning: AutoPilot = KeepRunning + } + + case object NoAutoPilot extends AutoPilot { + def run(sender: ActorRef, msg: Any): AutoPilot = this + } + + case object KeepRunning extends AutoPilot { + def run(sender: ActorRef, msg: Any): AutoPilot = sys.error("must not call") } case class SetIgnore(i: Ignore) @@ -43,15 +54,18 @@ class TestActor(queue: BlockingDeque[TestActor.Message]) extends Actor { var ignore: Ignore = None - var autopilot: Option[AutoPilot] = None + var autopilot: AutoPilot = NoAutoPilot def receive = { case SetIgnore(ign) ⇒ ignore = ign case x @ Watch(ref) ⇒ context.watch(ref); queue.offerLast(RealMessage(x, self)) case x @ UnWatch(ref) ⇒ context.unwatch(ref); queue.offerLast(RealMessage(x, self)) - case SetAutoPilot(pilot) ⇒ autopilot = Some(pilot) + case SetAutoPilot(pilot) ⇒ autopilot = pilot case x: AnyRef ⇒ - autopilot = autopilot.flatMap(_.run(sender, x)) + autopilot = autopilot.run(sender, x) match { + case KeepRunning ⇒ autopilot + case other ⇒ other + } val observe = ignore map (ignoreFunc ⇒ if (ignoreFunc isDefinedAt x) !ignoreFunc(x) else true) getOrElse true if (observe) queue.offerLast(RealMessage(x, sender)) } @@ -126,20 +140,20 @@ trait TestKitBase { * Have the testActor watch someone (i.e. `context.watch(...)`). Waits until * the Watch message is received back using expectMsg. */ - def watch(ref: ActorRef) { + def watch(ref: ActorRef): ActorRef = { val msg = TestActor.Watch(ref) testActor ! msg - expectMsg(msg) + expectMsg(msg).ref } /** * Have the testActor stop watching someone (i.e. `context.unwatch(...)`). Waits until * the Watch message is received back using expectMsg. */ - def unwatch(ref: ActorRef) { + def unwatch(ref: ActorRef): ActorRef = { val msg = TestActor.UnWatch(ref) testActor ! msg - expectMsg(msg) + expectMsg(msg).ref } /** @@ -242,22 +256,6 @@ trait TestKitBase { */ def within[T](max: Duration)(f: ⇒ T): T = within(0 seconds, max)(f) - /** - * Java API for within(): - * - * {{{ - * new Within(Duration.parse("3 seconds")) { - * public void run() { - * // your test code here - * } - * } - * }}} - */ - abstract class Within(max: Duration) { - def run(): Unit - within(max)(run()) - } - /** * Same as `expectMsg(remaining, obj)`, but correctly treating the timeFactor. */ diff --git a/akka-testkit/src/test/java/akka/testkit/TestActorRefJavaCompile.java b/akka-testkit/src/test/java/akka/testkit/TestActorRefJavaCompile.java index 5c13557854..ecf2cd6e51 100644 --- a/akka-testkit/src/test/java/akka/testkit/TestActorRefJavaCompile.java +++ b/akka-testkit/src/test/java/akka/testkit/TestActorRefJavaCompile.java @@ -4,13 +4,14 @@ package akka.testkit; -import org.junit.Test; +import akka.actor.Actor; import akka.actor.Props; public class TestActorRefJavaCompile { public void shouldBeAbleToCompileWhenUsingApply() { //Just a dummy call to make sure it compiles - TestActorRef ref = TestActorRef.apply(new Props(), null); + TestActorRef ref = TestActorRef.apply(new Props(), null); + ref.toString(); } } \ No newline at end of file diff --git a/akka-testkit/src/test/scala/akka/testkit/TestProbeSpec.scala b/akka-testkit/src/test/scala/akka/testkit/TestProbeSpec.scala index 14fbee8bc1..6e764c96dc 100644 --- a/akka-testkit/src/test/scala/akka/testkit/TestProbeSpec.scala +++ b/akka-testkit/src/test/scala/akka/testkit/TestProbeSpec.scala @@ -44,10 +44,10 @@ class TestProbeSpec extends AkkaSpec with DefaultTimeout { //#autopilot val probe = TestProbe() probe.setAutoPilot(new TestActor.AutoPilot { - def run(sender: ActorRef, msg: Any): Option[TestActor.AutoPilot] = + def run(sender: ActorRef, msg: Any): TestActor.AutoPilot = msg match { - case "stop" ⇒ None - case x ⇒ testActor.tell(x, sender); Some(this) + case "stop" ⇒ TestActor.NoAutoPilot + case x ⇒ testActor.tell(x, sender); TestActor.KeepRunning } }) //#autopilot