All Downloads are FREE. Search and download functionalities are using the official Maven repository.

teststate.run.Runner.scala Maven / Gradle / Ivy

The newest version!
package teststate.run

// import acyclic.file
import scala.annotation.tailrec
import scala.collection.mutable
import teststate.data._
import teststate.typeclass._
import teststate.core._
import CoreExports._
import CoreExports2._
import Types.SackE
import Result.{Fail, Skip, Pass}

object Runner {

  trait HalfCheck[O, S, E] {
    type A
    val check: Around.DeltaAux[OS[O, S], E, A]
    val before: Tri[Failure[E], A]
  }

  def HalfCheck[O, S, E, a](_check: Around.DeltaAux[OS[O, S], E, a])(_before: Tri[Failure[E], a]): HalfCheck[O, S, E] =
    new HalfCheck[O, S, E] {
      override type A = a
      override val check = _check
      override val before = _before
    }

  val ActionName    : Name = "Action"
  val PreName       : Name = "Pre-conditions"
  val PostName      : Name = "Post-conditions"
  val InvariantsName: Name = "Invariants"

  val Observation  = "Observation"
  val UpdateState  = "Update expected state"
  val InitialState = "Initial state."


  final def foreachSack[I, A](sack: Sack[I, A])(i: I)(err: (Name, Throwable) => Unit)(f: A => Unit): Boolean = {
    var coproductFound = false
    def go(s: Sack[I, A]): Unit =
      s match {
        case Sack.Value(a)        => f(a)
        case Sack.Product(ss)     => ss foreach go
        case Sack.CoProduct(n, p) =>
          coproductFound = true
          Recover.id.attempt(p(i)) match {
            case Right(s) => go(s)
            case Left(e)  => err(Recover.byToString.name(n, Some(i)), e.failure)
          }
      }
    go(sack)
    coproductFound
  }

  def foreachSackE[A, B, E](s: SackE[A, B, E])(a: A)(f: NamedError[Failure[E]] Or B => Unit)(implicit r: Recover[E]) =
    foreachSack(s)(a)((n, t) => f(Left(NamedError(n, r(t)))))(f)

  private case class ActionQueue[F[_], R, O, S, E](head: NamedError[Failure[E]] Or Action.Outer[F, R, O, S, E],
                                                   tail: Actions[F, R, O, S, E])

  private case class Progress[F[_], R, O, S, E](queue  : Option[ActionQueue[F, R, O, S, E]],
                                                ros    : ROS[R, O, S],
                                                history: History[Failure[E]]) {
    def failure: Option[Failure[E]] = history.failure
    def failed : Boolean            = history.failed

    def :+(s: History.Step[Failure[E]])  = copy(history = history :+ s)
    def ++(s: History.Steps[Failure[E]]) = copy(history = history ++ s)
    def ++(s: History[Failure[E]])       = copy(history = history ++ s.steps)
  }

  private object Progress {
    def prepareNext[F[_], R, O, S, E](actions: Actions[F, R, O, S, E],
                                      ros    : ROS[R, O, S],
                                      history: History[Failure[E]])(implicit r: Recover[E]): Progress[F, R, O, S, E] = {

      @tailrec
      def queue(subject: Actions[F, R, O, S, E], tail: Actions[F, R, O, S, E]): Option[ActionQueue[F, R, O, S, E]] =
        subject match {
          case Sack.Value(a) =>
            Some(ActionQueue(a, tail))

          case Sack.Product(as) =>
            as.length match {
              case 0 => None
              case 1 => queue(as.head, tail)
              case _ => queue(as.head, Sack.append(Sack.Product(as.tail), tail))
            }

          case Sack.CoProduct(n, p) =>
            r.attempt(p(ros)) match {
              case Right(as) => queue(as, tail)
              case Left(e)   => Some(ActionQueue(Left(NamedError(r.name(n, ros.some), e)), tail))
            }
        }

      Progress(queue(actions, Sack.empty), ros, history)
    }
  }

  private type CheckNE[C[_, _], O, S, E] = NamedError[Failure[E]] Or C[OS[O, S], E]

  final class RefEq[+A <: AnyRef](val value: A) {
    override def hashCode = value.hashCode
    override def equals(x: Any) = x match {
      case y: RefEq[AnyRef] => value eq y.value
      case _                => false
    }
  }

  final class UniqueListBuilder[A <: AnyRef] {
    private val set = mutable.LinkedHashSet.empty[RefEq[A]]

    def +=(a: A): Unit = {
      set add new RefEq(a)
      ()
    }

    def iterator(): Iterator[A] =
      set.iterator.map(_.value)

    def result(): List[A] =
      iterator().toList
  }

  case class UnpackChecks[F[_], O, S, E](befores       : F[Point        [OS[O, S], E]],
                                         deltas        : F[Around.DeltaA[OS[O, S], E]],
                                         aftersA       : F[Point        [OS[O, S], E]],
                                         aftersI       : F[Point        [OS[O, S], E]],
                                         errors        : F[NamedError[Failure[E]]],
                                         coproductFound: Boolean)

  def unpackChecks[O, S, E](invariants: Invariants[O, S, E],
                            arounds   : Arounds[O, S, E],
                            input     : OS[O, S])
                           (implicit r: Recover[E]): UnpackChecks[List, O, S, E] = {

    import Around.{Before, After}

    type Builder[A <: AnyRef] = UniqueListBuilder[A]

    def newBuilder[A <: AnyRef]: Builder[A] =
      new UniqueListBuilder[A]

    def add[A <: AnyRef](b: Builder[A], a: A): Unit =
      b += a

    def result[A <: AnyRef](r: Builder[A]): List[A] =
      r.result()

    val bs = newBuilder[Point        [OS[O, S], E]]
    val ds = newBuilder[Around.DeltaA[OS[O, S], E]]
    val aa = newBuilder[Point        [OS[O, S], E]]
    val ai = newBuilder[Point        [OS[O, S], E]]
    val es = newBuilder[NamedError[Failure[E]]]

    val coproductFoundI =
      foreachSackE(invariants)(input) {
        case Right(Invariant.Point(p)) => add(ai, p)
        case Right(Invariant.Delta(d)) => add(ds, d)
        case Left(e)                   => add(es, e)
      }

    val coproductFoundA =
      foreachSackE(arounds)(input) {
        case Right(Around.Delta(d)        ) => add(ds, d)
        case Right(Around.Point(p, Before)) => add(bs, p)
        case Right(Around.Point(p, After )) => add(aa, p)
        case Left(e)                        => add(es, e)
      }

    UnpackChecks(result(bs), result(ds), result(aa), result(ai), result(es), coproductFoundI || coproductFoundA)
  }

  def run[F[_], R, O, S, E](test: Test[F, R, O, S, E])(initialState: S, ref: => R): F[Report[E]] = {
    val runner = new Runner[F, R, O, S, E]()(test.executionModel, test.recover)
    runner.run(test)(initialState, () => ref)
  }

  @inline implicit class RunnerTriExt[E, A](private val self: Tri[E, A]) extends AnyVal {
    def noCause: Tri[Failure[E], A] =
      self.mapE(Failure NoCause _)
  }
  @inline implicit class RunnerOptionExt[E](private val self: Option[E]) extends AnyVal {
    def noCause: Option[Failure[E]] =
      self.map(Failure NoCause _)
  }
}

/**
  * Internal use only. Not thread-safe (due to mutable stats). Don't expose.
  */
private final class Runner[F[_], R, O, S, E](implicit EM: ExecutionModel[F], recover: Recover[E]) {
  import Runner._

  private type FE   = Failure[E]
  private type H    = History[FE]
  private type ROS  = teststate.data.ROS[R, O, S]
  private type Test = teststate.run.Test[F, R, O, S, E]
  private type P    = Progress[F, R, O, S, E]

  private def observe(test: Test, ref: R): FE Or O =
    recover.recover(test.observer(ref).leftMap(Failure NoCause _), Left(_))

  private var stats: Stats.Mutable = _

  def run(test: Test)(initialState: S, refFn: () => R): F[Report[E]] =
    EM flatten EM.point {
      stats = new Stats.Mutable
      stats.startTimer()
      val ref = refFn()

      val history: F[H] =
        observe(test, ref) match {

          case Right(obs) =>
            val ros = new ROS(ref, obs, initialState)
            EM.map(subtest(test, Sack.empty, refFn, ros, true))(_.history)

          case Left(e) =>
            val s = History.Step(Observation, Fail(e))
            val h = History(History.parent(InitialState, History(s)))
            EM pure h
        }

      EM.map(history) { h =>
        stats.stopTimer()
        Report(test.name, h, stats.result())
      }
    }

  private def checkAround0(name      : Name,
                           invariants: Invariants[O, S, E],
                           arounds   : Arounds[O, S, E],
                           collapse  : Boolean,
                           p         : P,
                           run       : => F[(H, ROS)]): F[P] = {

    val checksPre = unpackChecks(invariants, arounds, p.ros.os)

    // Perform before
    val pre = {
      val b = History.newBuilder[E](stats)
      checksPre.errors foreach b.addNE
      b.addEach(checksPre.befores)(_.name)(p.ros.sos, _.test(p.ros.os).noCause)
      b.group(PreName)
    }

    if (pre.failed) {
      EM.pure(p :+ History.parent(name, pre))

    } else {

      // Perform around-pre
      val hcs = {
        val b = Vector.newBuilder[HalfCheck[O, S, E]]
        for (d0 <- checksPre.deltas) {
          val d = d0.aux
          val r = recover.attempt(d.before(p.ros.os)).fold[Tri[FE, d.A]](Failed(_), _.noCause)
          b += HalfCheck(d)(r)
        }
        b.result()
      }

      // Perform action
      EM.map(run) { case (step, ros2) =>

        def addStep(s: History.Step[FE]) =
          p.copy(ros = ros2, history = p.history :+ s)

        val collapseIfNoPost = collapse && pre.isEmpty && step.steps.length == 1
        def collapsed = step.steps(0).copy(name = name)
        if (step.failed) {

          if (collapseIfNoPost)
            addStep(collapsed)
          else
            addStep(History.parent(name, pre ++ step))

        } else {

          // TODO This didn't work out as planned - redo unpackChecks stuff
          var checksPostA, checksPostI = checksPre
          if (checksPre.coproductFound) {
            checksPostA = unpackChecks(Sack.empty, arounds, ros2.os)
            checksPostI = unpackChecks(invariants, Sack.empty, ros2.os)
          }

          // Post conditions
          val post1 = {
            val b = History.newBuilder[E](stats)
            checksPostA.errors foreach b.addNE
            b.addEach(hcs)(
              c => c.check.name)(Some(BeforeAfter(p.ros.os, ros2.os)),
              c => c.before.flatMap(a => Tri failedOption c.check.test(ros2.os, a).noCause)) // Perform around-post
            b.addEach(checksPostA.aftersA)(_.name)(ros2.sos, _.test(ros2.os).noCause) // Perform post
            b.group(PostName)
          }

          // Check invariants
          val invs = {
            val b = History.newBuilder[E](stats)
            checksPostI.errors foreach b.addNE
            b.addEach(checksPostI.aftersI)(_.name)(ros2.sos, _.test(ros2.os).noCause)
            b.group(InvariantsName)
          }

          val post = post1 ++ invs

          if (collapseIfNoPost && post.isEmpty)
            addStep(collapsed)
          else
            addStep(History.parent(name, pre ++ step ++ post))
        }
      }
    }
  }

  private def checkAround[A](name      : Name,
                             invariants: Invariants[O, S, E],
                             arounds   : Arounds[O, S, E],
                             collapse  : Boolean,
                             p         : P)
                            (prepare   : ROS => Option[A])
                            (run       : A => F[(Name => H, ROS)]): F[P] =
    prepare(p.ros) match {
      case Some(a) =>
        checkAround0(name, invariants, arounds, collapse, p, EM.map(run(a))(x => (x._1(ActionName), x._2)))
      case None =>
        EM.pure(p :+ History.Step(name, Skip))
    }

  private def subtest(test: Test,
                      nonInitialInvariants: Invariants[O, S, E],
                      refFn: () => R,
                      initROS: ROS,
                      summariseFinalResult: Boolean): F[P] = {

    def start(actions: Actions[F, R, O, S, E], ros: ROS, history: H = History.empty) =
      go(Progress.prepareNext(actions, ros, history))

    def go(p0: P): F[P] =
      EM.tailrec(p0)(_.queue.isEmpty) { p =>
        val queue = p.queue.get
        import p.ros

        val processNextStep: F[P] =
          queue.head match {
            case Right(Action.Outer(nameFn, innerAction, check)) =>

              val name = recover.name(nameFn, ros.some)

              if (p.failed)
                EM.pure(p :+ History.Step(name, Skip))

              else {

                val invariants = nonInitialInvariants & test.invariants
                innerAction match {

                  // ==============================================================================
                  case Action.Single(run) =>
                    checkAround(name, invariants, check, true, p)(run) { act =>

                      def ret(ros: ROS, r: Result[FE]): (Name => H, ROS) =
                        (n => History(vector1(History.Step(n, r))), ros)

                      def rets(ros: ROS, r: Result[FE], hs: History.Step[FE]): (Name => H, ROS) =
                        (n => History(vector1(History.Step(n, r)) :+ hs), ros)

                      stats.actions += 1
                      def act2 = EM.map(act())(_.leftMap(Failure NoCause _)): F[FE Or (O => E Or S)]
                      EM.map(EM.recover(act2)) {

                        case Right(nextStateFn) =>
                          val ref2 = refFn()
                          observe(test, ref2) match {
                            case Right(obs2) =>
                              recover.attempt(nextStateFn(obs2)) match {
                                case Right(Right(state2)) => ret(new ROS(ref2, obs2, state2), Pass)
                                case Right(Left(e))       => rets(ros, Pass, History.Step(Observation, Fail(Failure NoCause e)))
                                case Left(e)              => rets(ros, Pass, History.Step(UpdateState, Fail(e)))
                              }
                            case Left(e) =>
                              rets(ros, Pass, History.Step(Observation, Fail(e)))
                          }

                        case Left(e) => ret(ros, Fail(e))
                      }

                    }

                  // ==============================================================================
                  case Action.Group(actionFn) =>
                    checkAround(name, invariants, check, false, p)(actionFn)(children =>
                      EM.map(start(children, ros))(omgC =>
                        ((_: Name) => omgC.history, omgC.ros))
                    )

                  // ==============================================================================
                  case Action.SubTest(action, subInvariants) =>
                    val omg2F = checkAround0(name, Sack.empty, check, false, p.copy(history = History.empty), {
                      val t = Plan(action, subInvariants).test(test.observer)
                      val subP = subtest(t, invariants, refFn, ros, false)
                      EM.map(subP)(s => (s.history, s.ros))
                    })
                    EM.map(omg2F)(s => s.copy(history = p.history ++ s.history))
                }
            }

            case Left(NamedError(n, e)) =>
              EM.pure(p :+ History.Step(n, Fail(e)))
          }

        EM.map(processNextStep)(p => Progress.prepareNext(queue.tail, p.ros, p.history))
      }

    val finalResult: F[P] = {
      val ros = initROS

      val invariantsPoints = {
        val ps = new UniqueListBuilder[Point[OS[O, S], E]]
        val es = new UniqueListBuilder[NamedError[FE]]
        foreachSackE(test.invariants)(ros.os) {
          case Right(Invariant.Point(p)) => ps += p
          case Right(Invariant.Delta(_)) => ()
          case Left(e)                   => es += e
        }
        type T = CheckNE[Point, O, S, E]
        val pi = ps.iterator().map[T](Right(_))
        val ei = es.iterator().map[T](Left(_))
        (pi ++ ei).toList
      }

      val firstSteps: H =
        if (invariantsPoints.isEmpty)
          History.empty
        else {
          val children = {
            val b = History.newBuilder[E](stats)
            b.addEachNE(invariantsPoints)(_.name)(ros.sos, _.test(ros.os).noCause)
            b.history()
          }
          History(History.parent(InitialState, children))
        }

      val fh: F[P] =
        if (firstSteps.failed)
          EM.pure(Progress[F, R, O, S, E](None, ros, firstSteps))
        else
          start(test.actions, ros, firstSteps)

      EM.map(fh) { omg =>
        import omg.{history => h}

        // Summarise
        val h2: H =
          if (h.isEmpty)
            History(History.Step("Nothing to do.", Skip))
          else if (summariseFinalResult)
            h.result match {
              case Pass    => h :+ History.Step("All pass.", Pass)
              case Skip    => h :+ History.Step("All skipped.", Skip)
              case Fail(_) => h
            }
          else
            h

        omg.copy(history = h2)
      }
    }

    finalResult
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy