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:
Roland 2011-06-05 10:45:27 +02:00
parent b1533cb3d8
commit 3d40a0f529
5 changed files with 132 additions and 49 deletions

View file

@ -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)

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}
}
}