Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explore only previously matched branch when visiting partially satisfied OR expectation #3904

Merged
merged 4 commits into from
Jun 29, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
34 changes: 34 additions & 0 deletions test/shared/src/main/scala/zio/test/mock/DebugProperties.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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

/**
* Java properties debug output of ZIO Mock framework
*
* @see [[DebugProperties]]
*/
object DebugProperties {

/**
* To see mock debug output during test execution, set `-Dzio.test.mock.debug=true` system property!
*
* {{{
* sbt -Dzio.test.mock.debug=true test
* }}}
*/
final val `zio.test.mock.debug` = "zio.test.mock.debug"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible, can we configure this through a TestAspect? We don't normally use system properties for anything in the library.

Copy link
Member Author

@ioleo ioleo Jun 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would require threading the TestAspect information to mock execution, not sure if possible without major refactoring.
It apprears System.properties fails to link on ScalaJS, so I switched to env variable instead.

This debug information is not really intended for the end user, but to library developers when extending/debugging the Mocking framework itself.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this supposed to be a user facing feature at all? Maybe it should just be a private val that contributors can set to true for debugging purposes when developing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, that will work too. Updated code.

}
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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 scala.math.Ordering

/**
* A `ExpectationState` represents the state of an expectation tree branch.
*/
private[test] sealed abstract class ExpectationState(val value: Int) extends Ordered[ExpectationState] {
def compare(that: ExpectationState) = Ordering.Int.compare(this.value, that.value)

lazy val isFailed: Boolean = this < ExpectationState.Satisfied
}

private[test] object ExpectationState {

/**
* Expectation that has yet to be satisfied by invocations.
*
* The test will fail, if it ends with expectation in this state.
*/
case object Unsatisfied extends ExpectationState(0)

/**
* Expectation that has been partially satisfied (meaning, there is a chained
* expectation that has not yet completed).
*
* The test will fail, if it ends with expectation in this state.
*/
case object PartiallySatisfied extends ExpectationState(1)

/**
* Expectation that has been satisfied, but could potentially match further calls.
*
* The test will succeed, if it ends with expectation in this state.
*/
case object Satisfied extends ExpectationState(2)

/**
* Expectation that has been satisfied and saturated - it cannot match further calls.
* Will short-circuit and skip ahead to the next expectation when looking for matches.
*
* The test will succeed, if it ends with expectation in this state.
*/
case object Saturated extends ExpectationState(3)
}