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

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

There is a newer version: 2.1.9
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

import zio.internal.stacktracer.{SourceLocation, Tracer}
import zio.stacktracer.TracingImplicits.disableAutoTrace
import zio.stream.{ZChannel, ZSink, ZStream}
import zio.test.ReporterEventRenderer.ConsoleEventRenderer
import zio.test.Spec.LabeledCase

import scala.language.implicitConversions

/**
 * _ZIO Test_ is a featherweight testing library for effectful programs.
 *
 * The library imagines every spec as an ordinary immutable value, providing
 * tremendous potential for composition. Thanks to tight integration with ZIO,
 * specs can use resources (including those requiring disposal), have well-
 * defined linear and parallel semantics, and can benefit from a host of ZIO
 * combinators.
 *
 * {{{
 *   import zio.test._
 *   import zio.Clock.nanoTime
 *
 *   object MyTest extends ZIOSpecDefault {
 *     def spec = suite("clock")(
 *       test("time is non-zero") {
 *         for {
 *           time <- Live.live(nanoTime)
 *         } yield assertTrue(time >= 0L)
 *       }
 *     )
 *   }
 * }}}
 */
package object test extends CompileVariants {
  type TestEnvironment = Annotations with Live with Sized with TestConfig

  object TestEnvironment {
    val any: ZLayer[TestEnvironment, Nothing, TestEnvironment] =
      ZLayer.environment[TestEnvironment](Tracer.newTrace)
    val live: ZLayer[Clock with Console with System with Random, Nothing, TestEnvironment] = {
      implicit val trace = Tracer.newTrace
      Annotations.live ++
        Live.default ++
        Sized.live(100) ++
        ((Live.default ++ Annotations.live) >>> TestClock.default) ++
        TestConfig.live(100, 100, 200, 1000) ++
        ((Live.default ++ Annotations.live) >>> TestConsole.debug) ++
        TestRandom.deterministic ++
        TestSystem.default

    }
  }

  val liveEnvironment: Layer[Nothing, Clock with Console with System with Random] = {
    implicit val trace = Trace.empty
    ZLayer.succeedEnvironment(
      ZEnvironment[Clock, Console, System, Random](
        Clock.ClockLive,
        Console.ConsoleLive,
        System.SystemLive,
        Random.RandomLive
      )
    )
  }

  private def testFiberRefGen(implicit trace: Trace): ULayer[Unit] =
    ZLayer.scoped(FiberRef.currentFiberIdGenerator.locallyScoped(FiberId.Gen.Monotonic))

  val testEnvironment: ZLayer[Any, Nothing, TestEnvironment] = {
    implicit val trace = Tracer.newTrace
    liveEnvironment >>> (TestEnvironment.live ++ testFiberRefGen)
  }

  /**
   * Provides an effect with the "real" environment as opposed to the test
   * environment. This is useful for performing effects such as timing out
   * tests, accessing the real time, or printing to the real console.
   */
  def live[R, E, A](zio: ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A] =
    Live.live(zio)

  /**
   * Retrieves the `TestClock` service for this test.
   */
  def testClock(implicit trace: Trace): UIO[TestClock] =
    testClockWith(ZIO.succeed(_))

  /**
   * Retrieves the `TestClock` service for this test and uses it to run the
   * specified workflow.
   */
  def testClockWith[R, E, A](f: TestClock => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A] =
    DefaultServices.currentServices.getWith(services => f(services.asInstanceOf[ZEnvironment[TestClock]].get))

  /**
   * Retrieves the `TestConsole` service for this test.
   */
  def testConsole(implicit trace: Trace): UIO[TestConsole] =
    testConsoleWith(ZIO.succeed(_))

  /**
   * Retrieves the `TestConsole` service for this test and uses it to run the
   * specified workflow.
   */
  def testConsoleWith[R, E, A](f: TestConsole => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A] =
    DefaultServices.currentServices.getWith(services => f(services.asInstanceOf[ZEnvironment[TestConsole]].get))

  /**
   * Retrieves the `TestRandom` service for this test.
   */
  def testRandom(implicit trace: Trace): UIO[TestRandom] =
    testRandomWith(ZIO.succeed(_))

  /**
   * Retrieves the `TestRandom` service for this test and uses it to run the
   * specified workflow.
   */
  def testRandomWith[R, E, A](f: TestRandom => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A] =
    DefaultServices.currentServices.getWith(services => f(services.asInstanceOf[ZEnvironment[TestRandom]].get))

  /**
   * Retrieves the `TestSystem` service for this test.
   */
  def testSystem(implicit trace: Trace): UIO[TestSystem] =
    testSystemWith(ZIO.succeed(_))

  /**
   * Retrieves the `TestSystem` service for this test and uses it to run the
   * specified workflow.
   */
  def testSystemWith[R, E, A](f: TestSystem => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A] =
    DefaultServices.currentServices.getWith(services => f(services.asInstanceOf[ZEnvironment[TestSystem]].get))

  /**
   * Retrieves the `Annotations` service for this test.
   */
  def annotations(implicit trace: Trace): UIO[Annotations] =
    annotationsWith(ZIO.succeed(_))

  /**
   * Retrieves the `Annotations` service for this test and uses it to run the
   * specified workflow.
   */
  def annotationsWith[R, E, A](f: Annotations => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A] =
    TestServices.currentServices.getWith(services => f(services.asInstanceOf[ZEnvironment[Annotations]].get))

  /**
   * Retrieves the `Live` service for this test.
   */
  def live(implicit trace: Trace): UIO[Live] =
    liveWith(ZIO.succeed(_))

  /**
   * Retrieves the `Live` service for this test and uses it to run the specified
   * workflow.
   */
  def liveWith[R, E, A](f: Live => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A] =
    TestServices.currentServices.getWith(services => f(services.asInstanceOf[ZEnvironment[Live]].get))

  /**
   * Retrieves the `Sized` service for this test.
   */
  def sized(implicit trace: Trace): UIO[Sized] =
    sizedWith(ZIO.succeed(_))

  /**
   * Retrieves the `Sized` service for this test and uses it to run the
   * specified workflow.
   */
  def sizedWith[R, E, A](f: Sized => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A] =
    TestServices.currentServices.getWith(services => f(services.asInstanceOf[ZEnvironment[Sized]].get))

  /**
   * Retrieves the `TestConfig` service for this test.
   */
  def testConfig(implicit trace: Trace): UIO[TestConfig] =
    testConfigWith(ZIO.succeed(_))

  /**
   * Retrieves the `TestConfig` service for this test and uses it to run the
   * specified workflow.
   */
  def testConfigWith[R, E, A](f: TestConfig => ZIO[R, E, A])(implicit trace: Trace): ZIO[R, E, A] =
    TestServices.currentServices.getWith(services => f(services.asInstanceOf[ZEnvironment[TestConfig]].get))

  /**
   * Executes the specified workflow with the specified implementation of the
   * annotations service.
   */
  def withAnnotations[R, E, A <: Annotations, B](annotations: => A)(
    zio: => ZIO[R, E, B]
  )(implicit tag: Tag[A], trace: Trace): ZIO[R, E, B] =
    TestServices.currentServices.locallyWith(_.add(annotations))(zio)

  /**
   * Sets the implementation of the annotations service to the specified value
   * and restores it to its original value when the scope is closed.
   */
  def withAnnotationsScoped[A <: Annotations](
    annotations: => A
  )(implicit tag: Tag[A], trace: Trace): ZIO[Scope, Nothing, Unit] =
    TestServices.currentServices.locallyScopedWith(_.add(annotations))

  /**
   * Executes the specified workflow with the specified implementation of the
   * config service.
   */
  def withTestConfig[R, E, A <: TestConfig, B](testConfig: => A)(
    zio: => ZIO[R, E, B]
  )(implicit tag: Tag[A], trace: Trace): ZIO[R, E, B] =
    TestServices.currentServices.locallyWith(_.add(testConfig))(zio)

  /**
   * Sets the implementation of the config service to the specified value and
   * restores it to its original value when the scope is closed.
   */
  def withTestConfigScoped[A <: TestConfig](
    testConfig: => A
  )(implicit tag: Tag[A], trace: Trace): ZIO[Scope, Nothing, Unit] =
    TestServices.currentServices.locallyScopedWith(_.add(testConfig))

  /**
   * Executes the specified workflow with the specified implementation of the
   * sized service.
   */
  def withSized[R, E, A <: Sized, B](sized: => A)(
    zio: => ZIO[R, E, B]
  )(implicit tag: Tag[A], trace: Trace): ZIO[R, E, B] =
    TestServices.currentServices.locallyWith(_.add(sized))(zio)

  /**
   * Sets the implementation of the sized service to the specified value and
   * restores it to its original value when the scope is closed.
   */
  def withSizedScoped[A <: Sized](sized: => A)(implicit tag: Tag[A], trace: Trace): ZIO[Scope, Nothing, Unit] =
    TestServices.currentServices.locallyScopedWith(_.add(sized))

  /**
   * Executes the specified workflow with the specified implementation of the
   * live service.
   */
  def withLive[R, E, A <: Live, B](live: => A)(
    zio: => ZIO[R, E, B]
  )(implicit tag: Tag[A], trace: Trace): ZIO[R, E, B] =
    TestServices.currentServices.locallyWith(_.add(live))(zio)

  /**
   * Sets the implementation of the live service to the specified value and
   * restores it to its original value when the scope is closed.
   */
  def withLiveScoped[A <: Live](live: => A)(implicit tag: Tag[A], trace: Trace): ZIO[Scope, Nothing, Unit] =
    TestServices.currentServices.locallyScopedWith(_.add(live))

  /**
   * Transforms this effect with the specified function. The test environment
   * will be provided to this effect, but the live environment will be provided
   * to the transformation function. This can be useful for applying
   * transformations to an effect that require access to the "real" environment
   * while ensuring that the effect itself uses the test environment.
   *
   * {{{
   *   withLive(test)(_.timeout(duration))
   * }}}
   */
  def withLive[R, E, E1, A, B](
    zio: ZIO[R, E, A]
  )(f: ZIO[R, E, A] => ZIO[R, E1, B])(implicit trace: Trace): ZIO[R, E1, B] =
    Live.withLive(zio)(f)

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

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

  /**
   * A `ZTest[R, E]` is an effectfully produced test that requires an `R` and
   * may fail with an `E`.
   */
  type ZTest[-R, +E] = ZIO[R, TestFailure[E], TestSuccess]

  object ZTest {

    /**
     * Builds a test with an effectual assertion.
     */
    def apply[R, E](label: String, assertion: => ZIO[R, E, TestResult])(implicit
      trace: Trace
    ): ZIO[R, TestFailure[E], TestSuccess] =
      for {
        promise <- Promise.make[TestFailure[E], TestSuccess]
        child <- ZIO
                   .suspendSucceed(assertion)
                   .foldCauseZIO(
                     cause =>
                       cause.dieOption match {
                         case Some(TestResult.Exit(assert)) => ZIO.fail(TestFailure.Assertion(assert))
                         case _                             => ZIO.fail(TestFailure.Runtime(cause))
                       },
                     assert =>
                       if (assert.isFailure)
                         ZIO.fail(TestFailure.Assertion(assert))
                       else
                         ZIO.succeed(TestSuccess.Succeeded())
                   )
                   .intoPromise(promise)
                   .forkDaemon
        result <- promise.await
        _      <- child.inheritAll
        _ <- ZIO.whenDiscard(child.isAlive()) {
               for {
                 fiber <- ZIO
                            .logWarning({
                              val quotedLabel = "\"" + label + "\""
                              s"Warning: ZIO Test is attempting to interrupt fiber " +
                                s"${child.id} forked in test $quotedLabel due to automatic, " +
                                "supervision, but interruption has taken more than 10 " +
                                "seconds to complete. This may indicate a resource leak. " +
                                "Make sure you are not forking a fiber in an " +
                                "uninterruptible region."
                            })
                            .delay(10.seconds)
                            .withClock(Clock.ClockLive)
                            .interruptible
                            .forkDaemon
                            .onExecutor(Runtime.defaultExecutor)
                 _ <- (child.interrupt *> fiber.interrupt).forkDaemon.onExecutor(Runtime.defaultExecutor)
               } yield ()
             }
      } yield result
  }

  private[zio] def assertImpl[A](
    value: => A,
    codeString: Option[String] = None,
    assertionString: Option[String] = None
  )(assertion: Assertion[A])(implicit trace: Trace, sourceLocation: SourceLocation): TestResult =
    Assertion.smartAssert(value, codeString, assertionString)(assertion)

  /**
   * Checks the assertion holds for the given effectfully-computed value.
   */
  private[zio] def assertZIOImpl[R, E, A](
    effect: ZIO[R, E, A],
    codeString: Option[String] = None,
    assertionString: Option[String] = None
  )(
    assertion: Assertion[A]
  )(implicit trace: Trace, sourceLocation: SourceLocation): ZIO[R, E, TestResult] =
    effect.map { value =>
      assertImpl(value, codeString, assertionString)(assertion)
    }

  /**
   * Asserts that the given test was completed.
   */
  def assertCompletes(implicit trace: Trace, sourceLocation: SourceLocation): TestResult =
    assertImpl(true)(Assertion.isTrue)

  /**
   * Asserts that the given test was completed.
   */
  def assertCompletesZIO(implicit trace: Trace, sourceLocation: SourceLocation): UIO[TestResult] =
    ZIO.succeed(assertCompletes)

  /**
   * Asserts that the given test was never completed.
   */
  def assertNever(message: String)(implicit trace: Trace, sourceLocation: SourceLocation): TestResult =
    assertImpl(true)(Assertion.equalTo(false)) ?? message

  /**
   * Checks the test passes for "sufficient" numbers of samples from the given
   * random variable.
   */
  def check[R <: ZAny, A, In](rv: Gen[R, A])(test: A => In)(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    TestConfig.samples.flatMap(n => checkStream(rv.sample.forever.take(n.toLong))(a => checkConstructor(test(a))))

  /**
   * A version of `check` that accepts two random variables.
   */
  def check[R <: ZAny, A, B, In](rv1: Gen[R, A], rv2: Gen[R, B])(
    test: (A, B) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    check(rv1 <*> rv2)(test.tupled)

  /**
   * A version of `check` that accepts three random variables.
   */
  def check[R <: ZAny, A, B, C, In](rv1: Gen[R, A], rv2: Gen[R, B], rv3: Gen[R, C])(
    test: (A, B, C) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    check(rv1 <*> rv2 <*> rv3)(test.tupled)

  /**
   * A version of `check` that accepts four random variables.
   */
  def check[R <: ZAny, A, B, C, D, In](rv1: Gen[R, A], rv2: Gen[R, B], rv3: Gen[R, C], rv4: Gen[R, D])(
    test: (A, B, C, D) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    check(rv1 <*> rv2 <*> rv3 <*> rv4)(test.tupled)

  /**
   * A version of `check` that accepts five random variables.
   */
  def check[R <: ZAny, A, B, C, D, F, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    rv4: Gen[R, D],
    rv5: Gen[R, F]
  )(
    test: (A, B, C, D, F) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    check(rv1 <*> rv2 <*> rv3 <*> rv4 <*> rv5)(test.tupled)

  /**
   * A version of `check` that accepts six random variables.
   */
  def check[R <: ZAny, A, B, C, D, F, G, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    rv4: Gen[R, D],
    rv5: Gen[R, F],
    rv6: Gen[R, G]
  )(
    test: (A, B, C, D, F, G) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    check(rv1 <*> rv2 <*> rv3 <*> rv4 <*> rv5 <*> rv6)(test.tupled)

  /**
   * A version of `check` that accepts seven random variables.
   */
  def check[R <: ZAny, A, B, C, D, F, G, H, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    rv4: Gen[R, D],
    rv5: Gen[R, F],
    rv6: Gen[R, G],
    rv7: Gen[R, H]
  )(
    test: (A, B, C, D, F, G, H) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    check(rv1 <*> rv2 <*> rv3 <*> rv4 <*> rv5 <*> rv6 <*> rv7)(test.tupled)

  /**
   * A version of `check` that accepts eight random variables.
   */
  def check[R <: ZAny, A, B, C, D, F, G, H, I, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    rv4: Gen[R, D],
    rv5: Gen[R, F],
    rv6: Gen[R, G],
    rv7: Gen[R, H],
    rv8: Gen[R, I]
  )(
    test: (A, B, C, D, F, G, H, I) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    check(rv1 <*> rv2 <*> rv3 <*> rv4 <*> rv5 <*> rv6 <*> rv7 <*> rv8)(test.tupled)

  /**
   * Checks the test passes for all values from the given finite, deterministic
   * generator. For non-deterministic or infinite generators use `check` or
   * `checkN`.
   */
  def checkAll[R <: ZAny, A, In](rv: Gen[R, A])(test: A => In)(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkStream(rv.sample)(a => checkConstructor(test(a)))

  /**
   * A version of `checkAll` that accepts two random variables.
   */
  def checkAll[R <: ZAny, A, B, In](rv1: Gen[R, A], rv2: Gen[R, B])(
    test: (A, B) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkAll(rv1 <*> rv2)(test.tupled)

  /**
   * A version of `checkAll` that accepts three random variables.
   */
  def checkAll[R <: ZAny, A, B, C, In](rv1: Gen[R, A], rv2: Gen[R, B], rv3: Gen[R, C])(
    test: (A, B, C) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkAll(rv1 <*> rv2 <*> rv3)(test.tupled)

  /**
   * A version of `checkAll` that accepts four random variables.
   */
  def checkAll[R <: ZAny, A, B, C, D, In](rv1: Gen[R, A], rv2: Gen[R, B], rv3: Gen[R, C], rv4: Gen[R, D])(
    test: (A, B, C, D) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkAll(rv1 <*> rv2 <*> rv3 <*> rv4)(test.tupled)

  /**
   * A version of `checkAll` that accepts five random variables.
   */
  def checkAll[R <: ZAny, A, B, C, D, F, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    rv4: Gen[R, D],
    rv5: Gen[R, F]
  )(
    test: (A, B, C, D, F) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkAll(rv1 <*> rv2 <*> rv3 <*> rv4 <*> rv5)(test.tupled)

  /**
   * A version of `checkAll` that accepts six random variables.
   */
  def checkAll[R <: ZAny, A, B, C, D, F, G, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    rv4: Gen[R, D],
    rv5: Gen[R, F],
    rv6: Gen[R, G]
  )(
    test: (A, B, C, D, F, G) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkAll(rv1 <*> rv2 <*> rv3 <*> rv4 <*> rv5 <*> rv6)(test.tupled)

  /**
   * A version of `checkAll` that accepts seven random variables.
   */
  def checkAll[R <: ZAny, A, B, C, D, F, G, H, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    rv4: Gen[R, D],
    rv5: Gen[R, F],
    rv6: Gen[R, G],
    rv7: Gen[R, H]
  )(
    test: (A, B, C, D, F, G, H) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkAll(rv1 <*> rv2 <*> rv3 <*> rv4 <*> rv5 <*> rv6 <*> rv7)(test.tupled)

  /**
   * A version of `checkAll` that accepts eight random variables.
   */
  def checkAll[R <: ZAny, E, A, B, C, D, F, G, H, I, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    rv4: Gen[R, D],
    rv5: Gen[R, F],
    rv6: Gen[R, G],
    rv7: Gen[R, H],
    rv8: Gen[R, I]
  )(
    test: (A, B, C, D, F, G, H, I) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkAll(rv1 <*> rv2 <*> rv3 <*> rv4 <*> rv5 <*> rv6 <*> rv7 <*> rv8)(test.tupled)

  /**
   * Checks in parallel the effectual test passes for all values from the given
   * random variable. This is useful for deterministic `Gen` that
   * comprehensively explore all possibilities in a given domain.
   */
  def checkAllPar[R <: ZAny, E, A, In](rv: Gen[R, A], parallelism: Int)(
    test: A => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkStreamPar(rv.sample, parallelism)(a => checkConstructor(test(a)))

  /**
   * A version of `checkAllPar` that accepts two random variables.
   */
  def checkAllPar[R <: ZAny, E, A, B, In](rv1: Gen[R, A], rv2: Gen[R, B], parallelism: Int)(
    test: (A, B) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkAllPar(rv1 <*> rv2, parallelism)(test.tupled)

  /**
   * A version of `checkAllPar` that accepts three random variables.
   */
  def checkAllPar[R <: ZAny, E, A, B, C, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    parallelism: Int
  )(
    test: (A, B, C) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkAllPar(rv1 <*> rv2 <*> rv3, parallelism)(test.tupled)

  /**
   * A version of `checkAllPar` that accepts four random variables.
   */
  def checkAllPar[R <: ZAny, E, A, B, C, D, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    rv4: Gen[R, D],
    parallelism: Int
  )(
    test: (A, B, C, D) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkAllPar(rv1 <*> rv2 <*> rv3 <*> rv4, parallelism)(test.tupled)

  /**
   * A version of `checkAllPar` that accepts five random variables.
   */
  def checkAllPar[R <: ZAny, E, A, B, C, D, F, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    rv4: Gen[R, D],
    rv5: Gen[R, F],
    parallelism: Int
  )(
    test: (A, B, C, D, F) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkAllPar(rv1 <*> rv2 <*> rv3 <*> rv4 <*> rv5, parallelism)(test.tupled)

  /**
   * A version of `checkAllPar` that accepts six random variables.
   */
  def checkAllPar[R <: ZAny, E, A, B, C, D, F, G, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    rv4: Gen[R, D],
    rv5: Gen[R, F],
    rv6: Gen[R, G],
    parallelism: Int
  )(
    test: (A, B, C, D, F, G) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkAllPar(rv1 <*> rv2 <*> rv3 <*> rv4 <*> rv5 <*> rv6, parallelism)(test.tupled)

  /**
   * A version of `checkAllPar` that accepts six random variables.
   */
  def checkAllPar[R <: ZAny, E, A, B, C, D, F, G, H, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    rv4: Gen[R, D],
    rv5: Gen[R, F],
    rv6: Gen[R, G],
    rv7: Gen[R, H],
    parallelism: Int
  )(
    test: (A, B, C, D, F, G, H) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkAllPar(rv1 <*> rv2 <*> rv3 <*> rv4 <*> rv5 <*> rv6 <*> rv7, parallelism)(test.tupled)

  /**
   * A version of `checkAllPar` that accepts six random variables.
   */
  def checkAllPar[R <: ZAny, E, A, B, C, D, F, G, H, I, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    rv4: Gen[R, D],
    rv5: Gen[R, F],
    rv6: Gen[R, G],
    rv7: Gen[R, H],
    rv8: Gen[R, I],
    parallelism: Int
  )(
    test: (A, B, C, D, F, G, H, I) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkAllPar(rv1 <*> rv2 <*> rv3 <*> rv4 <*> rv5 <*> rv6 <*> rv7 <*> rv8, parallelism)(test.tupled)

  /**
   * Checks the test passes for the specified number of samples from the given
   * random variable.
   */
  def checkN(n: Int): CheckVariants.CheckN =
    new CheckVariants.CheckN(n)

  /**
   * Checks in parallel the test passes for "sufficient" numbers of samples from
   * the given random variable.
   */
  def checkPar[R <: ZAny, A, In](rv: Gen[R, A], parallelism: Int)(test: A => In)(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    TestConfig.samples.flatMap(n =>
      checkStreamPar(rv.sample.forever.take(n.toLong), parallelism)(a => checkConstructor(test(a)))
    )

  /**
   * A version of `checkPar` that accepts two random variables.
   */
  def checkPar[R <: ZAny, A, B, In](rv1: Gen[R, A], rv2: Gen[R, B], parallelism: Int)(test: (A, B) => In)(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkPar(rv1 <*> rv2, parallelism)(test.tupled)

  /**
   * A version of `checkPar` that accepts three random variables.
   */
  def checkPar[R <: ZAny, A, B, C, In](rv1: Gen[R, A], rv2: Gen[R, B], rv3: Gen[R, C], parallelism: Int)(
    test: (A, B, C) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkPar(rv1 <*> rv2 <*> rv3, parallelism)(test.tupled)

  /**
   * A version of `checkPar` that accepts four random variables.
   */
  def checkPar[R <: ZAny, A, B, C, D, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    rv4: Gen[R, D],
    parallelism: Int
  )(test: (A, B, C, D) => In)(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkPar(rv1 <*> rv2 <*> rv3 <*> rv4, parallelism)(test.tupled)

  /**
   * A version of `checkPar` that accepts five random variables.
   */
  def checkPar[R <: ZAny, A, B, C, D, F, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    rv4: Gen[R, D],
    rv5: Gen[R, F],
    parallelism: Int
  )(
    test: (A, B, C, D, F) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkPar(rv1 <*> rv2 <*> rv3 <*> rv4 <*> rv5, parallelism)(test.tupled)

  /**
   * A version of `checkPar` that accepts six random variables.
   */
  def checkPar[R <: ZAny, A, B, C, D, F, G, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    rv4: Gen[R, D],
    rv5: Gen[R, F],
    rv6: Gen[R, G],
    parallelism: Int
  )(
    test: (A, B, C, D, F, G) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkPar(rv1 <*> rv2 <*> rv3 <*> rv4 <*> rv5 <*> rv6, parallelism)(test.tupled)

  /**
   * A version of `checkPar` that accepts seven random variables.
   */
  def checkPar[R <: ZAny, A, B, C, D, F, G, H, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    rv4: Gen[R, D],
    rv5: Gen[R, F],
    rv6: Gen[R, G],
    rv7: Gen[R, H],
    parallelism: Int
  )(
    test: (A, B, C, D, F, G, H) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkPar(rv1 <*> rv2 <*> rv3 <*> rv4 <*> rv5 <*> rv6 <*> rv7, parallelism)(test.tupled)

  /**
   * A version of `checkPar` that accepts eight random variables.
   */
  def checkPar[R <: ZAny, A, B, C, D, F, G, H, I, In](
    rv1: Gen[R, A],
    rv2: Gen[R, B],
    rv3: Gen[R, C],
    rv4: Gen[R, D],
    rv5: Gen[R, F],
    rv6: Gen[R, G],
    rv7: Gen[R, H],
    rv8: Gen[R, I],
    parallelism: Int
  )(
    test: (A, B, C, D, F, G, H, I) => In
  )(implicit
    checkConstructor: CheckConstructor[R, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
    checkPar(rv1 <*> rv2 <*> rv3 <*> rv4 <*> rv5 <*> rv6 <*> rv7 <*> rv8, parallelism)(test.tupled)

  /**
   * A `Runner` that provides a default testable environment.
   */
  lazy val defaultTestRunner: TestRunner[TestEnvironment, Any] = {
    implicit val trace = Trace.empty
    TestRunner(
      TestExecutor.default(
        testEnvironment,
        Scope.default ++ testEnvironment,
        ExecutionEventSink.live(Console.ConsoleLive, ConsoleEventRenderer),
        ZTestEventHandler.silent // The default test runner handles its own events, writing their output to the provided sink.
      )
    )
  }

  /**
   * Creates a failed test result with the specified runtime cause.
   */
  def failed[E](cause: Cause[E])(implicit trace: Trace): ZIO[Any, TestFailure[E], Nothing] =
    ZIO.fail(TestFailure.Runtime(cause))

  /**
   * Creates an ignored test result.
   */
  val ignored: UIO[TestSuccess] =
    ZIO.succeed(TestSuccess.Ignored())(Trace.empty)

  /**
   * Passes platform specific information to the specified function, which will
   * use that information to create a test. If the platform is neither ScalaJS
   * nor the JVM, an ignored test result will be returned.
   */
  def platformSpecific[R, E, A](js: => A, jvm: => A)(f: A => ZTest[R, E]): ZTest[R, E] =
    if (TestPlatform.isJS) f(js)
    else if (TestPlatform.isJVM) f(jvm)
    else ignored

  /**
   * Builds a suite containing a number of other specs.
   */
  def suite[In](label: String)(specs: In*)(implicit
    suiteConstructor: SuiteConstructor[In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): Spec[suiteConstructor.OutEnvironment, suiteConstructor.OutError] =
    Spec.labeled(
      label,
      if (specs.isEmpty) Spec.empty
      else if (specs.length == 1) {
        wrapIfLabelledCase(specs.head)
      } else Spec.multiple(Chunk.fromIterable(specs).map(spec => suiteConstructor(spec)))
    )

  // Ensures we render suite label when we have an individual Labeled test case
  private def wrapIfLabelledCase[In](spec: In)(implicit suiteConstructor: SuiteConstructor[In], trace: Trace) =
    spec match {
      case Spec(LabeledCase(_, _)) =>
        Spec.multiple(Chunk(suiteConstructor(spec)))
      case _ => suiteConstructor(spec)
    }

  /**
   * Builds a spec with a single test.
   */
  def test[In](label: String)(assertion: => In)(implicit
    testConstructor: TestConstructor[Nothing, In],
    sourceLocation: SourceLocation,
    trace: Trace
  ): testConstructor.Out =
    testConstructor(label)(assertion)

  /**
   * Passes version specific information to the specified function, which will
   * use that information to create a test. If the version is neither Scala 3
   * nor Scala 2, an ignored test result will be returned.
   */
  def versionSpecific[R, E, A](scala3: => A, scala2: => A)(f: A => ZTest[R, E]): ZTest[R, E] =
    if (TestVersion.isScala3) f(scala3)
    else if (TestVersion.isScala2) f(scala2)
    else ignored

  object CheckVariants {

    final class CheckN(private val n: Int) extends AnyVal {
      def apply[R <: ZAny, A, In](rv: Gen[R, A])(test: A => In)(implicit
        checkConstructor: CheckConstructor[R, In],
        trace: Trace
      ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
        checkStream(rv.sample.forever.take(n.toLong))(a => checkConstructor(test(a)))
      def apply[R <: ZAny, A, B, In](rv1: Gen[R, A], rv2: Gen[R, B])(
        test: (A, B) => In
      )(implicit
        checkConstructor: CheckConstructor[R, In],
        trace: Trace
      ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
        checkN(n)(rv1 <*> rv2)(test.tupled)
      def apply[R <: ZAny, A, B, C, In](rv1: Gen[R, A], rv2: Gen[R, B], rv3: Gen[R, C])(
        test: (A, B, C) => In
      )(implicit
        checkConstructor: CheckConstructor[R, In],
        trace: Trace
      ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
        checkN(n)(rv1 <*> rv2 <*> rv3)(test.tupled)
      def apply[R <: ZAny, A, B, C, D, In](rv1: Gen[R, A], rv2: Gen[R, B], rv3: Gen[R, C], rv4: Gen[R, D])(
        test: (A, B, C, D) => In
      )(implicit
        checkConstructor: CheckConstructor[R, In],
        trace: Trace
      ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
        checkN(n)(rv1 <*> rv2 <*> rv3 <*> rv4)(test.tupled)
      def apply[R <: ZAny, A, B, C, D, F, In](
        rv1: Gen[R, A],
        rv2: Gen[R, B],
        rv3: Gen[R, C],
        rv4: Gen[R, D],
        rv5: Gen[R, F]
      )(
        test: (A, B, C, D, F) => In
      )(implicit
        checkConstructor: CheckConstructor[R, In],
        trace: Trace
      ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
        checkN(n)(rv1 <*> rv2 <*> rv3 <*> rv4 <*> rv5)(test.tupled)
      def apply[R <: ZAny, A, B, C, D, F, G, In](
        rv1: Gen[R, A],
        rv2: Gen[R, B],
        rv3: Gen[R, C],
        rv4: Gen[R, D],
        rv5: Gen[R, F],
        rv6: Gen[R, G]
      )(
        test: (A, B, C, D, F, G) => In
      )(implicit
        checkConstructor: CheckConstructor[R, In],
        trace: Trace
      ): ZIO[checkConstructor.OutEnvironment, checkConstructor.OutError, TestResult] =
        checkN(n)(rv1 <*> rv2 <*> rv3 <*> rv4 <*> rv5 <*> rv6)(test.tupled)
    }
  }

  private def checkStream[R, R1 <: R, E, A](stream: ZStream[R, Nothing, Sample[R, A]])(
    test: A => ZIO[R1, E, TestResult]
  )(implicit trace: Trace): ZIO[R1, E, TestResult] =
    testConfigWith { testConfig =>
      val flag = Ref.unsafe.make(false)(Unsafe.unsafe)
      warningEmptyGen(flag) *> shrinkStream {
        stream.zipWithIndex.mapZIO { case (initial, index) =>
          flag.set(true) *> initial.foreach(input =>
            (test(input) @@ testConfig.checkAspect)
              .map(_.setGenFailureDetails(GenFailureDetails(initial.value, input, index)))
              .either
          )
        }
      }(testConfig.shrinks)
    }

  private def shrinkStream[R, R1 <: R, E, A](
    stream: ZStream[R1, Nothing, Sample[R1, Either[E, TestResult]]]
  )(maxShrinks: Int)(implicit trace: Trace): ZIO[R1, E, TestResult] =
    stream
      .dropWhile(!_.value.fold(_ => true, _.isFailure)) // Drop until we get to a failure
      .take(1)                                          // Get the first failure
      .flatMap(_.shrinkSearch(_.fold(_ => true, _.isFailure)).take(maxShrinks.toLong + 1))
      .run(ZSink.collectAll[Either[E, TestResult]]) // Collect all the shrunken values
      .flatMap { shrinks =>
        // Get the "last" failure, the smallest according to the shrinker:
        shrinks
          .filter(_.fold(_ => true, _.isFailure))
          .lastOption
          .fold[ZIO[R, E, TestResult]](
            ZIO.succeed(assertCompletes)
          )(ZIO.fromEither(_))
      }

  private def checkStreamPar[R, R1 <: R, E, A](stream: ZStream[R, Nothing, Sample[R, A]], parallelism: Int)(
    test: A => ZIO[R1, E, TestResult]
  )(implicit trace: Trace): ZIO[R1, E, TestResult] =
    testConfigWith { testConfig =>
      shrinkStream {
        stream.zipWithIndex
          .mapZIOPar(parallelism) { case (initial, index) =>
            initial.foreach { input =>
              (test(input) @@ testConfig.checkAspect)
                .map(_.setGenFailureDetails(GenFailureDetails(initial.value, input, index)))
                .either
            // convert test failures to failures to terminate parallel tests on first failure
            }.flatMap(sample => sample.value.fold(_ => ZIO.fail(sample), _ => ZIO.succeed(sample)))
          // move failures back into success channel for shrinking logic
          }
          .catchAll(ZStream.succeed(_))
      }(testConfig.shrinks)
    }

  private def warningEmptyGen(flag: Ref[Boolean])(implicit trace: Trace): UIO[Unit] =
    Live
      .live(ZIO.logWarning(warning).unlessZIODiscard(flag.get).delay(5.seconds))
      .interruptible
      .fork
      .onExecutor(Runtime.defaultExecutor)
      .unit

  private val warning =
    "Warning: A check / checkAll / checkN generator did not produce any test cases, " +
      "which may result in the test hanging. Ensure that the provided generator is producing values."

  implicit final class TestLensOptionOps[A](private val self: TestLens[Option[A]]) extends AnyVal {

    /**
     * Transforms an [[scala.Option]] to its `Some` value `A`, otherwise fails
     * if it is a `None`.
     */
    def some: TestLens[A] = throw SmartAssertionExtensionError()
  }

  implicit final class TestLensTryOps[A](private val self: TestLens[scala.util.Try[A]]) extends AnyVal {

    /**
     * Transforms an [[scala.util.Try]] to its [[scala.util.Success]] value `A`,
     * otherwise fails if it is a [[scala.util.Failure]].
     */
    def success: TestLens[A] = throw SmartAssertionExtensionError()

    /**
     * Transforms an [[scala.util.Try]] to a [[scala.Throwable]] if it is a
     * [[scala.util.Failure]], otherwise fails.
     */
    def failure: TestLens[Throwable] = throw SmartAssertionExtensionError()
  }

  implicit final class TestLensEitherOps[E, A](private val self: TestLens[Either[E, A]]) extends AnyVal {

    /**
     * Transforms an [[scala.Either]] to its [[scala.Left]] value `E`, otherwise
     * fails if it is a [[scala.Right]].
     */
    def left: TestLens[E] = throw SmartAssertionExtensionError()

    /**
     * Transforms an [[scala.Either]] to its [[scala.Right]] value `A`,
     * otherwise fails if it is a [[scala.Left]].
     */
    def right: TestLens[A] = throw SmartAssertionExtensionError()
  }

  implicit final class TestLensExitOps[E, A](private val self: TestLens[Exit[E, A]]) extends AnyVal {

    /**
     * Transforms an [[Exit]] to a [[scala.Throwable]] if it is a `die`,
     * otherwise fails.
     */
    def die: TestLens[Throwable] = throw SmartAssertionExtensionError()

    /**
     * Transforms an [[Exit]] to its failure type (`E`) if it is a `fail`,
     * otherwise fails.
     */
    def failure: TestLens[E] = throw SmartAssertionExtensionError()

    /**
     * Transforms an [[Exit]] to its success type (`A`) if it is a `succeed`,
     * otherwise fails.
     */
    def success: TestLens[A] = throw SmartAssertionExtensionError()

    /**
     * Transforms an [[Exit]] to its underlying [[Cause]] if it has one,
     * otherwise fails.
     */
    def cause: TestLens[Cause[E]] = throw SmartAssertionExtensionError()

    /**
     * Transforms an [[Exit]] to a boolean value representing whether or not it
     * was interrupted.
     */
    def interrupted: TestLens[Boolean] = throw SmartAssertionExtensionError()
  }

  implicit final class TestLensCauseOps[E](private val self: TestLens[Cause[E]]) extends AnyVal {

    /**
     * Transforms a [[Cause]] to a [[scala.Throwable]] if it is a `die`,
     * otherwise fails.
     */
    def die: TestLens[Throwable] = throw SmartAssertionExtensionError()

    /**
     * Transforms a [[Cause]] to its failure type (`E`) if it is a `fail`,
     * otherwise fails.
     */
    def failure: TestLens[E] = throw SmartAssertionExtensionError()

    /**
     * Transforms a [[Cause]] to a boolean value representing whether or not it
     * was interrupted.
     */
    def interrupted: TestLens[Boolean] = throw SmartAssertionExtensionError()
  }

  implicit final class TestLensAnyOps[A](private val self: TestLens[A]) extends AnyVal {

    /**
     * Always returns true as long the chain of preceding transformations has
     * succeeded.
     *
     * {{{
     *   val option: Either[Int, Option[String]] = Right(Some("Cool"))
     *   assertTrue(option.is(_.right.some.anything)) // returns true
     *   assertTrue(option.is(_.left.anything)) // will fail because of `.left`.
     * }}}
     */
    def anything: TestLens[Boolean] = throw SmartAssertionExtensionError()

    /**
     * Transforms a value of some type into the given `Subtype` if possible,
     * otherwise fails.
     *
     * {{{
     *   sealed trait CustomError
     *   case class Explosion(blastRadius: Int) extends CustomError
     *   case class Melting(degrees: Double) extends CustomError
     *   case class Fulminating(wow: Boolean) extends CustomError
     *
     *   val error: CustomError = Melting(100)
     *   assertTrue(option.is(_.subtype[Melting]).degrees > 10) // succeeds
     *   assertTrue(option.is(_.subtype[Explosion]).blastRadius == 12) // fails
     * }}}
     */
    def subtype[Subtype <: A]: TestLens[Subtype] = throw SmartAssertionExtensionError()

    /**
     * Transforms a value with the given [[CustomAssertion]]
     */
    def custom[B](customAssertion: CustomAssertion[A, B]): TestLens[B] = {
      val _ = customAssertion
      throw SmartAssertionExtensionError()
    }
  }

  implicit final class SmartAssertionOps[A](private val self: A) extends AnyVal {

    /**
     * This extension method can only be called inside of the `assertTrue`
     * method. It will transform the value using the given [[TestLens]].
     *
     * {{{
     *   val option: Either[Int, Option[String]] = Right(Some("Cool"))
     *   assertTrue(option.is(_.right.some) == "Cool") // returns true
     *   assertTrue(option.is(_.left) < 100) // will fail because of `.left`.
     * }}}
     */
    def is[B](f: TestLens[A] => TestLens[B]): B = {
      val _ = f
      throw SmartAssertionExtensionError()
    }
  }

  implicit final class TestResultZIOOps[R, E](private val self: ZIO[R, E, TestResult]) extends AnyVal {
    def &&[R1 <: R, E1 >: E](that: => ZIO[R1, E1, TestResult])(implicit trace: Trace): ZIO[R1, E1, TestResult] =
      self.zipWith(that)(_ && _)
    def ||[R1 <: R, E1 >: E](that: => ZIO[R1, E1, TestResult])(implicit trace: Trace): ZIO[R1, E1, TestResult] =
      self.zipWith(that)(_ || _)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy