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

hedgehog.state.Action.scala Maven / Gradle / Ivy

The newest version!
package hedgehog.state

import hedgehog.{Result => _, _}
import hedgehog.core._
import hedgehog.predef._

import scala.concurrent._

/**
 * An instantiation of a 'Command' which can be executed, and its effect evaluated.
 */
trait Action[S] {

  type Input
  type Output

  def input: Input

  def output: Var[Output]

  def command: Command[S, Input, Output]
}

object Action {

  type ActionCheck[S] = State[(S, Environment), Result]

  def action[S, I, O](command: Command[S, I, O], context: Context[S]): StateT[GenT, Context[S], Option[Action[S]]] =
    for {
      input <- command.gen(context.state) match {
        case None =>
          sys.error("Command.gen: internal error, tried to use generator with invalid state.")
        case Some(nb) =>
          stateT.lift[Context[S], I](nb)
      }
      x <-
        if (!command.require(context.state, input)) {
          stateT[GenT].point[Context[S], Option[Action[S]]](None)
        } else {
          val (context2, output) = Context.newVar[S, O](context)
          val context3 = context2.copy(state = command.update(context.state, input, output))
          for {
            _ <- stateT[GenT].put(context3)
            y <- stateT[GenT].point[Context[S], Option[Action[S]]](Some(fromCommand(command, input, output)))
          } yield y
        }
    } yield x

  def action[S](commands: List[CommandIO[S]]): StateT[GenT, Context[S], Action[S]] =
    MonadGen[StateT[GenT, Context[S], *]]
      .fromSome(for {
        context <- stateT[GenT].get[Context[S]]
        cmd <- stateT[GenT].lift(Gen.elementUnsafe(commands.filter(command =>
            command.command.gen(context.state).isDefined
          )))
        a <- action(cmd.command, context)
        } yield a)

  def genActions[S](range: Range[Int], commands: List[CommandIO[S]], ctx: Context[S]): GenT[(Context[S], List[Action[S]])] =
    MonadGen[StateT[GenT, Context[S], *]]
      .list(action(commands), range).eval(ctx)
      .map(xs => dropInvalid(xs).run(ctx).value)

  /** Drops invalid actions from the sequence. */
  def dropInvalid[S](actions: List[Action[S]]): State[Context[S], List[Action[S]]] = {

    def loop(step: Action[S]): State[Context[S], Option[Action[S]]] =
      for {
        c <- State.get[Context[S]]
        x <-
          // Checking that the variables are OK ensures we don't shrink to a state that is not valid
          if (step.command.require(c.state, step.input) && Var.variablesOK(step.command.vars(step.input), c.vars)) {
            for {
              _ <- State.put[Context[S]](Context(
                step.command.update(c.state, step.input, step.output)
              , Var.insert(c.vars, step.output)
              ))
            } yield some(step)
          } else {
            State.point[Context[S], Option[Action[S]]](Option.empty[Action[S]])
          }
      } yield x

    State.traverse(actions)(loop).map(_.flatMap(_.toList))
  }

  def execute[S](action: Action[S]): StateT[Either[ExecutionError, *], Environment, ActionCheck[S]] =
    stateT[Either[ExecutionError, *]](env0 =>
      // Apologies, we're going to assume that environment variables are uncommon enough we don't want to force
      // users to have to pass back the exceptions, instead we'll catch them here. Dodgy.
      (try {
        action.command.execute(env0, action.input)
          .leftMap(ExecutionError.execute)
      } catch {
        case e: EnvironmentError =>
          Left(ExecutionError.environment(e))
        case e: Exception =>
          Left(ExecutionError.unknown(e))
      }).rightMap(output => {
        // NOTE: We need to update the environment in different contexts, once for the original execution
        // and then later again for linearization
        val env = Environment(env0.value + (action.output.name -> output))
        (env, State.state { case (s0, env1) =>
          val env2 = Environment(env1.value + (action.output.name -> output))
          val s = action.command.update(s0, action.input, action.output)
          ((s, env2), action.command.ensure(env2, s0, s, action.input, output))
        })
        }
      )
    )

  def executeUpdateEnsure[S](action: Action[S]): State[(S, Environment), Result] =
    stateT.state { case (state0, env0) =>
      execute(action).eval(env0).rightMap { check =>
        check.run((state0, env0)).value
      }.fold(e => ((state0, env0), Runner.executionErrorToResult(e)), identity)
    }

  def executeSequential[S](initial: S, actions: List[Action[S]]): Result = {
    def goActions: (Result, List[Action[S]]) => StateT[Identity, (S, Environment), Result] = {
      case (Result.Success, a :: as) => for {
        result <- executeUpdateEnsure(a)
        rest   <- goActions(result, as)
      } yield rest
      case (result, _) =>
        StateT.StateTMonad[Identity, (S, Environment)].point(result)
    }

    goActions(Result.Success, actions)
      .eval((initial, Environment(Map())))
      .value
  }


  /**
   * Given the initial model state and set of commands, generates prefix
   * actions to be run sequentially, followed by two branches to be run in
   * parallel.
   */
  def genParallel[S](prefixN: Range[Int], parallelN: Range[Int], initial: S, commands: List[CommandIO[S]]): Gen[Parallel[S]] =
    for {
      ctx0 <- genActions(prefixN, commands, Context.create(initial))
      ctx1 <- genActions(parallelN, commands, ctx0._1)
      ctx2 <- genActions(parallelN, commands, ctx1._1.copy(state = ctx0._1.state))
    } yield Parallel(ctx0._2, ctx1._2, ctx2._2)

  /**
   * Executes the prefix actions sequentially, then executes the two branches
   * in parallel, verifying that no exceptions are thrown and that there is at
   * least one sequential interleaving where all the post-conditions are met.
   *
   * To generate parallel actions to execute, see the 'Hedgehog.Gen.parallel'
   * combinator in the "Hedgehog.Gen" module.
   */
  def executeParallel[S](initial: S, parallel: Parallel[S])(implicit E: ExecutionContext): Future[Result] = {
    val (e, r) = stateT.traverse(parallel.prefix)(executeUpdateEnsure)
      .run((initial, Environment(Map())))
      .value
    Future(stateT[Either[ExecutionError, *]].traverse(parallel.branch1)(execute).eval(e._2))
      .zip(Future(stateT[Either[ExecutionError, *]].traverse(parallel.branch2)(execute).eval(e._2)))
      .map { case (xs, ys) =>
        Applicative.zip[Either[ExecutionError, *], List[ActionCheck[S]], List[ActionCheck[S]]](xs, ys)
          .fold[Result](
            e => Runner.executionErrorToResult(e)
          , x => Result.all(r).and(linearize(e._1, e._2, x._1, x._2))
          )
      }
  }

  def interleave[A](xs00: List[A], ys00: List[A]): List[List[A]] =
    (xs00, ys00) match {
      case (Nil, Nil) =>
        Nil
      case (xs, Nil) =>
        List(xs)
      case (Nil, ys) =>
        List(ys)
      case (xs0@x :: xs, ys0@y :: ys) =>
        List(
          interleave(xs, ys0).map(x :: _)
        , interleave(xs0, ys).map(y :: _)
        ).flatten
    }

  def linearize[S](initial: S, env: Environment, branch1: List[ActionCheck[S]], branch2: List[ActionCheck[S]]): Result =
    Result.any(
      interleave(branch1, branch2).zipWithIndex.map { case (bs, i) =>
        Result.all(
          State.traverse(bs)(identity).eval((initial, env)).value
        ).log(s"=== Counterexample ${i.toString} ===")
      }
    ).log("no valid interleaving")

  def fromCommand[S, I, O](c: Command[S, I, O], i: I, o: Var[O]): Action[S] =
    new Action[S] {
      override type Input = I
      override type Output = O
      override def input: Input = i
      override def output: Var[Output] = o
      override def command: Command[S, Input, Output] = c
    }
}

/**
 * A sequential prefix of actions to execute, with two branches to execute in parallel.
 */
case class Parallel[S](

    prefix: List[Action[S]]

  , branch1: List[Action[S]]

  , branch2: List[Action[S]]
  )

sealed trait ExecutionError extends Exception

object ExecutionError {

  case class Environment(e: EnvironmentError) extends ExecutionError

  case class Execute(e: String) extends ExecutionError

  case class Unknown(e: Exception) extends ExecutionError

  def environment(e: EnvironmentError): ExecutionError =
    Environment(e)

  def execute(e: String): ExecutionError =
    Execute(e)

  def unknown(e: Exception): ExecutionError =
    Unknown(e)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy