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

zio.test.TestAspect.scala Maven / Gradle / Ivy

There is a newer version: 2.1.11
Show newest version
/*
 * Copyright 2019-2024 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

import zio._
import zio.stacktracer.TracingImplicits.disableAutoTrace

import java.util.concurrent.atomic.AtomicReference
import scala.collection.immutable.SortedSet
import zio.test.TestAspectPoly
import zio.System.env
import zio.test.TestAspectAtLeastR

/**
 * A `TestAspect` is an aspect that can be weaved into specs. You can think of
 * an aspect as a polymorphic function, capable of transforming one test into
 * another, possibly enlarging the environment or error type.
 */
abstract class TestAspect[+LowerR, -UpperR, +LowerE, -UpperE] { self =>

  /**
   * Applies the aspect to some tests in the spec, chosen by the provided
   * predicate.
   */
  def some[R >: LowerR <: UpperR, E >: LowerE <: UpperE](spec: Spec[R, E])(implicit trace: Trace): Spec[R, E]

  /**
   * An alias for [[all]].
   */
  final def apply[R >: LowerR <: UpperR, E >: LowerE <: UpperE](spec: Spec[R, E])(implicit
    trace: Trace
  ): Spec[R, E] =
    all(spec)

  /**
   * Applies the aspect to every test in the spec.
   */
  final def all[R >: LowerR <: UpperR, E >: LowerE <: UpperE](spec: Spec[R, E])(implicit
    trace: Trace
  ): Spec[R, E] =
    some[R, E](spec)

  /**
   * Returns a new aspect that represents the sequential composition of this
   * aspect with the specified one.
   */
  final def >>>[LowerR1 >: LowerR, UpperR1 <: UpperR, LowerE1 >: LowerE, UpperE1 <: UpperE](
    that: TestAspect[LowerR1, UpperR1, LowerE1, UpperE1]
  ): TestAspect[LowerR1, UpperR1, LowerE1, UpperE1] =
    new TestAspect[LowerR1, UpperR1, LowerE1, UpperE1] {
      def some[R >: LowerR1 <: UpperR1, E >: LowerE1 <: UpperE1](
        spec: Spec[R, E]
      )(implicit trace: Trace): Spec[R, E] =
        that.some(self.some(spec))
    }

  /**
   * Returns a new aspect that represents the sequential composition of this
   * aspect with the specified one.
   */
  final def @@[LowerR1 >: LowerR, UpperR1 <: UpperR, LowerE1 >: LowerE, UpperE1 <: UpperE](
    that: TestAspect[LowerR1, UpperR1, LowerE1, UpperE1]
  ): TestAspect[LowerR1, UpperR1, LowerE1, UpperE1] =
    self >>> that

  final def andThen[LowerR1 >: LowerR, UpperR1 <: UpperR, LowerE1 >: LowerE, UpperE1 <: UpperE](
    that: TestAspect[LowerR1, UpperR1, LowerE1, UpperE1]
  ): TestAspect[LowerR1, UpperR1, LowerE1, UpperE1] =
    self >>> that
}
object TestAspect extends TimeoutVariants {

  type CheckAspect = ZIOAspect[Nothing, Any, Nothing, Any, TestResult, TestResult]

  /**
   * An aspect that returns the tests unchanged
   */
  val identity: TestAspectPoly =
    new TestAspectPoly {
      def some[R, E](spec: Spec[R, E])(implicit trace: Trace): Spec[R, E] =
        spec
    }

  /**
   * An aspect that marks tests as ignored.
   */
  val ignore: TestAspectPoly =
    new TestAspectPoly {
      def some[R, E](spec: Spec[R, E])(implicit trace: Trace): Spec[R, E] =
        spec.when(false)
    }

  /**
   * Constructs an aspect that runs the specified effect after every test.
   */
  def after[R0, E0](effect: ZIO[R0, E0, Any]): TestAspect[Nothing, R0, E0, Any] =
    new TestAspect.PerTest[Nothing, R0, E0, Any] {
      def perTest[R <: R0, E >: E0](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] =
        test.exit
          .zipWith(effect.catchAllCause(cause => ZIO.fail(TestFailure.Runtime(cause))).exit)(_ <* _)
          .flatMap(ZIO.done(_))
    }

  /**
   * Constructs an aspect that runs the specified effect after all tests.
   */
  def afterAll[R0](effect: ZIO[R0, Nothing, Any]): TestAspect[Nothing, R0, Nothing, Any] =
    aroundAll(ZIO.unit, effect)

  /**
   * Constructs an aspect that runs the specified effect after all tests if
   * there is at least one failure.
   */
  def afterAllFailure[R0](f: ZIO[R0, Nothing, Any]): TestAspect[Nothing, R0, Nothing, Any] =
    new TestAspect[Nothing, R0, Nothing, Any] {
      def some[R <: R0, E](spec: Spec[R, E])(implicit trace: Trace): Spec[R, E] =
        Spec.scoped[R](
          Ref.make(false).flatMap { failure =>
            ZIO.acquireRelease(ZIO.unit)(_ => f.whenZIO(failure.get)).as(afterFailure(failure.set(true))(spec))
          }
        )
    }

  /**
   * Constructs an aspect that runs the specified effect after all tests if
   * there are no failures.
   */
  def afterAllSuccess[R0](f: ZIO[R0, Nothing, Any]): TestAspect[Nothing, R0, Nothing, Any] =
    new TestAspect[Nothing, R0, Nothing, Any] {
      def some[R <: R0, E](spec: Spec[R, E])(implicit trace: Trace): Spec[R, E] =
        Spec.scoped[R](
          Ref.make(true).flatMap { success =>
            ZIO.acquireRelease(ZIO.unit)(_ => f.whenZIO(success.get)).as(afterFailure(success.set(false))(spec))
          }
        )
    }

  /**
   * Constructs an aspect that runs the specified effect after every failed
   * test.
   */
  def afterFailure[R0, E0](effect: ZIO[R0, E0, Any]): TestAspect[Nothing, R0, E0, Any] =
    new TestAspect.PerTest[Nothing, R0, E0, Any] {
      def perTest[R <: R0, E >: E0](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] =
        test.tapErrorCause(_ => effect.catchAllCause(cause => ZIO.fail(TestFailure.Runtime(cause))))
    }

  /**
   * Constructs an aspect that runs the specified effect after every successful
   * test.
   */
  def afterSuccess[R0, E0](effect: ZIO[R0, E0, Any]): TestAspect[Nothing, R0, E0, Any] =
    new TestAspect.PerTest[Nothing, R0, E0, Any] {
      def perTest[R <: R0, E >: E0](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] =
        test.tap(_ => effect.catchAllCause(cause => ZIO.fail(TestFailure.Runtime(cause))))
    }

  /**
   * Annotates tests with the specified test annotation.
   */
  def annotate[V](key: TestAnnotation[V], value: V): TestAspectPoly =
    new TestAspectPoly {
      def some[R, E](spec: Spec[R, E])(implicit trace: Trace): Spec[R, E] =
        spec.annotate(key, value)
    }

  /**
   * Constructs an aspect that evaluates every test between two effects,
   * `before` and `after`, where the result of `before` can be used in `after`.
   */
  def aroundWith[R0, E0, A0](
    before: ZIO[R0, E0, A0]
  )(after: A0 => ZIO[R0, Nothing, Any]): TestAspect[Nothing, R0, E0, Any] =
    new TestAspect.PerTest[Nothing, R0, E0, Any] {
      def perTest[R <: R0, E >: E0](test: ZIO[R, TestFailure[E], TestSuccess])(implicit
        trace: Trace
      ): ZIO[R, TestFailure[E], TestSuccess] =
        ZIO.acquireReleaseWith(before.catchAllCause(c => ZIO.fail(TestFailure.Runtime(c))))(after)(_ => test)
    }

  /**
   * A less powerful variant of `around` where the result of `before` is not
   * required by after.
   */
  def around[R0, E0](before: ZIO[R0, E0, Any], after: ZIO[R0, Nothing, Any]): TestAspect[Nothing, R0, E0, Any] =
    aroundWith(before)(_ => after)

  /**
   * Constructs an aspect that evaluates all tests between two effects, `before`
   * and `after`, where the result of `before` can be used in `after`.
   */
  def aroundAllWith[R0, E0, A0](
    before: ZIO[R0, E0, A0]
  )(after: A0 => ZIO[R0, Nothing, Any]): TestAspect[Nothing, R0, E0, Any] =
    new TestAspect[Nothing, R0, E0, Any] {
      def some[R <: R0, E >: E0](spec: Spec[R, E])(implicit trace: Trace): Spec[R, E] =
        Spec.scoped[R](
          ZIO.acquireRelease(before)(after).mapError(TestFailure.fail).as(spec)
        )
    }

  /**
   * A less powerful variant of `aroundAll` where the result of `before` is not
   * required by `after`.
   */
  def aroundAll[R0, E0](before: ZIO[R0, E0, Any], after: ZIO[R0, Nothing, Any]): TestAspect[Nothing, R0, E0, Any] =
    aroundAllWith(before)(_ => after)

  /**
   * Constructs an aspect that evaluates every test inside the context of the
   * scoped function.
   */
  def aroundTest[R0, E0](
    scoped: ZIO[Scope with R0, TestFailure[E0], TestSuccess => ZIO[R0, TestFailure[E0], TestSuccess]]
  ): TestAspect[Nothing, R0, E0, Any] =
    new TestAspect.PerTest[Nothing, R0, E0, Any] {
      def perTest[R <: R0, E >: E0](test: ZIO[R, TestFailure[E], TestSuccess])(implicit
        trace: Trace
      ): ZIO[R, TestFailure[E], TestSuccess] =
        ZIO.scoped[R](scoped.flatMap(f => test.flatMap(f)))
    }

  /**
   * Constructs a simple monomorphic aspect that only works with the specified
   * environment and error type.
   */
  def aspect[R0, E0](
    f: ZIO[R0, TestFailure[E0], TestSuccess] => ZIO[R0, TestFailure[E0], TestSuccess]
  ): TestAspect[R0, R0, E0, E0] =
    new TestAspect.PerTest[R0, R0, E0, E0] {
      def perTest[R >: R0 <: R0, E >: E0 <: E0](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] =
        f(test)
    }

  /**
   * Constructs an aspect that runs the specified effect before every test.
   */
  def before[R0, E0](effect: ZIO[R0, E0, Any]): TestAspect[Nothing, R0, E0, Any] =
    new TestAspect.PerTest[Nothing, R0, E0, Any] {
      def perTest[R <: R0, E >: E0](test: ZIO[R, TestFailure[E], TestSuccess])(implicit
        trace: Trace
      ): ZIO[R, TestFailure[E], TestSuccess] =
        effect.catchAllCause(cause => ZIO.fail(TestFailure.failCause(cause))) *> test
    }

  /**
   * Constructs an aspect that runs the specified effect a single time before
   * all tests.
   */
  def beforeAll[R0, E0](effect: ZIO[R0, E0, Any]): TestAspect[Nothing, R0, E0, Any] =
    aroundAll(effect, ZIO.unit)

  /**
   * An aspect that runs each test on the blocking threadpool. Useful for tests
   * that contain blocking code
   */
  val blocking: TestAspectPoly =
    new PerTest.Poly {
      def perTest[R, E](test: ZIO[R, TestFailure[E], TestSuccess])(implicit
        trace: Trace
      ): ZIO[R, TestFailure[E], TestSuccess] =
        ZIO.blocking(test)
    }

  /**
   * An aspect that applies the provided zio aspect to each sample of all checks
   * in the test.
   *
   * i.e.
   * {{{
   * test("example") {
   *   check(Gen.int) { i =>
   *     ZIO.succeed(assert(i, Assertion.equalTo(1)))
   *   }
   * } @@ checks(ZIOAspect.debug)
   * }}}
   *
   * is equivalent to
   *
   * {{{
   * test("example") {
   *   check(Gen.int) { i =>
   *     ZIO.succeed(assert(i, Assertion.equalTo(1))) @@ ZIOAspect.debug
   *   }
   * }
   * }}}
   */
  def checks(aspect: CheckAspect): TestAspectPoly = checksZIO(
    ZIO.succeed(aspect)(Trace.empty)
  )

  /**
   * An aspect that applies the provided zio aspect to each sample of all checks
   * in the test. The aspect will be constructed from the provided effect before
   * each test is run.
   */
  def checksZIO[R, E](
    makeAspect: ZIO[R, E, CheckAspect]
  ): TestAspect[Nothing, R, E, Any] =
    new TestAspect[Nothing, R, E, Any] {
      def some[R1 <: R, E1 >: E](spec: Spec[R1, E1])(implicit trace: Trace): Spec[R1, E1] =
        spec.transform[R1, E1] {
          case Spec.TestCase(oldTest, annotations) =>
            val newTest = makeAspect.mapError(TestFailure.fail).flatMap { aspect =>
              testConfigWith { oldConfig =>
                val newConfig = TestConfig.TestV2(
                  repeats = oldConfig.repeats,
                  retries = oldConfig.retries,
                  samples = oldConfig.samples,
                  shrinks = oldConfig.shrinks,
                  checkAspect = oldConfig.checkAspect >>> aspect
                )
                withTestConfig(newConfig)(oldTest)
              }
            }
            Spec.TestCase(newTest, annotations)
          case c => c
        }
    }

  /**
   * An aspect that runs each test on a separate fiber and prints a fiber dump
   * if the test fails or has not terminated within the specified duration.
   */
  def diagnose(duration: Duration): TestAspectPoly =
    new TestAspectPoly {
      def some[R, E](
        spec: Spec[R, E]
      )(implicit trace: Trace): Spec[R, E] = {
        def diagnose[R, E](
          label: String,
          test: ZIO[R, TestFailure[E], TestSuccess]
        ): ZIO[R, TestFailure[E], TestSuccess] =
          test.fork.flatMap { fiber =>
            fiber.join.raceWith[R, TestFailure[E], TestFailure[E], Unit, TestSuccess](Live.live(ZIO.sleep(duration)))(
              (exit, sleepFiber) => dump(label).when(!exit.isSuccess) *> sleepFiber.interrupt *> ZIO.done(exit),
              (_, _) => dump(label) *> fiber.join
            )
          }

        def dump[E, A](label: String): UIO[Unit] =
          Annotations.supervisedFibers.flatMap { fibers =>
            Live.live(ZIO.foreachDiscard(fibers) { fiber =>
              for {
                dump <- fiber.dump
                str  <- dump.prettyPrint
                _    <- Console.printLine(str).orDie
              } yield ()
            })
          }

        spec.transform[R, E] {
          case Spec.TestCase(test, annotations) => Spec.TestCase(diagnose("", test), annotations)
          case c                                => c
        }
      }
    }

  /**
   * An aspect that runs each test with the `TestConsole` instance in the
   * environment set to debug mode so that console output is rendered to
   * standard output in addition to being written to the output buffer.
   */
  val debug: TestAspectPoly =
    new PerTest.Poly {
      def perTest[R, E](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] =
        TestConsole.debug(test)
    }

  /**
   * An aspect that retries a test until success, without limit.
   */
  val eventually: TestAspectPoly = {
    val eventually = new PerTest.Poly {
      def perTest[R, E](test: ZIO[R, TestFailure[E], TestSuccess])(implicit
        trace: Trace
      ): ZIO[R, TestFailure[E], TestSuccess] =
        test.eventually
    }
    restoreTestEnvironment >>> eventually
  }

  /**
   * An aspect that runs tests on all platforms except ScalaJS.
   */
  val exceptJS: TestAspectPoly =
    if (TestPlatform.isJS) ignore else identity

  /**
   * An aspect that that applies an aspect on all platforms except ScalaJS.
   */
  def exceptJS[LowerR, UpperR, LowerE, UpperE](
    that: TestAspect[LowerR, UpperR, LowerE, UpperE]
  ): TestAspect[LowerR, UpperR, LowerE, UpperE] =
    if (TestPlatform.isJS) identity else that

  /**
   * An aspect that runs tests on all platforms except the JVM.
   */
  val exceptJVM: TestAspectPoly =
    if (TestPlatform.isJVM) ignore else identity

  /**
   * An aspect that that applies an aspect on all platforms except the JVM.
   */
  def exceptJVM[LowerR, UpperR, LowerE, UpperE](
    that: TestAspect[LowerR, UpperR, LowerE, UpperE]
  ): TestAspect[LowerR, UpperR, LowerE, UpperE] =
    if (TestPlatform.isJVM) identity else that

  /**
   * An aspect that runs tests on all platforms except ScalaNative.
   */
  val exceptNative: TestAspectPoly =
    if (TestPlatform.isNative) ignore else identity

  /**
   * An aspect that that applies an aspect on all platforms except ScalaNative.
   */
  def exceptNative[LowerR, UpperR, LowerE, UpperE](
    that: TestAspect[LowerR, UpperR, LowerE, UpperE]
  ): TestAspect[LowerR, UpperR, LowerE, UpperE] =
    if (TestPlatform.isNative) identity else that

  /**
   * An aspect that runs tests on all versions except Scala 2.
   */
  val exceptScala2: TestAspectPoly =
    if (TestVersion.isScala2) ignore else identity

  /**
   * An aspect that runs tests on all versions except Scala 2.12.
   */
  val exceptScala212: TestAspectPoly =
    if (TestVersion.isScala212) ignore else identity

  /**
   * An aspect that runs tests on all versions except Scala 2.13.
   */
  val exceptScala213: TestAspectPoly =
    if (TestVersion.isScala213) ignore else identity

  /**
   * An aspect that runs tests on all versions except Scala 3.
   */
  val exceptScala3: TestAspectPoly =
    if (TestVersion.isScala3) ignore else identity

  /**
   * An aspect that sets suites to the specified execution strategy, but only if
   * their current strategy is inherited (undefined).
   */
  def executionStrategy(exec: ExecutionStrategy): TestAspectPoly =
    new TestAspectPoly {
      def some[R, E](spec: Spec[R, E])(implicit trace: Trace): Spec[R, E] =
        Spec.exec(exec, spec)
    }

  /**
   * An aspect that makes a test that failed for any reason pass. Note that if
   * the test passes this aspect will make it fail.
   */
  val failing: TestAspectPoly =
    failing(_ => true)

  /**
   * An aspect that makes a test that failed for the specified failure pass.
   * Note that the test will fail for other failures and also if it passes
   * correctly.
   */
  def failing[E0](assertion: TestFailure[E0] => Boolean): TestAspect[Nothing, Any, Nothing, E0] =
    new TestAspect.PerTest[Nothing, Any, Nothing, E0] {
      def perTest[R, E <: E0](test: ZIO[R, TestFailure[E], TestSuccess])(implicit
        trace: Trace
      ): ZIO[R, TestFailure[E], TestSuccess] =
        test.foldZIO(
          failure =>
            if (assertion(failure)) ZIO.succeed(TestSuccess.Succeeded())
            else ZIO.fail(TestFailure.die(new RuntimeException("did not fail as expected"))),
          _ => ZIO.fail(TestFailure.die(new RuntimeException("did not fail as expected")))
        )
    }

  /**
   * An aspect that records the state of fibers spawned by the current test in
   * [[TestAnnotation.fibers]]. Applied by default in [[ZIOSpecAbstract]]. This
   * aspect is required for the proper functioning of `TestClock.adjust`.
   */
  lazy val fibers: TestAspectPoly =
    new PerTest.Poly {
      def perTest[R, E](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] = {
        val acquire = ZIO.succeed(new AtomicReference(SortedSet.empty[Fiber.Runtime[Any, Any]])).tap { ref =>
          Annotations.annotate(TestAnnotation.fibers, Right(Chunk(ref)))
        }
        val release = Annotations.get(TestAnnotation.fibers).flatMap {
          case Right(refs) =>
            ZIO
              .foreach(refs)(ref => ZIO.succeed(ref.get))
              .map(_.foldLeft(SortedSet.empty[Fiber.Runtime[Any, Any]])(_ ++ _).size)
              .tap { n =>
                Annotations.annotate(TestAnnotation.fibers, Left(n))
              }
          case Left(_) => ZIO.unit
        }
        ZIO.acquireReleaseWith(acquire)(_ => release) { ref =>
          Supervisor.fibersIn(ref).flatMap(supervisor => test.supervised(supervisor))
        }
      }
    }

  /**
   * An aspect that retries a test until success, with a default limit, for use
   * with flaky tests.
   */
  val flaky: TestAspectPoly = {
    val flaky = new PerTest.Poly {
      def perTest[R, E](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] =
        TestConfig.retries.flatMap { n =>
          test.catchAll(_ => test.tapError(_ => Annotations.annotate(TestAnnotation.retried, 1)).retryN(n - 1))
        }
    }
    restoreTestEnvironment >>> flaky
  }

  /**
   * An aspect that retries a test until success, with the specified limit, for
   * use with flaky tests.
   */
  def flaky(n: Int): TestAspectPoly = {
    val flaky = new PerTest.Poly {
      def perTest[R, E](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] =
        test.catchAll(_ => test.tapError(_ => Annotations.annotate(TestAnnotation.retried, 1)).retryN(n - 1))
    }
    restoreTestEnvironment >>> flaky
  }

  /**
   * An aspect that runs each test on its own separate fiber.
   */
  val forked: TestAspectPoly =
    new PerTest.Poly {
      def perTest[R, E](test: ZIO[R, TestFailure[E], TestSuccess])(implicit
        trace: Trace
      ): ZIO[R, TestFailure[E], TestSuccess] =
        test.fork.flatMap(_.join)
    }

  /**
   * An aspect that provides each test with the specified layer that does not
   * produce any services.
   */
  def fromLayer[R0, E0](layer: ZLayer[R0, E0, Any]): TestAspect[Nothing, R0, E0, Any] =
    new TestAspect[Nothing, R0, E0, Any] {
      def some[R <: R0, E >: E0](spec: Spec[R, E])(implicit trace: Trace): Spec[R, E] =
        spec.provideSomeLayer[R](layer)
    }

  /**
   * An aspect that provides all tests with a shared version of the specified
   * layer that does not produce any services.
   */
  def fromLayerShared[R0, E0](layer: ZLayer[R0, E0, Any]): TestAspect[Nothing, R0, E0, Any] =
    new TestAspect[Nothing, R0, E0, Any] {
      def some[R <: R0, E >: E0](spec: Spec[R, E])(implicit trace: Trace): Spec[R, E] =
        spec.provideSomeLayerShared[R](layer)
    }

  /**
   * As aspect that runs each test with the specified `ZIOAspect`.
   */
  def fromZIOAspect[LowerR, UpperR, LowerE, UpperE](
    zioAspect: ZIOAspect[LowerR, UpperR, TestFailure[LowerE], TestFailure[UpperE], TestSuccess, TestSuccess]
  ): TestAspect[LowerR, UpperR, LowerE, UpperE] =
    new TestAspect.PerTest[LowerR, UpperR, LowerE, UpperE] {
      def perTest[R >: LowerR <: UpperR, E >: LowerE <: UpperE](test: ZIO[R, TestFailure[E], TestSuccess])(implicit
        trace: Trace
      ): ZIO[R, TestFailure[E], TestSuccess] =
        zioAspect(test)
    }

  /**
   * An aspect that only runs a test if the specified environment variable
   * satisfies the specified assertion.
   */
  def ifEnv(env: String)(assertion: String => Boolean): TestAspectPoly =
    ifEnvOption(env)(_.fold(false)(assertion))

  /**
   * An aspect that only runs a test if the specified environment variable is
   * not set.
   */
  def ifEnvNotSet(env: String): TestAspectPoly =
    ifEnvOption(env)(_.isEmpty)

  /**
   * An aspect that only runs a test if the specified optional environment
   * variable satisfies the specified assertion.
   */
  def ifEnvOption(env: String)(assertion: Option[String] => Boolean): TestAspectPoly =
    new TestAspectPoly {
      def some[R, E](spec: Spec[R, E])(implicit trace: Trace): Spec[R, E] =
        spec.whenZIO(Live.live(System.env(env)).orDie.map(assertion))
    }

  /**
   * An aspect that only runs a test if the specified environment variable is
   * set.
   */
  def ifEnvSet(env: String): TestAspectPoly =
    ifEnvOption(env)(_.nonEmpty)

  /**
   * An aspect that only runs a test if the specified Java property satisfies
   * the specified assertion.
   */
  def ifProp(prop: String)(assertion: String => Boolean): TestAspectPoly =
    ifPropOption(prop)(_.fold(false)(assertion))

  /**
   * An aspect that only runs a test if the specified Java property is not set.
   */
  def ifPropNotSet(env: String): TestAspectPoly =
    ifPropOption(env)(_.isEmpty)

  /**
   * An aspect that only runs a test if the specified optional Java property
   * satisfies the specified assertion.
   */
  def ifPropOption(prop: String)(assertion: Option[String] => Boolean): TestAspectPoly =
    new TestAspectPoly {
      def some[R, E](spec: Spec[R, E])(implicit trace: Trace): Spec[R, E] =
        spec.whenZIO(Live.live(System.property(prop)).orDie.map(assertion))
    }

  /**
   * An aspect that only runs a test if the specified Java property is set.
   */
  def ifPropSet(prop: String): TestAspectPoly =
    ifPropOption(prop)(_.nonEmpty)

  /**
   * An aspect that applies the specified aspect on ScalaJS.
   */
  def js[LowerR, UpperR, LowerE, UpperE](
    that: TestAspect[LowerR, UpperR, LowerE, UpperE]
  ): TestAspect[LowerR, UpperR, LowerE, UpperE] =
    if (TestPlatform.isJS) that else identity

  /**
   * An aspect that only runs tests on ScalaJS.
   */
  val jsOnly: TestAspectPoly =
    if (TestPlatform.isJS) identity else ignore

  /**
   * An aspect that applies the specified aspect on the JVM.
   */
  def jvm[LowerR, UpperR, LowerE, UpperE](
    that: TestAspect[LowerR, UpperR, LowerE, UpperE]
  ): TestAspect[LowerR, UpperR, LowerE, UpperE] =
    if (TestPlatform.isJVM) that else identity

  /**
   * An aspect that only runs tests on the JVM.
   */
  val jvmOnly: TestAspectPoly =
    if (TestPlatform.isJVM) identity else ignore

  /**
   * An aspect that runs only on operating systems accepted by the specified
   * predicate.
   */
  def os(f: System.OS => Boolean): TestAspectPoly =
    if (f(System.os)) identity else ignore

  /**
   * Runs only on Mac operating systems.
   */
  val mac: TestAspectPoly = os(_.isMac)

  /**
   * An aspect that applies the specified aspect on ScalaNative.
   */
  def native[LowerR, UpperR, LowerE, UpperE](
    that: TestAspect[LowerR, UpperR, LowerE, UpperE]
  ): TestAspect[LowerR, UpperR, LowerE, UpperE] =
    if (TestPlatform.isNative) that else identity

  /**
   * An aspect that only runs tests on ScalaNative.
   */
  val nativeOnly: TestAspectPoly =
    if (TestPlatform.isNative) identity else ignore

  /**
   * An aspect that repeats the test a default number of times, ensuring it is
   * stable ("non-flaky"). Stops at the first failure.
   */
  val nonFlaky: TestAspectPoly = {
    val nonFlaky = new PerTest.Poly {
      def perTest[R, E](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] =
        TestConfig.repeats.flatMap { n =>
          test *> test.tap(_ => Annotations.annotate(TestAnnotation.repeated, 1)).repeatN(n - 1)
        }
    }
    restoreTestEnvironment >>> nonFlaky
  }

  /**
   * An aspect that repeats the test a specified number of times, ensuring it is
   * stable ("non-flaky"). Stops at the first failure.
   */
  def nonFlaky(n: Int): TestAspectPoly = {
    val nonFlaky = new PerTest.Poly {
      def perTest[R, E](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] =
        test *> test.tap(_ => Annotations.annotate(TestAnnotation.repeated, 1)).repeatN(n - 1)
    }
    restoreTestEnvironment >>> nonFlaky
  }

  /**
   * Constructs an aspect that requires a test to not terminate within the
   * specified time.
   */
  def nonTermination(duration: Duration): TestAspectPoly =
    timeout(duration) >>>
      failing {
        case TestFailure.Assertion(_, _) => false
        case TestFailure.Runtime(cause, _) =>
          cause.dieOption match {
            case Some(t) => t.getMessage == s"Timeout of ${duration.render} exceeded."
            case None    => false
          }
      }

  /**
   * Sets the seed of the `TestRandom` instance in the environment to a random
   * value before each test.
   */
  val nondeterministic: TestAspectPoly =
    before(
      Live
        .live(Random.nextLong(Trace.empty))(Trace.empty)
        .flatMap(TestRandom.setSeed(_)(Trace.empty))(Trace.empty)
    )

  /**
   * An aspect that executes the members of a suite in parallel.
   */
  val parallel: TestAspectPoly =
    executionStrategy(ExecutionStrategy.Parallel)

  /**
   * An aspect that executes the members of a suite in parallel, up to the
   * specified number of concurrent fibers.
   */
  def parallelN(n: Int): TestAspectPoly =
    executionStrategy(ExecutionStrategy.ParallelN(n))

  /**
   * An aspect that repeats successful tests according to a schedule.
   */
  def repeat[R0](
    schedule: Schedule[R0, TestSuccess, Any]
  ): TestAspectAtLeastR[R0] = {
    val repeat = new PerTest.AtLeastR[R0] {
      def perTest[R <: R0, E](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] = {
        val repeatSchedule: Schedule[R0, TestSuccess, TestSuccess] =
          schedule
            .zipRight(Schedule.identity[TestSuccess])
            .tapOutput(_ => Annotations.annotate(TestAnnotation.repeated, 1))
        Live.withLive(test)(_.repeat(repeatSchedule))
      }
    }
    restoreTestEnvironment >>> repeat
  }

  /**
   * An aspect that runs each test with the number of times to repeat tests to
   * ensure they are stable set to the specified value.
   */
  def repeats(n: Int): TestAspectPoly =
    new PerTest.Poly {
      def perTest[R, E](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] =
        testConfigWith { old =>
          val testConfig = TestConfig.TestV2(
            repeats = n,
            retries = old.retries,
            samples = old.samples,
            shrinks = old.shrinks,
            checkAspect = old.checkAspect
          )
          withTestConfig(testConfig)(test)
        }
    }

  /**
   * An aspect that restores a given [[zio.test.Restorable Restorable]]'s state
   * to its starting state after the test is run. Note that this is only useful
   * when repeating tests.
   */
  def restore(restorable: UIO[Restorable]): TestAspectPoly =
    aroundWith(restorable.flatMap(_.save(Trace.empty))(Trace.empty))(restore => restore)

  /**
   * An aspect that restores the [[zio.test.TestClock TestClock]]'s state to its
   * starting state after the test is run. Note that this is only useful when
   * repeating tests.
   */
  def restoreTestClock: TestAspectPoly =
    restore(testClock(Trace.empty))

  /**
   * An aspect that restores the [[zio.test.TestConsole TestConsole]]'s state to
   * its starting state after the test is run. Note that this is only useful
   * when repeating tests.
   */
  def restoreTestConsole: TestAspectPoly =
    restore(testConsole(Trace.empty))

  /**
   * An aspect that restores the [[zio.test.TestRandom TestRandom]]'s state to
   * its starting state after the test is run. Note that this is only useful
   * when repeating tests.
   */
  def restoreTestRandom: TestAspectPoly =
    restore(testRandom(Trace.empty))

  /**
   * An aspect that restores the [[zio.test.TestSystem TestSystem]]'s state to
   * its starting state after the test is run. Note that this is only useful
   * when repeating tests.
   */
  def restoreTestSystem: TestAspectPoly =
    restore(testSystem(Trace.empty))

  /**
   * An aspect that restores all state in the standard provided test
   * environments ([[zio.test.TestClock TestClock]],
   * [[zio.test.TestConsole TestConsole]], [[zio.test.TestRandom TestRandom]],
   * and [[zio.test.TestSystem TestSystem]]) to their starting state after the
   * test is run. Note that this is only useful when repeating tests.
   */
  def restoreTestEnvironment: TestAspectPoly =
    restoreTestClock >>> restoreTestConsole >>> restoreTestRandom >>> restoreTestSystem

  /**
   * An aspect that runs each test with the number of times to retry flaky tests
   * set to the specified value.
   */
  def retries(n: Int): TestAspectPoly =
    new PerTest.Poly {
      def perTest[R, E](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] =
        testConfigWith { old =>
          val testConfig = TestConfig.TestV2(
            repeats = old.repeats,
            retries = n,
            samples = old.samples,
            shrinks = old.shrinks,
            checkAspect = old.checkAspect
          )
          withTestConfig(testConfig)(test)
        }
    }

  /**
   * An aspect that retries failed tests according to a schedule.
   */
  def retry[R0, E0](
    schedule: Schedule[R0, TestFailure[E0], Any]
  ): TestAspect[Nothing, R0, Nothing, E0] = {
    val retry = new TestAspect.PerTest[Nothing, R0, Nothing, E0] {
      def perTest[R <: R0, E <: E0](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] = {
        val retrySchedule: Schedule[R0, TestFailure[E0], Any] =
          schedule.tapOutput(_ => Annotations.annotate(TestAnnotation.retried, 1))
        Live.withLive(test)(_.retry(retrySchedule))
      }
    }
    restoreTestEnvironment >>> retry
  }

  /**
   * An aspect that runs each test with the number of sufficient samples to
   * check for a random variable set to the specified value.
   */
  def samples(n: Int): TestAspectPoly =
    new PerTest.Poly {
      def perTest[R, E](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] =
        testConfigWith { old =>
          val testConfig = TestConfig.TestV2(
            repeats = old.repeats,
            retries = old.retries,
            samples = n,
            shrinks = old.shrinks,
            checkAspect = old.checkAspect
          )
          withTestConfig(testConfig)(test)
        }
    }

  /**
   * An aspect that executes the members of a suite sequentially.
   */
  val sequential: TestAspectPoly =
    executionStrategy(ExecutionStrategy.Sequential)

  /**
   * An aspect that applies the specified aspect on Scala 2.
   */
  def scala2[LowerR, UpperR, LowerE, UpperE](
    that: TestAspect[LowerR, UpperR, LowerE, UpperE]
  ): TestAspect[LowerR, UpperR, LowerE, UpperE] =
    if (TestVersion.isScala2) that else identity

  /**
   * An aspect that applies the specified aspect on Scala 2.12.
   */
  def scala212[LowerR, UpperR, LowerE, UpperE](
    that: TestAspect[LowerR, UpperR, LowerE, UpperE]
  ): TestAspect[LowerR, UpperR, LowerE, UpperE] =
    if (TestVersion.isScala212) that else identity

  /**
   * An aspect that applies the specified aspect on Scala 2.13.
   */
  def scala213[LowerR, UpperR, LowerE, UpperE](
    that: TestAspect[LowerR, UpperR, LowerE, UpperE]
  ): TestAspect[LowerR, UpperR, LowerE, UpperE] =
    if (TestVersion.isScala213) that else identity

  /**
   * An aspect that applies the specified aspect on Scala 3.
   */
  def scala3[LowerR, UpperR, LowerE, UpperE](
    that: TestAspect[LowerR, UpperR, LowerE, UpperE]
  ): TestAspect[LowerR, UpperR, LowerE, UpperE] =
    if (TestVersion.isScala3) that else identity

  /**
   * An aspect that only runs tests on Scala 2.
   */
  val scala2Only: TestAspectPoly =
    if (TestVersion.isScala2) identity else ignore

  /**
   * An aspect that only runs tests on Scala 2.12.
   */
  val scala212Only: TestAspectPoly =
    if (TestVersion.isScala212) identity else ignore

  /**
   * An aspect that only runs tests on Scala 2.13.
   */
  val scala213Only: TestAspectPoly =
    if (TestVersion.isScala213) identity else ignore

  /**
   * An aspect that only runs tests on Scala 3.
   */
  val scala3Only: TestAspectPoly =
    if (TestVersion.isScala3) identity else ignore

  /**
   * Sets the seed of the `TestRandom` instance in the environment to the
   * specified value before each test.
   */
  def setSeed(seed: => Long): TestAspectPoly =
    before(TestRandom.setSeed(seed)(Trace.empty))

  /**
   * An aspect that runs each test with the maximum number of shrinkings to
   * minimize large failures set to the specified value.
   */
  def shrinks(n: Int): TestAspectPoly =
    new PerTest.Poly {
      def perTest[R, E](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] =
        testConfigWith { old =>
          val testConfig = TestConfig.TestV2(
            repeats = old.repeats,
            retries = old.retries,
            samples = old.samples,
            shrinks = n,
            checkAspect = old.checkAspect
          )
          withTestConfig(testConfig)(test)
        }
    }

  /**
   * An aspect that runs each test with the [[zio.test.TestConsole TestConsole]]
   * instance in the environment set to silent mode so that console output is
   * only written to the output buffer and not rendered to standard output.
   */
  val silent: TestAspectPoly =
    new PerTest.Poly {
      def perTest[R, E](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] =
        TestConsole.silent(test)
    }

  /**
   * As aspect that runs each test with the default console logger removed so
   * that logs are only written to the output buffer and not rendered to
   * standard output.
   */
  val silentLogging: TestAspectPoly =
    TestAspect.fromLayer(Runtime.removeDefaultLoggers)

  /**
   * An aspect that runs each test with the size set to the specified value.
   */
  def size(n: Int): TestAspectPoly =
    new PerTest.Poly {
      def perTest[R, E](test: ZIO[R, TestFailure[E], TestSuccess])(implicit
        trace: Trace
      ): ZIO[R, TestFailure[E], TestSuccess] =
        Sized.withSize(n)(test)
    }

  /**
   * An aspect that runs each test with the size set to the specified value.
   */
  @deprecated("use size", "2.0.2")
  def sized(n: Int): TestAspectPoly =
    size(n)

  /**
   * An aspect that converts ignored tests into test failures.
   */
  val success: TestAspectPoly =
    new PerTest.Poly {
      def perTest[R, E](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] =
        test.flatMap {
          case TestSuccess.Ignored(_) =>
            ZIO.fail(TestFailure.Runtime(Cause.die(new RuntimeException("Test was ignored."))))
          case x => ZIO.succeed(x)
        }
    }

  /**
   * Annotates tests with string tags.
   */
  def tag(tag: String, tags: String*): TestAspectPoly =
    annotate(TestAnnotation.tagged, Set(tag) union tags.toSet)

  /**
   * Annotates tests with their execution times.
   */
  val timed: TestAspectPoly =
    new PerTest.Poly {
      def perTest[R, E](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] =
        Live.withLive(test)(_.either.summarized(Clock.instant)(TestDuration.fromInterval)).flatMap {
          case (duration, result) =>
            ZIO.fromEither(result).ensuring(Annotations.annotate(TestAnnotation.timing, duration))
        }
    }

  /**
   * An aspect that times out tests using the specified duration.
   * @param duration
   *   maximum test duration
   */
  def timeout(
    duration: Duration
  ): TestAspectPoly =
    new PerTest.Poly {
      def perTest[R, E](
        test: ZIO[R, TestFailure[E], TestSuccess]
      )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess] = {
        def timeoutFailure =
          TestTimeoutException(s"Timeout of ${duration.render} exceeded.")
        Live
          .withLive(test)(_.either.disconnect.timeout(duration).flatMap {
            case None         => ZIO.fail(TestFailure.Runtime(Cause.die(timeoutFailure)))
            case Some(result) => ZIO.fromEither(result)
          })
      }
    }

  /**
   * Verifies the specified post-condition after each test is run.
   */
  def verify[R0, E0](condition: => ZIO[R0, E0, TestResult]): TestAspect[Nothing, R0, E0, Any] =
    new TestAspect.PerTest[Nothing, R0, E0, Any] {
      def perTest[R <: R0, E >: E0](test: ZIO[R, TestFailure[E], TestSuccess])(implicit
        trace: Trace
      ): ZIO[R, TestFailure[E], TestSuccess] =
        test <* ZTest("verify", condition)
    }

  /**
   * Runs only on Unix / Linux operating systems.
   */
  val unix: TestAspectPoly = os(_.isUnix)

  /**
   * Runs only on Windows operating systems.
   */
  val windows: TestAspectPoly = os(_.isWindows)

  /**
   * An aspect that runs tests with the specified config provider.
   */
  def withConfigProvider(configProvider: ConfigProvider): TestAspectPoly =
    new TestAspectPoly {
      def some[R, E](spec: Spec[R, E])(implicit trace: Trace): Spec[R, E] =
        spec.provideSomeLayer[R](Runtime.setConfigProvider(configProvider))
    }

  /**
   * An aspect that runs tests with the live clock service.
   */
  lazy val withLiveClock: TestAspectPoly =
    new TestAspectPoly {
      def some[R, E](spec: Spec[R, E])(implicit trace: Trace): Spec[R, E] = {
        val layer = ZLayer.scoped {
          for {
            clock <- live(ZIO.clock)
            _     <- ZIO.withClockScoped(clock)
          } yield ()
        }
        spec.provideSomeLayer[R](layer)
      }
    }

  /**
   * An aspect that runs tests with the live console service.
   */
  lazy val withLiveConsole: TestAspectPoly =
    new TestAspectPoly {
      def some[R, E](spec: Spec[R, E])(implicit trace: Trace): Spec[R, E] = {
        val layer = ZLayer.scoped {
          for {
            console <- live(ZIO.console)
            _       <- ZIO.withConsoleScoped(console)
          } yield ()
        }
        spec.provideSomeLayer[R](layer)
      }
    }

  /**
   * An aspect that runs tests with the live default ZIO services.
   */
  lazy val withLiveEnvironment: TestAspectPoly =
    withLiveClock >>>
      withLiveConsole >>>
      withLiveRandom >>>
      withLiveSystem

  /**
   * An aspect that runs tests with the live random service.
   */
  lazy val withLiveRandom: TestAspectPoly =
    new TestAspectPoly {
      def some[R, E](spec: Spec[R, E])(implicit trace: Trace): Spec[R, E] = {
        val layer = ZLayer.scoped {
          for {
            random <- live(ZIO.random)
            _      <- ZIO.withRandomScoped(random)
          } yield ()
        }
        spec.provideSomeLayer[R](layer)
      }
    }

  /**
   * An aspect that runs tests with the live system service.
   */
  lazy val withLiveSystem: TestAspectPoly =
    new TestAspectPoly {
      def some[R, E](spec: Spec[R, E])(implicit trace: Trace): Spec[R, E] = {
        val layer = ZLayer.scoped {
          for {
            system <- live(ZIO.system)
            _      <- ZIO.withSystemScoped(system)
          } yield ()
        }
        spec.provideSomeLayer[R](layer)
      }
    }

  abstract class PerTest[+LowerR, -UpperR, +LowerE, -UpperE] extends TestAspect[LowerR, UpperR, LowerE, UpperE] {

    def perTest[R >: LowerR <: UpperR, E >: LowerE <: UpperE](
      test: ZIO[R, TestFailure[E], TestSuccess]
    )(implicit trace: Trace): ZIO[R, TestFailure[E], TestSuccess]

    final def some[R >: LowerR <: UpperR, E >: LowerE <: UpperE](
      spec: Spec[R, E]
    )(implicit trace: Trace): Spec[R, E] =
      spec.transform[R, E] {
        case Spec.TestCase(test, annotations) => Spec.TestCase(perTest(test), annotations)
        case c                                => c
      }
  }
  object PerTest {

    /**
     * A `PerTest.AtLeast[R]` is a `TestAspect.PerTest` that that requires at
     * least an `R` in its environment
     */
    type AtLeastR[R] = TestAspect.PerTest[Nothing, R, Nothing, Any]

    /**
     * A `PerTest.Poly` is a `TestAspect.PerTest` that is completely
     * polymorphic, having no requirements ZRTestEnv on error or environment.
     */
    type Poly = TestAspect.PerTest[Nothing, Any, Nothing, Any]
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy