From 2a979fe29601e16efe86d78e6f2370963767581f Mon Sep 17 00:00:00 2001 From: Christopher Batey Date: Mon, 4 Jun 2018 17:01:09 +0100 Subject: [PATCH] Expose Effect classes for BehaviorTestKit Java DSL, #25070, #24781 * Expose Effect classes for BehaviorTestKit Java DSL - Share effect classes between java and scala dsl - make spawn adapter private - add message adapter effect - add tests for java behavior test kit * Mirror Effect factory in java and scala dsl --- .../akka/actor/testkit/typed/Effect.scala | 181 ++++++++++- .../typed/internal/BehaviorTestKitImpl.scala | 2 +- .../internal/EffectfulActorContext.scala | 21 +- .../actor/testkit/typed/javadsl/Effects.scala | 46 +-- .../testkit/typed/scaladsl/Effects.scala | 135 ++------ .../typed/javadsl/ActorTestKitTest.java | 3 - .../typed/javadsl/BehaviorTestKitTest.java | 298 ++++++++++++++++++ .../typed/scaladsl/BehaviorTestKitSpec.scala | 35 +- .../scaladsl/SyncTestingExampleSpec.scala | 2 +- 9 files changed, 571 insertions(+), 152 deletions(-) create mode 100644 akka-actor-testkit-typed/src/test/java/akka/actor/testkit/typed/javadsl/BehaviorTestKitTest.java diff --git a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/Effect.scala b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/Effect.scala index e19ab3946c..17607dbb00 100644 --- a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/Effect.scala +++ b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/Effect.scala @@ -4,13 +4,18 @@ package akka.actor.testkit.typed -import akka.annotation.DoNotInherit +import akka.actor.typed.{ ActorRef, Behavior, Props } +import akka.annotation.{ DoNotInherit, InternalApi } +import akka.util.JavaDurationConverters._ + +import scala.compat.java8.FunctionConverters._ +import scala.concurrent.duration.FiniteDuration /** * All tracked effects for the [[akka.actor.testkit.typed.scaladsl.BehaviorTestKit]] and * [[akka.actor.testkit.typed.javadsl.BehaviorTestKit]] must extend this type. * - * Factories/types for effects are available through [[akka.actor.testkit.typed.scaladsl.Effects]] + * Factories/types for effects are available through [[akka.actor.testkit.typed.javadsl.Effects]] * and [[akka.actor.testkit.typed.javadsl.Effects]] * * Not for user extension @@ -18,3 +23,175 @@ import akka.annotation.DoNotInherit @DoNotInherit abstract class Effect private[akka] () +object Effect { + /** + * The behavior spawned a named child with the given behavior (and optionally specific props) + */ + final class Spawned[T](val behavior: Behavior[T], val childName: String, val props: Props, val ref: ActorRef[T]) + extends Effect with Product3[Behavior[T], String, Props] with Serializable { + + override def equals(other: Any) = other match { + case o: Spawned[_] ⇒ + this.behavior == o.behavior && + this.childName == o.childName && + this.props == o.props + case _ ⇒ false + } + override def hashCode: Int = (behavior.## * 31 + childName.##) * 31 + props.## + override def toString: String = s"Spawned($behavior, $childName, $props)" + + override def productPrefix = "Spawned" + override def _1: Behavior[T] = behavior + override def _2: String = childName + override def _3: Props = props + override def canEqual(o: Any) = o.isInstanceOf[Spawned[_]] + } + + object Spawned { + def apply[T](behavior: Behavior[T], childName: String, props: Props = Props.empty): Spawned[T] = new Spawned(behavior, childName, props, null) + def unapply[T](s: Spawned[T]): Option[(Behavior[T], String, Props)] = Some((s.behavior, s.childName, s.props)) + } + + /** + * The behavior spawned an anonymous child with the given behavior (and optionally specific props) + */ + final class SpawnedAnonymous[T](val behavior: Behavior[T], val props: Props, val ref: ActorRef[T]) + extends Effect with Product2[Behavior[T], Props] with Serializable { + + override def equals(other: Any) = other match { + case o: SpawnedAnonymous[_] ⇒ this.behavior == o.behavior && this.props == o.props + case _ ⇒ false + } + override def hashCode: Int = behavior.## * 31 + props.## + override def toString: String = s"SpawnedAnonymous($behavior, $props)" + + override def productPrefix = "SpawnedAnonymous" + override def _1: Behavior[T] = behavior + override def _2: Props = props + override def canEqual(o: Any) = o.isInstanceOf[SpawnedAnonymous[_]] + } + + object SpawnedAnonymous { + def apply[T](behavior: Behavior[T], props: Props = Props.empty): SpawnedAnonymous[T] = new SpawnedAnonymous(behavior, props, null) + def unapply[T](s: SpawnedAnonymous[T]): Option[(Behavior[T], Props)] = Some((s.behavior, s.props)) + } + + /** + * INTERNAL API + * Spawning adapters is private[akka] + */ + @InternalApi + private[akka] final class SpawnedAdapter[T](val name: String, val ref: ActorRef[T]) + extends Effect with Product1[String] with Serializable { + + override def equals(other: Any) = other match { + case o: SpawnedAdapter[_] ⇒ this.name == o.name + case _ ⇒ false + } + override def hashCode: Int = name.## + override def toString: String = s"SpawnedAdapter($name)" + + override def productPrefix = "SpawnedAdapter" + override def _1: String = name + override def canEqual(o: Any) = o.isInstanceOf[SpawnedAdapter[_]] + } + + /** + * INTERNAL API + * Spawning adapters is private[akka] + */ + @InternalApi + private[akka] object SpawnedAdapter { + def apply[T](name: String): SpawnedAdapter[T] = new SpawnedAdapter(name, null) + def unapply[T](s: SpawnedAdapter[T]): Option[Tuple1[String]] = Some(Tuple1(s.name)) + } + + /** + * INTERNAL API + * The behavior spawned an anonymous adapter, through `ctx.spawnMessageAdapter` + */ + @InternalApi + private[akka] final class SpawnedAnonymousAdapter[T](val ref: ActorRef[T]) + extends Effect with Product with Serializable { + + override def equals(other: Any) = other match { + case _: SpawnedAnonymousAdapter[_] ⇒ true + case _ ⇒ false + } + override def hashCode: Int = Nil.## + override def toString: String = "SpawnedAnonymousAdapter" + + override def productPrefix = "SpawnedAnonymousAdapter" + override def productIterator = Iterator.empty + override def productArity = 0 + override def productElement(n: Int) = throw new NoSuchElementException + override def canEqual(o: Any) = o.isInstanceOf[SpawnedAnonymousAdapter[_]] + } + + /** + * INTERNAL API + */ + @InternalApi + private[akka] object SpawnedAnonymousAdapter { + def apply[T]() = new SpawnedAnonymousAdapter[T](null) + def unapply[T](s: SpawnedAnonymousAdapter[T]): Boolean = true + } + + /** + * The behavior create a message adapter for the messages of type clazz + */ + final case class MessageAdapter[A, T](messageClass: Class[A], adapt: A ⇒ T) extends Effect { + /** + * JAVA API + */ + def adaptFunction: java.util.function.Function[A, T] = adapt.asJava + } + + /** + * The behavior stopped `childName` + */ + final case class Stopped(childName: String) extends Effect + + /** + * The behavior started watching `other`, through `ctx.watch(other)` + */ + final case class Watched[T](other: ActorRef[T]) extends Effect + + /** + * The behavior started watching `other`, through `ctx.unwatch(other)` + */ + final case class Unwatched[T](other: ActorRef[T]) extends Effect + + /** + * The behavior set a new receive timeout, with `msg` as timeout notification + */ + final case class ReceiveTimeoutSet[T](d: FiniteDuration, msg: T) extends Effect { + /** + * Java API + */ + def duration(): java.time.Duration = d.asJava + } + + final case object ReceiveTimeoutCancelled extends ReceiveTimeoutCancelled + + sealed abstract class ReceiveTimeoutCancelled extends Effect + + /** + * The behavior used `ctx.schedule` to schedule `msg` to be sent to `target` after `delay` + * FIXME what about events scheduled through the scheduler? + */ + final case class Scheduled[U](delay: FiniteDuration, target: ActorRef[U], msg: U) extends Effect { + def duration(): java.time.Duration = delay.asJava + } + + /** + * Used to represent an empty list of effects - in other words, the behavior didn't do anything observable + */ + case object NoEffects extends NoEffects + + /** + * Used for NoEffects expectations by type + */ + sealed abstract class NoEffects extends Effect +} + diff --git a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/BehaviorTestKitImpl.scala b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/BehaviorTestKitImpl.scala index 78657d7967..a9cd50c724 100644 --- a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/BehaviorTestKitImpl.scala +++ b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/BehaviorTestKitImpl.scala @@ -11,7 +11,7 @@ import akka.actor.ActorPath import akka.actor.typed.{ Behavior, PostStop, Signal, ActorRef } import akka.annotation.InternalApi import akka.actor.testkit.typed.Effect -import akka.actor.testkit.typed.scaladsl.Effects._ +import akka.actor.testkit.typed.Effect._ import scala.annotation.tailrec import scala.collection.JavaConverters._ diff --git a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/EffectfulActorContext.scala b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/EffectfulActorContext.scala index afba39a8dc..04c3e22f68 100644 --- a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/EffectfulActorContext.scala +++ b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/EffectfulActorContext.scala @@ -5,14 +5,17 @@ package akka.actor.testkit.typed.internal import java.util.concurrent.ConcurrentLinkedQueue +import java.util.function -import akka.actor.{ Cancellable, ActorPath } +import akka.actor.{ ActorPath, Cancellable } import akka.actor.typed.{ ActorRef, Behavior, Props } import akka.annotation.InternalApi import akka.actor.testkit.typed.Effect -import akka.actor.testkit.typed.scaladsl.Effects._ +import akka.actor.testkit.typed.Effect._ import scala.concurrent.duration.{ Duration, FiniteDuration } +import scala.reflect.ClassTag +import scala.compat.java8.FunctionConverters._ /** * INTERNAL API @@ -26,18 +29,26 @@ import scala.concurrent.duration.{ Duration, FiniteDuration } effectQueue.offer(new SpawnedAnonymous(behavior, props, ref)) ref } - override def spawnMessageAdapter[U](f: U ⇒ T): ActorRef[U] = { val ref = super.spawnMessageAdapter(f) effectQueue.offer(new SpawnedAnonymousAdapter(ref)) ref } - override def spawnMessageAdapter[U](f: U ⇒ T, name: String): ActorRef[U] = { val ref = super.spawnMessageAdapter(f, name) effectQueue.offer(new SpawnedAdapter(name, ref)) ref } + override def messageAdapter[U: ClassTag](f: U ⇒ T): ActorRef[U] = { + val ref = super.messageAdapter(f) + effectQueue.offer(MessageAdapter(implicitly[ClassTag[U]].runtimeClass.asInstanceOf[Class[U]], f)) + ref + } + override def messageAdapter[U](messageClass: Class[U], f: function.Function[U, T]): ActorRef[U] = { + val ref = super.messageAdapter(messageClass, f) + effectQueue.offer(MessageAdapter[U, T](messageClass, f.asScala)) + ref + } override def spawn[U](behavior: Behavior[U], name: String, props: Props = Props.empty): ActorRef[U] = { val ref = super.spawn(behavior, name, props) effectQueue.offer(new Spawned(behavior, name, props, ref)) @@ -64,7 +75,7 @@ import scala.concurrent.duration.{ Duration, FiniteDuration } super.setReceiveTimeout(d, msg) } override def cancelReceiveTimeout(): Unit = { - effectQueue.offer(ReceiveTimeoutSet(Duration.Undefined, null)) + effectQueue.offer(ReceiveTimeoutCancelled) super.cancelReceiveTimeout() } override def schedule[U](delay: FiniteDuration, target: ActorRef[U], msg: U): Cancellable = { diff --git a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/javadsl/Effects.scala b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/javadsl/Effects.scala index f24fdb3bd8..d983b3738c 100644 --- a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/javadsl/Effects.scala +++ b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/javadsl/Effects.scala @@ -15,85 +15,69 @@ import akka.util.JavaDurationConverters._ * actual effects to expected ones. */ object Effects { - import akka.actor.testkit.typed.scaladsl.Effects._ + import akka.actor.testkit.typed.Effect._ /** * The behavior spawned a named child with the given behavior with no specific props */ - def spawned[T](behavior: Behavior[T], childName: String): Effect = Spawned(behavior, childName) + def spawned[T](behavior: Behavior[T], childName: String): Spawned[T] = Spawned(behavior, childName) /** * The behavior spawned a named child with the given behavior with no specific props */ - def spawned[T](behavior: Behavior[T], childName: String, ref: ActorRef[T]): Effect = new Spawned(behavior, childName, Props.empty, ref) + def spawned[T](behavior: Behavior[T], childName: String, ref: ActorRef[T]): Spawned[T] = new Spawned(behavior, childName, Props.empty, ref) /** * The behavior spawned a named child with the given behavior and specific props */ - def spawned[T](behavior: Behavior[T], childName: String, props: Props): Effect = Spawned(behavior, childName, props) + def spawned[T](behavior: Behavior[T], childName: String, props: Props): Spawned[T] = Spawned(behavior, childName, props) /** * The behavior spawned a named child with the given behavior and specific props */ - def spawned[T](behavior: Behavior[T], childName: String, props: Props, ref: ActorRef[T]): Effect = new Spawned(behavior, childName, props, ref) + def spawned[T](behavior: Behavior[T], childName: String, props: Props, ref: ActorRef[T]): Spawned[T] = new Spawned(behavior, childName, props, ref) /** * The behavior spawned an anonymous child with the given behavior with no specific props */ - def spawnedAnonymous[T](behavior: Behavior[T]): Effect = SpawnedAnonymous(behavior) + def spawnedAnonymous[T](behavior: Behavior[T]): SpawnedAnonymous[T] = SpawnedAnonymous(behavior) /** * The behavior spawned an anonymous child with the given behavior with no specific props */ - def spawnedAnonymous[T](behavior: Behavior[T], ref: ActorRef[T]): Effect = new SpawnedAnonymous(behavior, Props.empty, ref) + def spawnedAnonymous[T](behavior: Behavior[T], ref: ActorRef[T]): SpawnedAnonymous[T] = new SpawnedAnonymous(behavior, Props.empty, ref) /** * The behavior spawned an anonymous child with the given behavior with specific props */ - def spawnedAnonymous[T](behavior: Behavior[T], props: Props): Effect = SpawnedAnonymous(behavior, props) + def spawnedAnonymous[T](behavior: Behavior[T], props: Props): SpawnedAnonymous[T] = SpawnedAnonymous(behavior, props) /** * The behavior spawned an anonymous child with the given behavior with specific props */ - def spawnedAnonymous[T](behavior: Behavior[T], props: Props, ref: ActorRef[T]): Effect = new SpawnedAnonymous(behavior, props, ref) - /** - * The behavior spawned an anonymous adapter, through `ctx.spawnMessageAdapter` - */ - def spawnedAnonymousAdapter(): Effect = SpawnedAnonymousAdapter[Any]() - /** - * The behavior spawned an anonymous adapter, through `ctx.spawnMessageAdapter` - */ - def spawnedAnonymousAdapter[T](ref: ActorRef[T]): Effect = new SpawnedAnonymousAdapter(ref) - /** - * The behavior spawned a named adapter, through `ctx.spawnMessageAdapter` - */ - def spawnedAdapter(name: String): Effect = SpawnedAdapter[Any](name) - /** - * The behavior spawned a named adapter, through `ctx.spawnMessageAdapter` - */ - def spawnedAdapter[T](name: String, ref: ActorRef[T]): Effect = new SpawnedAdapter(name, ref) + def spawnedAnonymous[T](behavior: Behavior[T], props: Props, ref: ActorRef[T]): SpawnedAnonymous[T] = new SpawnedAnonymous(behavior, props, ref) /** * The behavior stopped `childName` */ - def stopped(childName: String): Effect = Stopped(childName) + def stopped(childName: String): Stopped = Stopped(childName) /** * The behavior started watching `other`, through `ctx.watch(other)` */ - def watched[T](other: ActorRef[T]): Effect = Watched(other) + def watched[T](other: ActorRef[T]): Watched[T] = Watched(other) /** * The behavior started watching `other`, through `ctx.unwatch(other)` */ - def unwatched[T](other: ActorRef[T]): Effect = Unwatched(other) + def unwatched[T](other: ActorRef[T]): Unwatched[T] = Unwatched(other) /** * The behavior set a new receive timeout, with `msg` as timeout notification */ - def receiveTimeoutSet[T](d: Duration, msg: T): Effect = ReceiveTimeoutSet(d.asScala, msg) + def receiveTimeoutSet[T](d: Duration, msg: T): ReceiveTimeoutSet[T] = ReceiveTimeoutSet(d.asScala, msg) /** * The behavior used `ctx.schedule` to schedule `msg` to be sent to `target` after `delay` * FIXME what about events scheduled through the scheduler? */ - def scheduled[U](delay: Duration, target: ActorRef[U], msg: U): Effect = + def scheduled[U](delay: Duration, target: ActorRef[U], msg: U): Scheduled[U] = Scheduled(delay.asScala, target, msg) /** * Used to represent an empty list of effects - in other words, the behavior didn't do anything observable */ - def noEffects(): Effect = NoEffects + def noEffects(): NoEffects = NoEffects } diff --git a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/scaladsl/Effects.scala b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/scaladsl/Effects.scala index 841c9e12b0..a6c0838aa3 100644 --- a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/scaladsl/Effects.scala +++ b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/scaladsl/Effects.scala @@ -5,142 +5,77 @@ package akka.actor.testkit.typed.scaladsl import akka.actor.typed.{ ActorRef, Behavior, Props } -import akka.actor.testkit.typed.Effect -import scala.concurrent.duration.{ Duration, FiniteDuration } +import scala.concurrent.duration.FiniteDuration /** - * Types for behavior effects for [[BehaviorTestKit]], each effect has a suitable equals and can be used to compare + * Factories for behavior effects for [[BehaviorTestKit]], each effect has a suitable equals and can be used to compare * actual effects to expected ones. */ object Effects { + import akka.actor.testkit.typed.Effect._ /** - * The behavior spawned a named child with the given behavior (and optionally specific props) + * The behavior spawned a named child with the given behavior with no specific props */ - final class Spawned[T](val behavior: Behavior[T], val childName: String, val props: Props, val ref: ActorRef[T]) - extends Effect with Product3[Behavior[T], String, Props] with Serializable { - - override def equals(other: Any) = other match { - case o: Spawned[_] ⇒ - this.behavior == o.behavior && - this.childName == o.childName && - this.props == o.props - case _ ⇒ false - } - override def hashCode: Int = (behavior.## * 31 + childName.##) * 31 + props.## - override def toString: String = s"Spawned($behavior, $childName, $props)" - - override def productPrefix = "Spawned" - override def _1: Behavior[T] = behavior - override def _2: String = childName - override def _3: Props = props - override def canEqual(o: Any) = o.isInstanceOf[Spawned[_]] - } - object Spawned { - def apply[T](behavior: Behavior[T], childName: String, props: Props = Props.empty): Spawned[T] = new Spawned(behavior, childName, props, null) - def unapply[T](s: Spawned[T]): Option[(Behavior[T], String, Props)] = Some((s.behavior, s.childName, s.props)) - } - + def spawned[T](behavior: Behavior[T], childName: String): Spawned[T] = Spawned(behavior, childName) /** - * The behavior spawned an anonymous child with the given behavior (and optionally specific props) + * The behavior spawned a named child with the given behavior with no specific props */ - final class SpawnedAnonymous[T](val behavior: Behavior[T], val props: Props, val ref: ActorRef[T]) - extends Effect with Product2[Behavior[T], Props] with Serializable { - - override def equals(other: Any) = other match { - case o: SpawnedAnonymous[_] ⇒ this.behavior == o.behavior && this.props == o.props - case _ ⇒ false - } - override def hashCode: Int = behavior.## * 31 + props.## - override def toString: String = s"SpawnedAnonymous($behavior, $props)" - - override def productPrefix = "SpawnedAnonymous" - override def _1: Behavior[T] = behavior - override def _2: Props = props - override def canEqual(o: Any) = o.isInstanceOf[SpawnedAnonymous[_]] - } - object SpawnedAnonymous { - def apply[T](behavior: Behavior[T], props: Props = Props.empty): SpawnedAnonymous[T] = new SpawnedAnonymous(behavior, props, null) - def unapply[T](s: SpawnedAnonymous[T]): Option[(Behavior[T], Props)] = Some((s.behavior, s.props)) - } - + def spawned[T](behavior: Behavior[T], childName: String, ref: ActorRef[T]): Spawned[T] = new Spawned(behavior, childName, Props.empty, ref) /** - * The behavior spawned a named adapter, through `ctx.spawnMessageAdapter` + * The behavior spawned a named child with the given behavior and specific props */ - final class SpawnedAdapter[T](val name: String, val ref: ActorRef[T]) - extends Effect with Product1[String] with Serializable { - - override def equals(other: Any) = other match { - case o: SpawnedAdapter[_] ⇒ this.name == o.name - case _ ⇒ false - } - override def hashCode: Int = name.## - override def toString: String = s"SpawnedAdapter($name)" - - override def productPrefix = "SpawnedAdapter" - override def _1: String = name - override def canEqual(o: Any) = o.isInstanceOf[SpawnedAdapter[_]] - } - object SpawnedAdapter { - def apply[T](name: String): SpawnedAdapter[T] = new SpawnedAdapter(name, null) - def unapply[T](s: SpawnedAdapter[T]): Option[Tuple1[String]] = Some(Tuple1(s.name)) - } - + def spawned[T](behavior: Behavior[T], childName: String, props: Props): Spawned[T] = Spawned(behavior, childName, props) /** - * The behavior spawned an anonymous adapter, through `ctx.spawnMessageAdapter` + * The behavior spawned a named child with the given behavior and specific props */ - final class SpawnedAnonymousAdapter[T](val ref: ActorRef[T]) - extends Effect with Product with Serializable { - - override def equals(other: Any) = other match { - case o: SpawnedAnonymousAdapter[_] ⇒ true - case _ ⇒ false - } - override def hashCode: Int = Nil.## - override def toString: String = "SpawnedAnonymousAdapter" - - override def productPrefix = "SpawnedAnonymousAdapter" - override def productIterator = Iterator.empty - override def productArity = 0 - override def productElement(n: Int) = throw new NoSuchElementException - override def canEqual(o: Any) = o.isInstanceOf[SpawnedAnonymousAdapter[_]] - } - object SpawnedAnonymousAdapter { - def apply[T]() = new SpawnedAnonymousAdapter[T](null) - def unapply[T](s: SpawnedAnonymousAdapter[T]): Boolean = true - } + def spawned[T](behavior: Behavior[T], childName: String, props: Props, ref: ActorRef[T]): Spawned[T] = new Spawned(behavior, childName, props, ref) + /** + * The behavior spawned an anonymous child with the given behavior with no specific props + */ + def spawnedAnonymous[T](behavior: Behavior[T]): SpawnedAnonymous[T] = SpawnedAnonymous(behavior) + /** + * The behavior spawned an anonymous child with the given behavior with no specific props + */ + def spawnedAnonymous[T](behavior: Behavior[T], ref: ActorRef[T]): SpawnedAnonymous[T] = new SpawnedAnonymous(behavior, Props.empty, ref) + /** + * The behavior spawned an anonymous child with the given behavior with specific props + */ + def spawnedAnonymous[T](behavior: Behavior[T], props: Props): SpawnedAnonymous[T] = SpawnedAnonymous(behavior, props) + /** + * The behavior spawned an anonymous child with the given behavior with specific props + */ + def spawnedAnonymous[T](behavior: Behavior[T], props: Props, ref: ActorRef[T]): SpawnedAnonymous[T] = new SpawnedAnonymous(behavior, props, ref) /** * The behavior stopped `childName` */ - final case class Stopped(childName: String) extends Effect + def stopped(childName: String): Stopped = Stopped(childName) /** * The behavior started watching `other`, through `ctx.watch(other)` */ - final case class Watched[T](other: ActorRef[T]) extends Effect + def watched[T](other: ActorRef[T]): Watched[T] = Watched(other) /** * The behavior started watching `other`, through `ctx.unwatch(other)` */ - final case class Unwatched[T](other: ActorRef[T]) extends Effect + def unwatched[T](other: ActorRef[T]): Unwatched[T] = Unwatched(other) + /** * The behavior set a new receive timeout, with `msg` as timeout notification */ - final case class ReceiveTimeoutSet[T](d: Duration, msg: T) extends Effect + def receiveTimeoutSet[T](d: FiniteDuration, msg: T): ReceiveTimeoutSet[T] = ReceiveTimeoutSet(d, msg) /** * The behavior used `ctx.schedule` to schedule `msg` to be sent to `target` after `delay` * FIXME what about events scheduled through the scheduler? */ - final case class Scheduled[U](delay: FiniteDuration, target: ActorRef[U], msg: U) extends Effect + def scheduled[U](delay: FiniteDuration, target: ActorRef[U], msg: U): Scheduled[U] = + Scheduled(delay, target, msg) /** * Used to represent an empty list of effects - in other words, the behavior didn't do anything observable */ - case object NoEffects extends NoEffects + def noEffects(): NoEffects = NoEffects - /** - * Used for NoEffects expectations by type - */ - sealed trait NoEffects extends Effect } diff --git a/akka-actor-testkit-typed/src/test/java/akka/actor/testkit/typed/javadsl/ActorTestKitTest.java b/akka-actor-testkit-typed/src/test/java/akka/actor/testkit/typed/javadsl/ActorTestKitTest.java index 35958c4ca4..c64a0a9b61 100644 --- a/akka-actor-testkit-typed/src/test/java/akka/actor/testkit/typed/javadsl/ActorTestKitTest.java +++ b/akka-actor-testkit-typed/src/test/java/akka/actor/testkit/typed/javadsl/ActorTestKitTest.java @@ -16,9 +16,6 @@ import java.util.concurrent.TimeUnit; import static akka.Done.done; import static org.junit.Assert.assertEquals; -/** - * Copyright (C) 2009-2018 Lightbend Inc. - */ public class ActorTestKitTest extends JUnitSuite { @ClassRule diff --git a/akka-actor-testkit-typed/src/test/java/akka/actor/testkit/typed/javadsl/BehaviorTestKitTest.java b/akka-actor-testkit-typed/src/test/java/akka/actor/testkit/typed/javadsl/BehaviorTestKitTest.java new file mode 100644 index 0000000000..dbbb7280e1 --- /dev/null +++ b/akka-actor-testkit-typed/src/test/java/akka/actor/testkit/typed/javadsl/BehaviorTestKitTest.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2018 Lightbend Inc. + */ + +package akka.actor.testkit.typed.javadsl; + +import akka.Done; +import akka.actor.testkit.typed.Effect; +import akka.actor.typed.ActorRef; +import akka.actor.typed.Behavior; +import akka.actor.typed.Props; +import akka.actor.typed.javadsl.Behaviors; +import org.junit.Ignore; +import org.junit.Test; +import org.scalatest.junit.JUnitSuite; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.stream.IntStream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class BehaviorTestKitTest extends JUnitSuite { + + public interface Command { + } + + public static class SpawnWatchAndUnWatch implements Command { + private final String name; + + public SpawnWatchAndUnWatch(String name) { + this.name = name; + } + } + + public static class SpawnAndWatchWith implements Command { + private final String name; + + public SpawnAndWatchWith(String name) { + this.name = name; + } + } + + public static class SpawnSession implements Command { + private final ActorRef> replyTo; + private final ActorRef sessionHandler; + + public SpawnSession(ActorRef> replyTo, ActorRef sessionHandler) { + this.replyTo = replyTo; + this.sessionHandler = sessionHandler; + } + } + + public static class KillSession implements Command { + private final ActorRef session; + private final ActorRef replyTo; + + public KillSession(ActorRef session, ActorRef replyTo) { + this.session = session; + this.replyTo = replyTo; + } + } + + public static class CreateMessageAdapter implements Command { + private final Class clazz; + private final Function f; + + public CreateMessageAdapter(Class clazz, Function f) { + this.clazz = clazz; + this.f = f; + } + } + + public static class SpawnChildren implements Command { + private final int numberOfChildren; + + public SpawnChildren(int numberOfChildren) { + this.numberOfChildren = numberOfChildren; + } + } + + public static class SpawnChildrenAnonymous implements Command { + private final int numberOfChildren; + + public SpawnChildrenAnonymous(int numberOfChildren) { + this.numberOfChildren = numberOfChildren; + } + } + + public static class SpawnChildrenWithProps implements Command { + private final int numberOfChildren; + private final Props props; + + public SpawnChildrenWithProps(int numberOfChildren, Props props) { + this.numberOfChildren = numberOfChildren; + this.props = props; + } + } + + public static class SpawnChildrenAnonymousWithProps implements Command { + private final int numberOfChildren; + private final Props props; + + public SpawnChildrenAnonymousWithProps(int numberOfChildren, Props props) { + this.numberOfChildren = numberOfChildren; + this.props = props; + } + } + + public interface Action { + } + + private static Behavior childInitial = Behaviors.ignore(); + + private static Props props = Props.empty().withDispatcherFromConfig("cat"); + + private static Behavior behavior = Behaviors.receive(Command.class) + .onMessage(SpawnChildren.class, (ctx, msg) -> { + IntStream.range(0, msg.numberOfChildren).forEach(i -> { + ctx.spawn(childInitial, "child" + i); + }); + return Behaviors.same(); + }) + .onMessage(SpawnChildrenAnonymous.class, (ctx, msg) -> { + IntStream.range(0, msg.numberOfChildren).forEach(i -> { + ctx.spawnAnonymous(childInitial); + }); + return Behaviors.same(); + }) + .onMessage(SpawnChildrenWithProps.class, (ctx, msg) -> { + IntStream.range(0, msg.numberOfChildren).forEach(i -> { + ctx.spawn(childInitial, "child" + i, msg.props); + }); + return Behaviors.same(); + }) + .onMessage(SpawnChildrenAnonymousWithProps.class, (ctx, msg) -> { + IntStream.range(0, msg.numberOfChildren).forEach(i -> { + ctx.spawnAnonymous(childInitial, msg.props); + }); + return Behaviors.same(); + }) + .onMessage(CreateMessageAdapter.class, (ctx, msg) -> { + ctx.messageAdapter(msg.clazz, msg.f); + return Behaviors.same(); + }) + .onMessage(SpawnWatchAndUnWatch.class, (ctx, msg) -> { + ActorRef c = ctx.spawn(childInitial, msg.name); + ctx.watch(c); + ctx.unwatch(c); + return Behaviors.same(); + }) + .onMessage(SpawnAndWatchWith.class, (ctx, msg) -> { + ActorRef c = ctx.spawn(childInitial, msg.name); + ctx.watchWith(c, msg); + return Behaviors.same(); + }) + .onMessage(SpawnSession.class, (ctx, msg) -> { + ActorRef session = ctx.spawnAnonymous(Behaviors.receiveMessage( m -> { + msg.sessionHandler.tell(m); + return Behaviors.same(); + })); + msg.replyTo.tell(session); + return Behaviors.same(); + }) + .onMessage(KillSession.class, (ctx, msg) -> { + ctx.stop(msg.session); + msg.replyTo.tell(Done.getInstance()); + return Behaviors.same(); + }) + .build(); + + + @Test + public void allowAssertionsOnEffectType() { + BehaviorTestKit test = BehaviorTestKit.create(behavior); + test.run(new SpawnChildren(1)); + Effect.Spawned spawned = test.expectEffectClass(Effect.Spawned.class); + assertEquals(spawned.childName(), "child0"); + } + + @Test + public void allowExpectingNoEffects() { + BehaviorTestKit test = BehaviorTestKit.create(behavior); + test.expectEffect(Effects.noEffects()); + } + + @Test + public void allowsExpectingNoEffectByType() { + BehaviorTestKit test = BehaviorTestKit.create(behavior); + test.expectEffectClass(Effect.NoEffects.class); + } + + @Test + public void returnEffectsThatHaveTakenPlace() { + BehaviorTestKit test = BehaviorTestKit.create(behavior); + assertFalse(test.hasEffects()); + test.run(new SpawnChildrenAnonymous(1)); + assertTrue(test.hasEffects()); + } + + @Test + @Ignore("Not supported for Java API") + public void allowAssertionsUsingPartialFunctions() { + } + + @Test + public void spawnChildrenWithNoProps() { + BehaviorTestKit test = BehaviorTestKit.create(behavior); + test.run(new SpawnChildren(2)); + List allEffects = test.getAllEffects(); + assertEquals( + Arrays.asList(Effects.spawned(childInitial, "child0"), Effects.spawned(childInitial, "child1", Props.empty())), + allEffects + ); + } + + @Test + public void spawnChildrenWithProps() { + BehaviorTestKit test = BehaviorTestKit.create(behavior); + test.run(new SpawnChildrenWithProps(1, props)); + assertEquals(props, test.expectEffectClass(Effect.Spawned.class).props()); + } + + @Test + public void spawnAnonChildrenWithNoProps() { + BehaviorTestKit test = BehaviorTestKit.create(behavior); + test.run(new SpawnChildrenAnonymous(2)); + List allEffects = test.getAllEffects(); + assertEquals( + Arrays.asList(Effects.spawnedAnonymous(childInitial), Effects.spawnedAnonymous(childInitial, Props.empty())), + allEffects + ); + } + + @Test + public void spawnAnonChildrenWithProps() { + BehaviorTestKit test = BehaviorTestKit.create(behavior); + test.run(new SpawnChildrenAnonymousWithProps(1, props)); + assertEquals(props, test.expectEffectClass(Effect.SpawnedAnonymous.class).props()); + } + + @Test + public void createMessageAdapters() { + BehaviorTestKit test = BehaviorTestKit.create(behavior); + SpawnChildren adaptedMessage = new SpawnChildren(1); + test.run(new CreateMessageAdapter(String.class, o -> adaptedMessage)); + Effect.MessageAdapter mAdapter = test.expectEffectClass(Effect.MessageAdapter.class); + assertEquals(String.class, mAdapter.messageClass()); + assertEquals(adaptedMessage, mAdapter.adaptFunction().apply("anything")); + } + + @Test + public void recordWatching() { + BehaviorTestKit test = BehaviorTestKit.create(behavior); + test.run(new SpawnWatchAndUnWatch("name")); + ActorRef child = test.childInbox("name").getRef(); + test.expectEffectClass(Effect.Spawned.class); + assertEquals(child, test.expectEffectClass(Effect.Watched.class).other()); + assertEquals(child, test.expectEffectClass(Effect.Unwatched.class).other()); + } + + @Test + public void recordWatchWith() { + BehaviorTestKit test = BehaviorTestKit.create(behavior); + test.run(new SpawnWatchAndUnWatch("name")); + ActorRef child = test.childInbox("name").getRef(); + test.expectEffectClass(Effect.Spawned.class); + assertEquals(child, test.expectEffectClass(Effect.Watched.class).other()); + } + + @Test + public void allowRetrievingAndKilling() { + BehaviorTestKit test = BehaviorTestKit.create(behavior); + TestInbox> i = TestInbox.create(); + TestInbox h = TestInbox.create(); + test.run(new SpawnSession(i.getRef(), h.getRef())); + + ActorRef sessionRef = i.receiveMessage(); + assertFalse(i.hasMessages()); + Effect.SpawnedAnonymous s = test.expectEffectClass(Effect.SpawnedAnonymous.class); + assertEquals(sessionRef, s.ref()); + + BehaviorTestKit session = test.childTestKit(sessionRef); + session.run("hello"); + assertEquals(Collections.singletonList("hello"), h.getAllReceived()); + + TestInbox d = TestInbox.create(); + test.run(new KillSession(sessionRef, d.getRef())); + + assertEquals(Collections.singletonList(Done.getInstance()), d.getAllReceived()); + test.expectEffectClass(Effect.Stopped.class); + } + +} diff --git a/akka-actor-testkit-typed/src/test/scala/akka/actor/testkit/typed/scaladsl/BehaviorTestKitSpec.scala b/akka-actor-testkit-typed/src/test/scala/akka/actor/testkit/typed/scaladsl/BehaviorTestKitSpec.scala index a933fe86b7..b36d7eb74b 100644 --- a/akka-actor-testkit-typed/src/test/scala/akka/actor/testkit/typed/scaladsl/BehaviorTestKitSpec.scala +++ b/akka-actor-testkit-typed/src/test/scala/akka/actor/testkit/typed/scaladsl/BehaviorTestKitSpec.scala @@ -7,11 +7,14 @@ package akka.actor.testkit.typed.scaladsl import akka.Done import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.{ ActorRef, Behavior, Props } -import akka.actor.testkit.typed.scaladsl.Effects.{ NoEffects, Spawned, SpawnedAdapter, SpawnedAnonymous, SpawnedAnonymousAdapter, Watched, Unwatched } +import akka.actor.testkit.typed.Effect +import akka.actor.testkit.typed.Effect._ import akka.actor.testkit.typed.scaladsl.BehaviorTestKitSpec.{ Child, Father } import akka.actor.testkit.typed.scaladsl.BehaviorTestKitSpec.Father._ import org.scalatest.{ Matchers, WordSpec } +import scala.reflect.ClassTag + object BehaviorTestKitSpec { object Father { @@ -25,6 +28,7 @@ object BehaviorTestKitSpec { case class SpawnAnonymousWithProps(numberOfChildren: Int, props: Props) extends Command case object SpawnAdapter extends Command case class SpawnAdapterWithName(name: String) extends Command + case class CreateMessageAdapter[U](messageClass: Class[U], f: U ⇒ Command) extends Command case class SpawnAndWatchUnwatch(name: String) extends Command case class SpawnAndWatchWith(name: String) extends Command case class SpawnSession(replyTo: ActorRef[ActorRef[String]], sessionHandler: ActorRef[String]) extends Command @@ -82,6 +86,10 @@ object BehaviorTestKitSpec { ctx.stop(session) replyTo ! Done Behaviors.same + case CreateMessageAdapter(messageClass, f) ⇒ + ctx.messageAdapter(f)(ClassTag(messageClass)) + Behaviors.same + } } } @@ -103,14 +111,14 @@ object BehaviorTestKitSpec { class BehaviorTestKitSpec extends WordSpec with Matchers { - private val props = Props.empty + private val props = Props.empty.withDispatcherFromConfig("cat") "BehaviorTestKit" must { "allow assertions on effect type" in { val testkit = BehaviorTestKit[Father.Command](Father.init) testkit.run(SpawnAnonymous(1)) - val spawnAnonymous = testkit.expectEffectType[Effects.SpawnedAnonymous[_]] + val spawnAnonymous = testkit.expectEffectType[Effect.SpawnedAnonymous[_]] spawnAnonymous.props should ===(Props.empty) } @@ -209,6 +217,14 @@ class BehaviorTestKitSpec extends WordSpec with Matchers { } } + "BehaviorTestkit's messageAdapter" must { + "create message adapters and record effects" in { + val testkit = BehaviorTestKit[Father.Command](Father.init) + testkit.run(CreateMessageAdapter(classOf[String], (_: String) ⇒ SpawnChildren(1))) + testkit.expectEffectType[MessageAdapter[String, Command]] + } + } + "BehaviorTestkit's run" can { "run behaviors with messages without canonicalization" in { val testkit = BehaviorTestKit[Father.Command](Father.init) @@ -224,9 +240,9 @@ class BehaviorTestKitSpec extends WordSpec with Matchers { testkit.run(SpawnAndWatchUnwatch("hello")) val child = testkit.childInbox("hello").ref testkit.retrieveAllEffects() should be(Seq( - Spawned(Child.initial, "hello", Props.empty), - Watched(child), - Unwatched(child) + Effects.spawned(Child.initial, "hello", Props.empty), + Effects.watched(child), + Effects.unwatched(child) )) } @@ -235,8 +251,8 @@ class BehaviorTestKitSpec extends WordSpec with Matchers { testkit.run(SpawnAndWatchWith("hello")) val child = testkit.childInbox("hello").ref testkit.retrieveAllEffects() should be(Seq( - Spawned(Child.initial, "hello", Props.empty), - Watched(child) + Effects.spawned(Child.initial, "hello", Props.empty), + Effects.watched(child) )) } } @@ -250,7 +266,7 @@ class BehaviorTestKitSpec extends WordSpec with Matchers { val sessionRef = i.receiveMessage() i.hasMessages shouldBe false - val (s: SpawnedAnonymous[_]) :: Nil = testkit.retrieveAllEffects() + val s = testkit.expectEffectType[SpawnedAnonymous[_]] // must be able to get the created ref, even without explicit reply s.ref shouldBe sessionRef @@ -262,6 +278,7 @@ class BehaviorTestKitSpec extends WordSpec with Matchers { testkit.run(KillSession(sessionRef, d.ref)) d.receiveAll shouldBe Seq(Done) + testkit.expectEffectType[Stopped] } } } diff --git a/akka-actor-testkit-typed/src/test/scala/akka/actor/testkit/typed/scaladsl/SyncTestingExampleSpec.scala b/akka-actor-testkit-typed/src/test/scala/akka/actor/testkit/typed/scaladsl/SyncTestingExampleSpec.scala index 0e3e8a8f19..776cff489d 100644 --- a/akka-actor-testkit-typed/src/test/scala/akka/actor/testkit/typed/scaladsl/SyncTestingExampleSpec.scala +++ b/akka-actor-testkit-typed/src/test/scala/akka/actor/testkit/typed/scaladsl/SyncTestingExampleSpec.scala @@ -7,7 +7,7 @@ package akka.actor.testkit.typed.scaladsl //#imports import akka.actor.typed._ import akka.actor.typed.scaladsl._ -import akka.actor.testkit.typed.scaladsl.Effects._ +import akka.actor.testkit.typed.Effect._ //#imports import org.scalatest.{ Matchers, WordSpec }