Skip to content

Commit

Permalink
Explore only previously matched branch when visiting partially satisf…
Browse files Browse the repository at this point in the history
…ied OR expectation (#3904)

* Explore only partially satisfied branches in OR nodes.

* Fix ScalaJS

* Remove garbage file

* Use bool value for debug toggle
  • Loading branch information
ioleo committed Jun 29, 2020
1 parent 069d7b8 commit 0e9db93
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 126 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ object AdvancedEffectMockSpec extends ZIOBaseSpec with MockSpecUtils[PureModule]
testValue("A passes")(A || B || C, a, equalTo("A")),
testValue("B passes")(A || B || C, b, equalTo("B")),
testValue("C passes")(A || B || C, c, equalTo("C"))
),
suite("(A andThen B) or (B andThen A)")(
testValue("A->B passes")((A ++ B) || (B ++ A), a *> b, equalTo("B")),
testValue("B->A passes")((A ++ B) || (B ++ A), b *> a, equalTo("A"))
), {
val expectation = A repeats (1 to 3)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package zio.test.mock

import zio.duration._
import zio.test.mock.internal.{ InvalidCall, MockException }
import zio.test.mock.internal.{ ExpectationState, InvalidCall, MockException }
import zio.test.mock.module.{ PureModule, PureModuleMock }
import zio.test.{ suite, Assertion, ZIOBaseSpec }
import zio.{ IO, UIO }
Expand All @@ -10,6 +10,7 @@ object BasicEffectMockSpec extends ZIOBaseSpec with MockSpecUtils[PureModule] {

import Assertion._
import Expectation._
import ExpectationState._
import InvalidCall._
import MockException._

Expand Down Expand Up @@ -412,11 +413,10 @@ object BasicEffectMockSpec extends ZIOBaseSpec with MockSpecUtils[PureModule] {

def cmd(n: Int) = PureModuleMock.ParameterizedCommand(equalTo(n))

def hasCall(index: Int, satisfied: Boolean, saturated: Boolean, invocations: List[Int]) =
def hasCall(index: Int, state: ExpectationState, invocations: List[Int]) =
hasAt(index)(
isSubtype[E1](
hasField[E1, Boolean]("satisfied", _.satisfied, equalTo(satisfied)) &&
hasField[E1, Boolean]("saturated", _.saturated, equalTo(saturated)) &&
hasField[E1, ExpectationState]("state", _.state, equalTo(state)) &&
hasField[E1, List[Int]]("invocations", _.invocations, equalTo(invocations))
)
)
Expand All @@ -433,13 +433,12 @@ object BasicEffectMockSpec extends ZIOBaseSpec with MockSpecUtils[PureModule] {
"children",
_.children,
isSubtype[L](
hasCall(0, true, true, List(1)) &&
hasCall(1, false, false, List.empty) &&
hasCall(2, false, false, List.empty)
hasCall(0, Saturated, List(1)) &&
hasCall(1, Unsatisfied, List.empty) &&
hasCall(2, Unsatisfied, List.empty)
)
) &&
hasField[E0, Boolean]("satisfied", _.satisfied, equalTo(false)) &&
hasField[E0, Boolean]("saturated", _.saturated, equalTo(false)) &&
hasField[E0, ExpectationState]("state", _.state, equalTo(PartiallySatisfied)) &&
hasField[E0, List[Int]]("invocations", _.invocations, equalTo(List(1)))
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package zio.test.mock

import zio.test.mock.internal.{ InvalidCall, MockException }
import zio.test.mock.internal.{ ExpectationState, InvalidCall, MockException }
import zio.test.mock.module.{ ImpureModule, ImpureModuleMock }
import zio.test.{ suite, Assertion, ZIOBaseSpec }
import zio.{ IO, UIO }
Expand All @@ -9,6 +9,7 @@ object BasicMethodMockSpec extends ZIOBaseSpec with MockSpecUtils[ImpureModule]

import Assertion._
import Expectation._
import ExpectationState._
import InvalidCall._
import MockException._

Expand Down Expand Up @@ -409,11 +410,10 @@ object BasicMethodMockSpec extends ZIOBaseSpec with MockSpecUtils[ImpureModule]

def cmd(n: Int) = ImpureModuleMock.ParameterizedCommand(equalTo(n))

def hasCall(index: Int, satisfied: Boolean, saturated: Boolean, invocations: List[Int]) =
def hasCall(index: Int, state: ExpectationState, invocations: List[Int]) =
hasAt(index)(
isSubtype[E1](
hasField[E1, Boolean]("satisfied", _.satisfied, equalTo(satisfied)) &&
hasField[E1, Boolean]("saturated", _.saturated, equalTo(saturated)) &&
hasField[E1, ExpectationState]("state", _.state, equalTo(state)) &&
hasField[E1, List[Int]]("invocations", _.invocations, equalTo(invocations))
)
)
Expand All @@ -430,13 +430,12 @@ object BasicMethodMockSpec extends ZIOBaseSpec with MockSpecUtils[ImpureModule]
"children",
_.children,
isSubtype[L](
hasCall(0, true, true, List(1)) &&
hasCall(1, false, false, List.empty) &&
hasCall(2, false, false, List.empty)
hasCall(0, Saturated, List(1)) &&
hasCall(1, Unsatisfied, List.empty) &&
hasCall(2, Unsatisfied, List.empty)
)
) &&
hasField[E0, Boolean]("satisfied", _.satisfied, equalTo(false)) &&
hasField[E0, Boolean]("saturated", _.saturated, equalTo(false)) &&
hasField[E0, ExpectationState]("state", _.state, equalTo(PartiallySatisfied)) &&
hasField[E0, List[Int]]("invocations", _.invocations, equalTo(List(1)))
)
)
Expand Down
14 changes: 7 additions & 7 deletions test/shared/src/main/scala/zio/test/DefaultTestReporter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -401,27 +401,27 @@ object FailureRenderer {
case Nil =>
lines

case (ident, Expectation.And(children, false, _, _, _)) :: tail =>
case (ident, Expectation.And(children, state, _, _)) :: tail if state.isFailed =>
val title = Line.fromString("in any order", ident)
val unsatisfied = children.filter(!_.satisfied).map(ident + tabSize -> _)
val unsatisfied = children.filter(_.state.isFailed).map(ident + tabSize -> _)
loop(unsatisfied ++ tail, lines :+ title)

case (ident, Expectation.Call(method, assertion, _, false, _, _)) :: tail =>
case (ident, Expectation.Call(method, assertion, _, state, _)) :: tail if state.isFailed =>
val rendered =
withOffset(ident)(Fragment(s"$method with arguments ") + cyan(assertion.toString))
loop(tail, lines :+ rendered)

case (ident, Expectation.Chain(children, false, _, _, _)) :: tail =>
case (ident, Expectation.Chain(children, state, _, _)) :: tail if state.isFailed =>
val title = Line.fromString("in sequential order", ident)
val unsatisfied = children.filter(!_.satisfied).map(ident + tabSize -> _)
val unsatisfied = children.filter(_.state.isFailed).map(ident + tabSize -> _)
loop(unsatisfied ++ tail, lines :+ title)

case (ident, Expectation.Or(children, false, _, _, _)) :: tail =>
case (ident, Expectation.Or(children, state, _, _)) :: tail if state.isFailed =>
val title = Line.fromString("one of", ident)
val unsatisfied = children.map(ident + tabSize -> _)
loop(unsatisfied ++ tail, lines :+ title)

case (ident, Expectation.Repeated(child, range, false, _, _, _, completed)) :: tail =>
case (ident, Expectation.Repeated(child, range, state, _, _, completed)) :: tail if state.isFailed =>
val min = Try(range.min.toString).getOrElse("0")
val max = Try(range.max.toString).getOrElse("")
val title =
Expand Down
41 changes: 16 additions & 25 deletions test/shared/src/main/scala/zio/test/mock/Expectation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import scala.language.implicitConversions
import zio.test.Assertion
import zio.test.mock.Expectation.{ And, Chain, Or, Repeated }
import zio.test.mock.Result.{ Fail, Succeed }
import zio.test.mock.internal.{ MockException, ProxyFactory, State }
import zio.test.mock.internal.{ ExpectationState, MockException, MockState, ProxyFactory }
import zio.{ Has, IO, Managed, Tag, ULayer, URLayer, ZLayer }

/**
Expand Down Expand Up @@ -149,35 +149,30 @@ sealed abstract class Expectation[R <: Has[_]: Tag] { self =>
private[test] val mock: Mock[R]

/**
* Mock execution flag.
* Mock execution state.
*/
private[test] val satisfied: Boolean

/**
* Short-circuit flag. If an expectation has been saturated
* it's branch will be skipped in the invocation search.
*/
private[test] val saturated: Boolean
private[test] 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[test] case class And[R <: Has[_]: Tag](
children: List[Expectation[R]],
satisfied: Boolean,
saturated: Boolean,
state: ExpectationState,
invocations: List[Int],
mock: Mock.Composed[R]
) extends Expectation[R]

private[test] object And {

def apply[R <: Has[_]: Tag](compose: URLayer[Has[Proxy], R])(children: List[Expectation[_]]): And[R] =
And(children.asInstanceOf[List[Expectation[R]]], false, false, List.empty, Mock.Composed(compose))
And(children.asInstanceOf[List[Expectation[R]]], Unsatisfied, List.empty, Mock.Composed(compose))

object Items {

Expand All @@ -194,8 +189,7 @@ object Expectation {
capability: Capability[R, I, E, A],
assertion: Assertion[I],
returns: I => IO[E, A],
satisfied: Boolean,
saturated: Boolean,
state: ExpectationState,
invocations: List[Int]
) extends Expectation[R] {
val mock: Mock[R] = capability.mock
Expand All @@ -208,24 +202,23 @@ object Expectation {
assertion: Assertion[I],
returns: I => IO[E, A]
): Call[R, I, E, A] =
Call(capability, assertion, returns, false, false, List.empty)
Call(capability, assertion, returns, Unsatisfied, List.empty)
}

/**
* Models sequential expectations on environment `R`.
*/
private[test] case class Chain[R <: Has[_]: Tag](
children: List[Expectation[R]],
satisfied: Boolean,
saturated: Boolean,
state: ExpectationState,
invocations: List[Int],
mock: Mock.Composed[R]
) extends Expectation[R]

private[test] object Chain {

def apply[R <: Has[_]: Tag](compose: URLayer[Has[Proxy], R])(children: List[Expectation[_]]): Chain[R] =
Chain(children.asInstanceOf[List[Expectation[R]]], false, false, List.empty, Mock.Composed(compose))
Chain(children.asInstanceOf[List[Expectation[R]]], Unsatisfied, List.empty, Mock.Composed(compose))

object Items {

Expand All @@ -240,16 +233,15 @@ object Expectation {
*/
private[test] case class Or[R <: Has[_]: Tag](
children: List[Expectation[R]],
satisfied: Boolean,
saturated: Boolean,
state: ExpectationState,
invocations: List[Int],
mock: Mock.Composed[R]
) extends Expectation[R]

private[test] object Or {

def apply[R <: Has[_]: Tag](compose: URLayer[Has[Proxy], R])(children: List[Expectation[_]]): Or[R] =
Or(children.asInstanceOf[List[Expectation[R]]], false, false, List.empty, Mock.Composed(compose))
Or(children.asInstanceOf[List[Expectation[R]]], Unsatisfied, List.empty, Mock.Composed(compose))

object Items {

Expand All @@ -264,8 +256,7 @@ object Expectation {
private[test] final case class Repeated[R <: Has[_]: Tag](
child: Expectation[R],
range: Range,
satisfied: Boolean,
saturated: Boolean,
state: ExpectationState,
invocations: List[Int],
started: Int,
completed: Int
Expand All @@ -277,7 +268,7 @@ object Expectation {

def apply[R <: Has[_]: Tag](child: Expectation[R], range: Range): Repeated[R] =
if (range.step <= 0) throw MockException.InvalidRangeException(range)
else Repeated(child, range, range.start == 0, false, List.empty, 0, 0)
else Repeated(child, range, if (range.start == 0) Satisfied else Unsatisfied, List.empty, 0, 0)
}

/**
Expand Down Expand Up @@ -326,7 +317,7 @@ object Expectation {
implicit def toLayer[R <: Has[_]: Tag](trunk: Expectation[R]): ULayer[R] =
ZLayer.fromManagedMany(
for {
state <- Managed.make(State.make(trunk))(State.checkUnmetExpectations)
state <- Managed.make(MockState.make(trunk))(MockState.checkUnmetExpectations)
env <- (ProxyFactory.mockProxy(state) >>> trunk.mock.compose).build
} yield env
)
Expand Down
78 changes: 78 additions & 0 deletions test/shared/src/main/scala/zio/test/mock/internal/Debug.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2017-2020 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.test.mock.internal

import zio.Has
import zio.test.mock.Expectation

private[mock] object Debug {

/**
* To see mock debug output during test execution, flip this flag to `true`.
*/
final val enabled = false

def debug(message: => String): Unit =
if (enabled) println(message)

def prettify[R <: Has[_]](expectation: Expectation[R], identSize: Int = 1): String = {
val ident = " " * 4 * identSize
val state = s"state = ${expectation.state}"
val invoked = s"""invocations = [${expectation.invocations.mkString(", ")}]"""

def renderRoot(name: String, children: List[Expectation[R]]): String = {
val header = (s"$name(" :: s"$state," :: s"$invoked," :: Nil).mkString(s"\n$ident")
val content = renderChildren(children).mkString("\n")
val prevIdent = " " * 4 * (identSize - 1)
s"$header,\n$content\n$prevIdent)"
}

def renderChildren(list: List[Expectation[R]]): List[String] =
list.map { child =>
val rendered = prettify(child, identSize + 1)
s"$ident$rendered"
}

expectation match {
case Expectation.Call(capability, assertion, _, _, _) =>
s"Call($state, $invoked, $capability, $assertion)"
case Expectation.And(children, _, _, _) =>
renderRoot("And", children)
case Expectation.Chain(children, _, _, _) =>
renderRoot("Chain", children)
case Expectation.Or(children, _, _, _) =>
renderRoot("Or", children)
case Expectation.Repeated(child, range, _, _, started, completed) =>
val progress = s"progress = $started out of $completed,"
("Repeated(" :: state :: s"range = $range," :: progress :: invoked :: prettify(child) :: ")" :: Nil)
.mkString(s"\n$ident")
}
}

def prettify[R <: Has[_]](scopes: List[Scope[R]]): String =
scopes.map {
case Scope(expectation, id, _) =>
val rendered = prettify(expectation)
s">>>\nInvocation ID: $id\n$rendered"
} match {
case Nil => ""
case head :: Nil => s"[Head]:\n$head"
case head :: tail =>
val renderedTail = tail.mkString("\n")
s"[Head]:\n$head\n[Tail]:\n$renderedTail"
}
}

0 comments on commit 0e9db93

Please sign in to comment.