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

zio.mock.Expectation.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2019-2022 John A. De Goes and the ZIO Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package zio.mock

import zio.mock.Expectation.{And, Chain, Exactly, Or, Repeated}
import zio.mock.Result.{Fail, Succeed}
import zio.mock.internal.{ExpectationState, MockException, MockState, ProxyFactory}
import zio.stacktracer.TracingImplicits.disableAutoTrace
import zio.test.Assertion
import zio.{EnvironmentTag, IO, Trace, ULayer, URLayer, ZIO, ZLayer}

import scala.language.implicitConversions

/** An `Expectation[R]` is an immutable tree structure that represents expectations on environment `R`.
  */
sealed abstract class Expectation[R: EnvironmentTag] { self =>

  /** Operator alias for `and`.
    */
  def &&[R0: EnvironmentTag](that: Expectation[R0]): Expectation[R with R0] =
    and[R0](that)

  /** Operator alias for `or`.
    */
  def ||[R0: EnvironmentTag](that: Expectation[R0]): Expectation[R with R0] =
    or[R0](that)

  /** Operator alias for `andThen`.
    */
  def ++[R0: EnvironmentTag](that: Expectation[R0]): Expectation[R with R0] =
    andThen[R0](that)

  /** Compose two expectations, producing a new expectation to satisfy both.
    *
    * {{ val mockEnv = MockClock.sleep(equalTo(1.second)) and MockConsole.readLine(value("foo")) }}
    */
  def and[R0: EnvironmentTag](that: Expectation[R0]): Expectation[R with R0] =
    (self, that) match {
      case (And.Items(xs1), And.Items(xs2)) =>
        And(self.mock.compose ++ that.mock.compose)(xs1 ++ xs2).asInstanceOf[Expectation[R with R0]]
      case (And.Items(xs), _)               =>
        And(self.mock.compose ++ that.mock.compose)(xs :+ that).asInstanceOf[Expectation[R with R0]]
      case (_, And.Items(xs))               =>
        And(self.mock.compose ++ that.mock.compose)(self :: xs).asInstanceOf[Expectation[R with R0]]
      case _                                => And(self.mock.compose ++ that.mock.compose)(self :: that :: Nil).asInstanceOf[Expectation[R with R0]]
    }

  /** Compose two expectations, producing a new expectation to satisfy both sequentially.
    *
    * {{ val mockEnv = MockClock.sleep(equalTo(1.second)) andThen MockConsole.readLine(value("foo")) }}
    */
  def andThen[R0: EnvironmentTag](that: Expectation[R0]): Expectation[R with R0] =
    (self, that) match {
      case (Chain.Items(xs1), Chain.Items(xs2)) =>
        Chain(self.mock.compose ++ that.mock.compose)(xs1 ++ xs2).asInstanceOf[Expectation[R with R0]]
      case (Chain.Items(xs), _)                 =>
        Chain(self.mock.compose ++ that.mock.compose)(xs :+ that).asInstanceOf[Expectation[R with R0]]
      case (_, Chain.Items(xs))                 =>
        Chain(self.mock.compose ++ that.mock.compose)(self :: xs).asInstanceOf[Expectation[R with R0]]
      case _                                    => Chain(self.mock.compose ++ that.mock.compose)(self :: that :: Nil).asInstanceOf[Expectation[R with R0]]
    }

  /** Lower-bounded variant of `repeated`, produces a new expectation to satisfy itself sequentially at least given
    * number of times.
    */
  def atLeast(min: Int): Expectation[R] =
    Repeated(self, min to -1)

  /** Upper-bounded variant of `repeated`, produces a new expectation to satisfy itself sequentially at most given
    * number of times.
    */
  def atMost(max: Int): Expectation[R] =
    Repeated(self, 0 to max)

  /** Alias for `atMost(1)`, produces a new expectation to satisfy itself at most once.
    */
  def optional: Expectation[R] =
    atMost(1)

  /** Produces a new expectation to satisfy itself exactly the given number of times.
    */
  def exactly(times: Int): Expectation[R] =
    Exactly(self, times)

  /** Alias for `exactly(2)`, produces a new expectation to satisfy itself exactly two times.
    */
  def twice: Expectation[R] =
    exactly(2)

  /** Alias for `exactly(3)`, produces a new expectation to satisfy itself exactly three times.
    */
  def thrice: Expectation[R] =
    exactly(3)

  /** Compose two expectations, producing a new expectation to satisfy one of them.
    *
    * {{ val mockEnv = MockClock.sleep(equalTo(1.second)) or MockConsole.readLine(value("foo")) }}
    */
  def or[R0: EnvironmentTag](that: Expectation[R0]): Expectation[R with R0] =
    (self, that) match {
      case (Or.Items(xs1), Or.Items(xs2)) =>
        Or(self.mock.compose ++ that.mock.compose)(xs1 ++ xs2).asInstanceOf[Expectation[R with R0]]
      case (Or.Items(xs), _)              =>
        Or(self.mock.compose ++ that.mock.compose)(xs :+ that).asInstanceOf[Expectation[R with R0]]
      case (_, Or.Items(xs))              =>
        Or(self.mock.compose ++ that.mock.compose)(self :: xs).asInstanceOf[Expectation[R with R0]]
      case _                              => Or(self.mock.compose ++ that.mock.compose)(self :: that :: Nil).asInstanceOf[Expectation[R with R0]]
    }

  /** Repeats this expectation within given bounds, producing a new expectation to satisfy itself sequentially given
    * number of times.
    *
    * {{{val mockEnv = MockClock.sleep(equalTo(1.second)).repeats(1, 5)}}}
    *
    * NOTE: once another repetition starts executing, it must be completed in order to satisfy the composite
    * expectation. For example (A ++ B).repeats(1, 2) will be satisfied by either A->B (one repetition) or A->B->A->B
    * (two repetitions), but will fail on A->B->A (incomplete second repetition).
    */
  def repeats(range: Range): Expectation[R] =
    Repeated(self, range)

  /** Converts this expectation to ZLayer.
    */
  def toLayer(implicit trace: Trace): ULayer[R] = Expectation.toLayer(self)

  /** Invocations log.
    */
  private[mock] val invocations: List[Int]

  /** Environment to which expectation belongs.
    */
  private[mock] val mock: Mock[R]

  /** Mock execution state.
    */
  private[mock] val state: ExpectationState
}

object Expectation {

  import ExpectationState._

  /** Models expectations conjunction on environment `R`. Expectations are checked in the order they are provided,
    * meaning that earlier expectations may shadow later ones.
    */
  private[mock] case class And[R: EnvironmentTag](
      children: List[Expectation[R]],
      state: ExpectationState,
      invocations: List[Int],
      mock: Mock.Composed[R]
  ) extends Expectation[R]

  private[mock] object And {

    def apply[R: EnvironmentTag](compose: URLayer[Proxy, R])(children: List[Expectation[_]]): And[R] =
      And(
        children.asInstanceOf[List[Expectation[R]]],
        if (children.exists(_.state.isFailed)) Unsatisfied else Satisfied,
        List.empty,
        Mock.Composed(compose)
      )

    object Items {

      private[mock] def unapply[R](and: And[R]): Option[(List[Expectation[R]])] =
        Some(and.children)
    }
  }

  /** Models a call in environment `R` that takes input arguments `I` and returns an effect that may fail with an error
    * `E` or produce a single `A`.
    */
  private[mock] case class Call[R: EnvironmentTag, I, E, A](
      capability: Capability[R, I, E, A],
      assertion: Assertion[I],
      returns: I => IO[E, A],
      state: ExpectationState,
      invocations: List[Int]
  ) extends Expectation[R] {
    val mock: Mock[R] = capability.mock
  }

  private[mock] object Call {

    def apply[R: EnvironmentTag, I, E, A](
        capability: Capability[R, I, E, A],
        assertion: Assertion[I],
        returns: I => IO[E, A]
    ): Call[R, I, E, A] =
      Call(capability, assertion, returns, Unsatisfied, List.empty)
  }

  /** Models sequential expectations on environment `R`.
    */
  private[mock] case class Chain[R: EnvironmentTag](
      children: List[Expectation[R]],
      state: ExpectationState,
      invocations: List[Int],
      mock: Mock.Composed[R]
  ) extends Expectation[R]

  private[mock] object Chain {

    def apply[R: EnvironmentTag](compose: URLayer[Proxy, R])(children: List[Expectation[_]]): Chain[R] =
      Chain(
        children.asInstanceOf[List[Expectation[R]]],
        if (children.exists(_.state.isFailed)) Unsatisfied else Satisfied,
        List.empty,
        Mock.Composed(compose)
      )

    object Items {

      private[mock] def unapply[R](chain: Chain[R]): Option[(List[Expectation[R]])] =
        Some(chain.children)
    }
  }

  private[mock] case class NoCalls[R: EnvironmentTag](mock: Mock[R]) extends Expectation[R] {

    override private[mock] val invocations: List[Int] = Nil

    override private[mock] val state: ExpectationState = Satisfied

  }

  /** Models expectations disjunction on environment `R`. Expectations are checked in the order they are provided,
    * meaning that earlier expectations may shadow later ones.
    */
  private[mock] case class Or[R: EnvironmentTag](
      children: List[Expectation[R]],
      state: ExpectationState,
      invocations: List[Int],
      mock: Mock.Composed[R]
  ) extends Expectation[R]

  private[mock] object Or {

    def apply[R: EnvironmentTag](compose: URLayer[Proxy, R])(children: List[Expectation[_]]): Or[R] =
      Or(
        children.asInstanceOf[List[Expectation[R]]],
        if (children.exists(_.state == Satisfied)) Satisfied else Unsatisfied,
        List.empty,
        Mock.Composed(compose)
      )

    object Items {

      private[mock] def unapply[R](or: Or[R]): Option[(List[Expectation[R]])] =
        Some(or.children)
    }
  }

  /** Models expectation repetition on environment `R`.
    */
  private[mock] final case class Repeated[R: EnvironmentTag](
      child: Expectation[R],
      range: Range,
      state: ExpectationState,
      invocations: List[Int],
      started: Int,
      completed: Int
  ) extends Expectation[R] {
    val mock: Mock[R] = child.mock
  }

  private[mock] object Repeated {

    def apply[R: EnvironmentTag](child: Expectation[R], range: Range): Repeated[R] =
      if (range.step <= 0) throw new IllegalArgumentException(range.toString())
      else Repeated(child, range, if (range.start == 0) Satisfied else Unsatisfied, List.empty, 0, 0)
  }

  /** Models expectation exactitude on environment `R`.
    */
  private[mock] final case class Exactly[R: EnvironmentTag](
      child: Expectation[R],
      times: Int,
      state: ExpectationState,
      invocations: List[Int],
      completed: Int
  ) extends Expectation[R] {
    val mock: Mock[R] = child.mock
  }

  private[mock] object Exactly {

    def apply[R: EnvironmentTag](child: Expectation[R], times: Int): Exactly[R] =
      Exactly(child, times, if (times == 0) Saturated else Unsatisfied, List.empty, 0)
  }

  /** Expectation result failing with `E`.
    */
  def failure[E](failure: E)(implicit trace: Trace): Fail[Any, E] = Fail(_ => ZIO.fail(failure))

  /** Maps the input arguments `I` to expectation result failing with `E`.
    */
  def failureF[I, E](f: I => E)(implicit trace: Trace): Fail[I, E] = Fail(i => ZIO.succeed(i).map(f).flip)

  /** Effectfully maps the input arguments `I` to expectation result failing with `E`.
    */
  def failureZIO[I, E](f: I => IO[E, Nothing])(implicit trace: Trace): Fail[I, E] = Fail(f)

  /** Expectation result computing forever.
    */
  def never(implicit trace: Trace): Succeed[Any, Nothing] = valueZIO(_ => ZIO.never)

  /** Expectation result succeeding with `Unit`.
    */
  def unit(implicit trace: Trace): Succeed[Any, Unit] = value(())

  /** Expectation result succeeding with `A`.
    */
  def value[A](value: A)(implicit trace: Trace): Succeed[Any, A] = Succeed(_ => ZIO.succeed(value))

  /** Maps the input arguments `I` to expectation result succeeding with `A`.
    */
  def valueF[I, A](f: I => A)(implicit trace: Trace): Succeed[I, A] = Succeed(i => ZIO.succeed(i).map(f))

  /** Effectfully maps the input arguments `I` expectation result succeeding with `A`.
    */
  def valueZIO[I, A](f: I => IO[Nothing, A]): Succeed[I, A] = Succeed(f)

  /** Implicitly converts Expectation to ZLayer mock environment.
    */
  implicit def toLayer[R: EnvironmentTag](
      trunk: Expectation[R]
  )(implicit trace: Trace): ULayer[R] =
    ZLayer.scopedEnvironment(
      for {
        state <- ZIO.acquireRelease(MockState.make(trunk))(MockState.checkUnmetExpectations)
        env   <- (ProxyFactory.mockProxy(state) >>> trunk.mock.compose).build
      } yield env
    )
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy