add TestFSMRefSpec and make TestFSMRef better accessible
- add address argument to TestFSMRef factory - set address of TestKit.testActor to "testActor#" with monotonically increasing number #
This commit is contained in:
parent
b1533cb3d8
commit
3d40a0f529
5 changed files with 132 additions and 49 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2011 Scalable Solutions AB <http://scalablesolutions.se>
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue