From 3d40a0f5294d1212f800bcf3fa1bd7d98dabd059 Mon Sep 17 00:00:00 2001 From: Roland Date: Sun, 5 Jun 2011 10:45:27 +0200 Subject: [PATCH] add TestFSMRefSpec and make TestFSMRef better accessible - add address argument to TestFSMRef factory - set address of TestKit.testActor to "testActor#" with monotonically increasing number # --- .../scala/akka/actor/actor/FSMActorSpec.scala | 4 +- .../src/main/scala/akka/actor/FSM.scala | 82 ++++++++++--------- .../main/scala/akka/testkit/TestFSMRef.scala | 24 ++++-- .../src/main/scala/akka/testkit/TestKit.scala | 9 +- .../scala/akka/testkit/TestFSMRefSpec.scala | 62 ++++++++++++++ 5 files changed, 132 insertions(+), 49 deletions(-) create mode 100644 akka-testkit/src/test/scala/akka/testkit/TestFSMRefSpec.scala diff --git a/akka-actor-tests/src/test/scala/akka/actor/actor/FSMActorSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/actor/FSMActorSpec.scala index 51f1ebf6ba..a16ed6902b 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/actor/FSMActorSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/actor/FSMActorSpec.scala @@ -230,13 +230,13 @@ class FSMActorSpec extends WordSpec with MustMatchers with TestKit with BeforeAn EventHandler.level = EventHandler.DebugLevel fsmref ! "go" expectMsgPF(1 second) { - case EventHandler.Debug(`fsm`, s: String) if s.startsWith("processing Event(go,null) from Actor[") ⇒ true + case EventHandler.Debug(`fsm`, s: String) if s.startsWith("processing Event(go,null) from Actor[testActor") ⇒ true } expectMsg(1 second, EventHandler.Debug(fsm, "setting timer 't'/1500 milliseconds: Shutdown")) expectMsg(1 second, EventHandler.Debug(fsm, "transition 1 -> 2")) fsmref ! "stop" expectMsgPF(1 second) { - case EventHandler.Debug(`fsm`, s: String) if s.startsWith("processing Event(stop,null) from Actor[") ⇒ true + case EventHandler.Debug(`fsm`, s: String) if s.startsWith("processing Event(stop,null) from Actor[testActor") ⇒ true } expectMsgAllOf(1 second, EventHandler.Debug(fsm, "canceling timer 't'"), Normal) expectNoMsg(1 second) diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index be4f255aa4..d91e843ea5 100644 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -65,6 +65,40 @@ object FSM { implicit def d2od(d: Duration): Option[Duration] = Some(d) val debugEvent = config.getBool("akka.actor.debug.fsm", false) + + case class State[S, D](stateName: S, stateData: D, timeout: Option[Duration] = None, stopReason: Option[Reason] = None, replies: List[Any] = Nil) { + + /** + * Modify state transition descriptor to include a state timeout for the + * next state. This timeout overrides any default timeout set for the next + * state. + */ + def forMax(timeout: Duration): State[S, D] = { + copy(timeout = Some(timeout)) + } + + /** + * Send reply to sender of the current message, if available. + * + * @return this state transition descriptor + */ + def replying(replyValue: Any): State[S, D] = { + copy(replies = replyValue :: replies) + } + + /** + * Modify state transition descriptor with new state data. The data will be + * set when transitioning to the new state. + */ + def using(nextStateDate: D): State[S, D] = { + copy(stateData = nextStateDate) + } + + private[akka] def withStopReason(reason: Reason): State[S, D] = { + copy(stopReason = Some(reason)) + } + } + } /** @@ -151,6 +185,7 @@ trait FSM[S, D] extends ListenerManagement { import FSM._ + type State = FSM.State[S, D] type StateFunction = scala.PartialFunction[Event, State] type Timeout = Option[Duration] type TransitionHandler = PartialFunction[(S, S), Unit] @@ -185,7 +220,7 @@ trait FSM[S, D] extends ListenerManagement { protected final def startWith(stateName: S, stateData: D, timeout: Timeout = None) = { - currentState = State(stateName, stateData, timeout) + currentState = FSM.State(stateName, stateData, timeout) } /** @@ -196,7 +231,7 @@ trait FSM[S, D] extends ListenerManagement { * @return state transition descriptor */ protected final def goto(nextStateName: S): State = { - State(nextStateName, currentState.stateData) + FSM.State(nextStateName, currentState.stateData) } /** @@ -464,7 +499,10 @@ trait FSM[S, D] extends ListenerManagement { private[akka] def applyState(nextState: State): Unit = { nextState.stopReason match { case None ⇒ makeTransition(nextState) - case _ ⇒ terminate(nextState); self.stop() + case _ ⇒ + nextState.replies.reverse foreach (self reply _) + terminate(nextState) + self.stop() } } @@ -472,6 +510,7 @@ trait FSM[S, D] extends ListenerManagement { if (!stateFunctions.contains(nextState.stateName)) { terminate(stay withStopReason Failure("Next state %s does not exist".format(nextState.stateName))) } else { + nextState.replies.reverse foreach (self reply _) if (currentState.stateName != nextState.stateName) { handleTransition(currentState.stateName, nextState.stateName) notifyListeners(Transition(self, currentState.stateName, nextState.stateName)) @@ -509,43 +548,6 @@ trait FSM[S, D] extends ListenerManagement { def unapply[D](e: Event): Option[Any] = Some(e.event) } - case class State(stateName: S, stateData: D, timeout: Timeout = None) { - - /** - * Modify state transition descriptor to include a state timeout for the - * next state. This timeout overrides any default timeout set for the next - * state. - */ - def forMax(timeout: Duration): State = { - copy(timeout = Some(timeout)) - } - - /** - * Send reply to sender of the current message, if available. - * - * @return this state transition descriptor - */ - def replying(replyValue: Any): State = { - self.channel safe_! replyValue - this - } - - /** - * Modify state transition descriptor with new state data. The data will be - * set when transitioning to the new state. - */ - def using(nextStateDate: D): State = { - copy(stateData = nextStateDate) - } - - private[akka] var stopReason: Option[Reason] = None - - private[akka] def withStopReason(reason: Reason): State = { - stopReason = Some(reason) - this - } - } - case class StopEvent[S, D](reason: Reason, currentState: S, stateData: D) } diff --git a/akka-testkit/src/main/scala/akka/testkit/TestFSMRef.scala b/akka-testkit/src/main/scala/akka/testkit/TestFSMRef.scala index 1abfdc947f..7fabcfbe8b 100644 --- a/akka-testkit/src/main/scala/akka/testkit/TestFSMRef.scala +++ b/akka-testkit/src/main/scala/akka/testkit/TestFSMRef.scala @@ -5,7 +5,9 @@ package akka.testkit import akka.actor._ -import akka.util.Duration +import akka.util._ + +import com.eaio.uuid.UUID /** * This is a specialised form of the TestActorRef with support for querying and @@ -32,7 +34,7 @@ import akka.util.Duration * @author Roland Kuhn * @since 1.2 */ -class TestFSMRef[S, D, T <: Actor with FSM[S, D]](factory: () => T) extends TestActorRef(factory) { +class TestFSMRef[S, D, T <: Actor](factory: () ⇒ T, address: String)(implicit ev: T <:< FSM[S, D]) extends TestActorRef(factory, address) { private def fsm = underlyingActor @@ -52,9 +54,8 @@ class TestFSMRef[S, D, T <: Actor with FSM[S, D]](factory: () => T) extends Test * corresponding transition initiated from within the FSM, including timeout * and stop handling. */ - def setState(stateName: S = fsm.stateName, stateData: D = fsm.stateData, timeout: Option[Duration] = None) { - val f = fsm // needed to make the following type-check - f.applyState(f.State(stateName, stateData, timeout)) + def setState(stateName: S = fsm.stateName, stateData: D = fsm.stateData, timeout: Option[Duration] = None, stopReason: Option[FSM.Reason] = None) { + fsm.applyState(FSM.State(stateName, stateData, timeout, stopReason)) } /** @@ -74,4 +75,17 @@ class TestFSMRef[S, D, T <: Actor with FSM[S, D]](factory: () => T) extends Test */ def timerActive_?(name: String) = fsm.timerActive_?(name) + override def start(): this.type = { + super.start() + this + } + +} + +object TestFSMRef { + + def apply[S, D, T <: Actor](factory: ⇒ T)(implicit ev: T <:< FSM[S, D]): TestFSMRef[S, D, T] = new TestFSMRef(() ⇒ factory, new UUID().toString) + + def apply[S, D, T <: Actor](factory: ⇒ T, address: String)(implicit ev: T <:< FSM[S, D]): TestFSMRef[S, D, T] = new TestFSMRef(() ⇒ factory, address) + } diff --git a/akka-testkit/src/main/scala/akka/testkit/TestKit.scala b/akka-testkit/src/main/scala/akka/testkit/TestKit.scala index 6bf78c5115..bb9f69168b 100644 --- a/akka-testkit/src/main/scala/akka/testkit/TestKit.scala +++ b/akka-testkit/src/main/scala/akka/testkit/TestKit.scala @@ -8,7 +8,8 @@ import Actor._ import akka.util.Duration import akka.util.duration._ -import java.util.concurrent.{ BlockingDeque, LinkedBlockingDeque, TimeUnit } +import java.util.concurrent.{ BlockingDeque, LinkedBlockingDeque, TimeUnit, atomic } +import atomic.AtomicInteger import scala.annotation.tailrec @@ -99,7 +100,7 @@ trait TestKitLight { * ActorRef of the test actor. Access is provided to enable e.g. * registration as message target. */ - val testActor = actorOf(new TestActor(queue)).start() + val testActor = actorOf(new TestActor(queue), "testActor" + TestKit.testActorId.incrementAndGet()).start() /** * Implicit sender reference so that replies are possible for messages sent @@ -555,6 +556,10 @@ trait TestKitLight { private def format(u: TimeUnit, d: Duration) = "%.3f %s".format(d.toUnit(u), u.toString.toLowerCase) } +object TestKit { + private[testkit] val testActorId = new AtomicInteger(0) +} + trait TestKit extends TestKitLight { implicit val self = testActor } diff --git a/akka-testkit/src/test/scala/akka/testkit/TestFSMRefSpec.scala b/akka-testkit/src/test/scala/akka/testkit/TestFSMRefSpec.scala new file mode 100644 index 0000000000..67fdadc529 --- /dev/null +++ b/akka-testkit/src/test/scala/akka/testkit/TestFSMRefSpec.scala @@ -0,0 +1,62 @@ +/** + * Copyright (C) 2009-2011 Scalable Solutions AB + */ + +package akka.testkit + +import org.scalatest.matchers.MustMatchers +import org.scalatest.{ BeforeAndAfterEach, WordSpec } +import akka.actor._ +import akka.util.duration._ + +class TestFSMRefSpec extends WordSpec with MustMatchers with TestKit { + + import FSM._ + + "A TestFSMRef" must { + + "allow access to state data" in { + val fsm = TestFSMRef(new Actor with FSM[Int, String] { + startWith(1, "") + when(1) { + case Ev("go") ⇒ goto(2) using "go" + case Ev(StateTimeout) ⇒ goto(2) using "timeout" + } + when(2) { + case Ev("back") ⇒ goto(1) using "back" + } + }).start() + fsm.stateName must be(1) + fsm.stateData must be("") + fsm ! "go" + fsm.stateName must be(2) + fsm.stateData must be("go") + fsm.setState(stateName = 1) + fsm.stateName must be(1) + fsm.stateData must be("go") + fsm.setState(stateData = "buh") + fsm.stateName must be(1) + fsm.stateData must be("buh") + fsm.setState(timeout = 100 millis) + within(80 millis, 500 millis) { + awaitCond(fsm.stateName == 2 && fsm.stateData == "timeout") + } + } + + "allow access to timers" in { + val fsm = TestFSMRef(new Actor with FSM[Int, Null] { + startWith(1, null) + when(1) { + case x ⇒ stay + } + }) + fsm.timerActive_?("test") must be(false) + fsm.setTimer("test", 12, 10 millis, true) + fsm.timerActive_?("test") must be(true) + fsm.cancelTimer("test") + fsm.timerActive_?("test") must be(false) + } + + } + +}