zio.test.Spec.scala Maven / Gradle / Ivy
/*
* 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 zio.test.Spec._
/**
* A `Spec[R, E]` is the backbone of _ZIO Test_. Every spec is either a suite,
* which contains other specs, or a test. All specs require an environment of
* type `R` and may potentially fail with an error of type `E`.
*/
final case class Spec[-R, +E](caseValue: SpecCase[R, E, Spec[R, E]]) extends SpecVersionSpecific[R, E] {
self =>
/**
* Combines this spec with the specified spec.
*/
def +[R1 <: R, E1 >: E](that: Spec[R1, E1]): Spec[R1, E1] =
(self.caseValue, that.caseValue) match {
case (MultipleCase(self), MultipleCase(that)) => Spec.multiple(self ++ that)
case (MultipleCase(self), _) => Spec.multiple(self :+ that)
case (_, MultipleCase(that)) => Spec.multiple(self +: that)
case _ => Spec.multiple(Chunk(self, that))
}
/**
* Syntax for adding aspects.
* {{{
* test("foo") { assert(42, equalTo(42)) } @@ ignore
* }}}
*/
final def @@[R0 <: R1, R1 <: R, E0 >: E, E1 >: E0](aspect: TestAspect[R0, R1, E0, E1])(implicit
trace: Trace
): Spec[R1, E0] =
aspect(self)
/**
* Annotates each test in this spec with the specified test annotation.
*/
final def annotate[V](key: TestAnnotation[V], value: V)(implicit trace: Trace): Spec[R, E] =
transform[R, E] {
case TestCase(test, annotations) => Spec.TestCase(test, annotations.annotate(key, value))
case c => c
}
/**
* Returns a new spec with the annotation map at each node.
*/
final def annotated(implicit trace: Trace): Spec[R, E] =
transform[R, E] {
case ExecCase(exec, spec) => ExecCase(exec, spec)
case LabeledCase(label, spec) => LabeledCase(label, spec)
case ScopedCase(scoped) =>
ScopedCase[R, E, Spec[R, E]](
scoped
)
case MultipleCase(specs) => MultipleCase(specs)
case TestCase(test, annotations) => TestCase(Annotations.withAnnotation(test), annotations)
}
/**
* Returns an effect that models execution of this spec.
*/
final def execute(defExec: ExecutionStrategy)(implicit
trace: Trace
): ZIO[R with Scope, Nothing, Spec[Any, E]] =
ZIO.environmentWithZIO(provideEnvironment(_).foreachExec(defExec)(Exit.failCause, ZIO.successFn))
/**
* Returns a new spec with only those tests with annotations satisfying the
* specified predicate. If no annotations satisfy the specified predicate then
* returns `Some` with an empty suite if this is a suite or `None` otherwise.
*/
final def filterAnnotations[V](
key: TestAnnotation[V]
)(f: V => Boolean)(implicit trace: Trace): Option[Spec[R, E]] =
caseValue match {
case ExecCase(exec, spec) =>
spec.filterAnnotations(key)(f).map(spec => Spec.exec(exec, spec))
case LabeledCase(label, spec) =>
spec.filterAnnotations(key)(f).map(spec => Spec.labeled(label, spec))
case ScopedCase(scoped) =>
Some(Spec.scoped[R](scoped.map(_.filterAnnotations(key)(f).getOrElse(Spec.empty))))
case MultipleCase(specs) =>
val filtered = specs.flatMap(_.filterAnnotations(key)(f))
if (filtered.isEmpty) None else Some(Spec.multiple(filtered))
case TestCase(test, annotations) =>
if (f(annotations.get(key))) Some(Spec.test(test, annotations)) else None
}
/**
* Returns a new spec with only those suites and tests satisfying the
* specified predicate. If a suite label satisfies the predicate the entire
* suite will be included in the new spec. Otherwise only those specs in a
* suite that satisfy the specified predicate will be included in the new
* spec. If no labels satisfy the specified predicate then returns `Some` with
* an empty suite if this is a suite or `None` otherwise.
*/
final def filterLabels(f: String => Boolean)(implicit trace: Trace): Option[Spec[R, E]] =
caseValue match {
case ExecCase(exec, spec) =>
spec.filterLabels(f).map(spec => Spec.exec(exec, spec))
case LabeledCase(label, spec) =>
if (f(label)) Some(Spec.labeled(label, spec))
else spec.filterLabels(f).map(spec => Spec.labeled(label, spec))
case ScopedCase(scoped) =>
Some(Spec.scoped[R](scoped.map(_.filterLabels(f).getOrElse(Spec.empty))))
case MultipleCase(specs) =>
val filtered = specs.flatMap(_.filterLabels(f))
if (filtered.isEmpty) None else Some(Spec.multiple(filtered))
case TestCase(_, _) =>
None
}
/**
* Returns a new spec with only those suites and tests with tags satisfying
* the specified predicate. If no tags satisfy the specified predicate then
* returns `Some` with an empty suite with the root label if this is a suite
* or `None` otherwise.
*/
final def filterTags(f: String => Boolean)(implicit trace: Trace): Option[Spec[R, E]] =
filterAnnotations(TestAnnotation.tagged)(_.exists(f))
/**
* Returns a new spec with only those suites and tests except for the ones
* with tags satisfying the predicate. If all tests or suites have tags that
* satisfy the specified predicate then returns `Some` with an empty suite
* with the root label if this is a suite or `None` otherwise.
*/
final def filterNotTags(f: String => Boolean)(implicit trace: Trace): Option[Spec[R, E]] =
filterAnnotations(TestAnnotation.tagged)(!_.exists(f))
/**
* Effectfully folds over all nodes according to the execution strategy of
* suites, utilizing the specified default for other cases.
*/
final def foldScoped[R1 <: R, E1, Z](
defExec: ExecutionStrategy
)(f: SpecCase[R, E, Z] => ZIO[R1 with Scope, E1, Z])(implicit trace: Trace): ZIO[R1 with Scope, E1, Z] =
caseValue match {
case ExecCase(exec, spec) => spec.foldScoped[R1, E1, Z](exec)(f).flatMap(z => f(ExecCase(exec, z)))
case LabeledCase(label, spec) => spec.foldScoped[R1, E1, Z](defExec)(f).flatMap(z => f(LabeledCase(label, z)))
case ScopedCase(scoped) =>
scoped.foldCauseZIO(
c => f(ScopedCase(Exit.failCause(c))),
spec => spec.foldScoped[R1, E1, Z](defExec)(f).flatMap(z => f(ScopedCase(ZIO.succeed(z))))
)
case MultipleCase(specs) =>
ZIO
.foreachExec(specs)(defExec)(spec => ZIO.scoped[R1](spec.foldScoped[R1, E1, Z](defExec)(f)))
.flatMap(zs => f(MultipleCase(zs)))
case t @ TestCase(_, _) => f(t)
}
/**
* Iterates over the spec with the specified default execution strategy, and
* effectfully transforming every test with the provided function, finally
* reconstructing the spec with the same structure.
*/
final def foreachExec[R1 <: R, E1](
defExec: ExecutionStrategy
)(
failure: Cause[TestFailure[E]] => ZIO[R1, TestFailure[E1], TestSuccess],
success: TestSuccess => ZIO[R1, E1, TestSuccess]
)(implicit
trace: Trace
): ZIO[R1 with Scope, Nothing, Spec[R1, E1]] =
foldScoped[R1, Nothing, Spec[R1, E1]](defExec) {
case ExecCase(exec, spec) => ZIO.succeed(Spec.exec(exec, spec))
case LabeledCase(label, spec) => ZIO.succeed(Spec.labeled(label, spec))
case ScopedCase(scoped) =>
scoped.foldCause(
c => Spec.test(failure(c), TestAnnotationMap.empty),
t => Spec.scoped(ZIO.succeed(t))
)
case MultipleCase(specs) => ZIO.succeed(Spec.multiple(specs))
case TestCase(test, annotations) =>
test
.foldCause(
e => Spec.test(failure(e), annotations),
t => Spec.test(success(t).mapError(TestFailure.fail), annotations)
)
}
/**
* Iterates over the spec with the sequential strategy as the default, and
* effectfully transforming every test with the provided function, finally
* reconstructing the spec with the same structure.
*/
final def foreach[R1 <: R, E1](
failure: Cause[TestFailure[E]] => ZIO[R1, TestFailure[E1], TestSuccess],
success: TestSuccess => ZIO[R1, E1, TestSuccess]
)(implicit trace: Trace): ZIO[R1 with Scope, Nothing, Spec[R1, E1]] =
foreachExec(ExecutionStrategy.Sequential)(failure, success)
/**
* Iterates over the spec with the parallel strategy as the default, and
* effectfully transforming every test with the provided function, finally
* reconstructing the spec with the same structure.
*/
final def foreachPar[R1 <: R, E1](
failure: Cause[TestFailure[E]] => ZIO[R1, TestFailure[E1], TestSuccess],
success: TestSuccess => ZIO[R1, E1, TestSuccess]
)(implicit trace: Trace): ZIO[R1 with Scope, Nothing, Spec[R1, E1]] =
foreachExec(ExecutionStrategy.Parallel)(failure, success)
/**
* Iterates over the spec with the parallel (`n`) strategy as the default, and
* effectfully transforming every test with the provided function, finally
* reconstructing the spec with the same structure.
*/
final def foreachParN[R1 <: R, E1](
n: Int
)(
failure: Cause[TestFailure[E]] => ZIO[R1, TestFailure[E1], TestSuccess],
success: TestSuccess => ZIO[R1, E1, TestSuccess]
)(implicit
trace: Trace
): ZIO[R1 with Scope, Nothing, Spec[R1, E1]] =
foreachExec(ExecutionStrategy.ParallelN(n))(failure, success)
/**
* Returns a new spec with remapped errors.
*/
final def mapError[E1](f: E => E1)(implicit ev: CanFail[E], trace: Trace): Spec[R, E1] =
transform[R, E1] {
case ExecCase(exec, spec) => ExecCase(exec, spec)
case LabeledCase(label, spec) => LabeledCase(label, spec)
case ScopedCase(scoped) => ScopedCase[R, E1, Spec[R, E1]](scoped.mapError(_.map(f)))
case MultipleCase(specs) => MultipleCase(specs)
case TestCase(test, annotations) => TestCase(test.mapError(_.map(f)), annotations)
}
/**
* Returns a new spec with remapped labels.
*/
final def mapLabel(f: String => String)(implicit trace: Trace): Spec[R, E] =
transform[R, E] {
case ExecCase(exec, spec) => ExecCase(exec, spec)
case LabeledCase(label, spec) => LabeledCase(f(label), spec)
case ScopedCase(scoped) => ScopedCase[R, E, Spec[R, E]](scoped)
case MultipleCase(specs) => MultipleCase(specs)
case TestCase(test, annotations) => TestCase(test, annotations)
}
/**
* Provides each test with the part of the environment that is not part of the
* `TestEnvironment`, leaving a spec that only depends on the
* `TestEnvironment`.
*
* {{{
* val loggingLayer: ZLayer[Any, Nothing, Logging] = ???
*
* val spec: Spec[TestEnvironment with Logging, Nothing] = ???
*
* val spec2 = spec.provideCustomLayer(loggingLayer)
* }}}
*/
@deprecated("use provideLayer", "2.0.2")
def provideCustomLayer[E1 >: E, R1](layer: ZLayer[TestEnvironment, E1, R1])(implicit
ev: TestEnvironment with R1 <:< R,
tagged: EnvironmentTag[R1],
trace: Trace
): Spec[TestEnvironment, E1] =
provideSomeLayer[TestEnvironment](layer)
/**
* Provides all tests with a shared version of the part of the environment
* that is not part of the `TestEnvironment`, leaving a spec that only depends
* on the `TestEnvironment`.
*
* {{{
* val loggingLayer: ZLayer[Any, Nothing, Logging] = ???
*
* val spec: Spec[TestEnvironment with Logging, Nothing] = ???
*
* val spec2 = spec.provideCustomLayerShared(loggingLayer)
* }}}
*/
@deprecated("use provideLayerShared", "2.0.2")
def provideCustomLayerShared[E1 >: E, R1](layer: ZLayer[TestEnvironment, E1, R1])(implicit
ev: TestEnvironment with R1 <:< R,
tagged: EnvironmentTag[R1],
trace: Trace
): Spec[TestEnvironment, E1] =
provideSomeLayerShared(layer)
/**
* Provides each test in this spec with its required environment
*/
final def provideEnvironment(r: ZEnvironment[R])(implicit trace: Trace): Spec[Any, E] =
provideSomeEnvironment(_ => r)
/**
* Provides a layer to the spec, translating it up a level.
*/
final def provideLayer[E1 >: E, R0](
layer: ZLayer[R0, E1, R]
)(implicit trace: Trace): Spec[R0, E1] =
transform[R0, E1] {
case ExecCase(exec, spec) => ExecCase(exec, spec)
case LabeledCase(label, spec) => LabeledCase(label, spec)
case ScopedCase(scoped) =>
ScopedCase[R0, E1, Spec[R0, E1]](
layer.mapError(TestFailure.fail).build.flatMap(r => scoped.provideSomeEnvironment[Scope](r.union[Scope](_)))
)
case MultipleCase(specs) => MultipleCase(specs)
case TestCase(test, annotations) => TestCase(test.provideLayer(layer.mapError(TestFailure.fail)), annotations)
}
/**
* Provides a layer to the spec, sharing services between all tests.
*/
final def provideLayerShared[E1 >: E, R0](
layer: ZLayer[R0, E1, R]
)(implicit trace: Trace): Spec[R0, E1] =
caseValue match {
case ExecCase(exec, spec) => Spec.exec(exec, spec.provideLayerShared(layer))
case LabeledCase(label, spec) => Spec.labeled(label, spec.provideLayerShared(layer))
case ScopedCase(scoped) =>
Spec.scoped[R0](
layer
.mapError(TestFailure.fail)
.build
.flatMap(r => scoped.map(_.provideEnvironment(r)).provideSomeEnvironment[Scope](r.union[Scope]))
)
case MultipleCase(specs) =>
Spec.scoped[R0](
layer.mapError(TestFailure.fail).build.map(r => Spec.multiple(specs.map(_.provideEnvironment(r))))
)
case TestCase(test, annotations) => Spec.test(test.provideLayer(layer.mapError(TestFailure.fail)), annotations)
}
/**
* Transforms the environment being provided to each test in this spec with
* the specified function.
*/
final def provideSomeEnvironment[R0](
f: ZEnvironment[R0] => ZEnvironment[R]
)(implicit trace: Trace): Spec[R0, E] =
transform[R0, E] {
case ExecCase(exec, spec) => ExecCase(exec, spec)
case LabeledCase(label, spec) => LabeledCase(label, spec)
case ScopedCase(scoped) =>
ScopedCase[R0, E, Spec[R0, E]](
scoped.provideSomeEnvironment[R0 with Scope](in => f(in).add[Scope](in.get[Scope]))
)
case MultipleCase(specs) => MultipleCase(specs)
case TestCase(test, annotations) => TestCase(test.provideSomeEnvironment(f), annotations)
}
/**
* Splits the environment into two parts, providing each test with one part
* using the specified layer and leaving the remainder `R0`.
*
* {{{
* val clockLayer: ZLayer[Any, Nothing, Clock] = ???
*
* val spec: Spec[Clock with Random, Nothing] = ???
*
* val spec2 = spec.provideSomeLayer[Random](clockLayer)
* }}}
*/
final def provideSomeLayer[R0]: Spec.ProvideSomeLayer[R0, R, E] =
new Spec.ProvideSomeLayer[R0, R, E](self)
/**
* Splits the environment into two parts, providing all tests with a shared
* version of one part using the specified layer and leaving the remainder
* `R0`.
*
* {{{
* val clockLayer: ZLayer[Any, Nothing, Clock] = ???
*
* val spec: Spec[Clock with Random, Nothing] = ???
*
* val spec2 = spec.provideSomeLayerShared[Random](clockLayer)
* }}}
*/
final def provideSomeLayerShared[R0]: Spec.ProvideSomeLayerShared[R0, R, E] =
new Spec.ProvideSomeLayerShared[R0, R, E](self)
/**
* Transforms the spec one layer at a time.
*/
final def transform[R1, E1](
f: SpecCase[R, E, Spec[R1, E1]] => SpecCase[R1, E1, Spec[R1, E1]]
)(implicit trace: Trace): Spec[R1, E1] =
caseValue match {
case ExecCase(exec, spec) => Spec(f(ExecCase(exec, spec.transform(f))))
case LabeledCase(label, spec) => Spec(f(LabeledCase(label, spec.transform(f))))
case ScopedCase(scoped) => Spec(f(ScopedCase[R, E, Spec[R1, E1]](scoped.map(_.transform[R1, E1](f)))))
case MultipleCase(specs) => Spec(f(MultipleCase(specs.map(_.transform(f)))))
case t @ TestCase(_, _) => Spec(f(t))
}
/**
* Updates a service in the environment of this effect.
*/
final def updateService[M] =
new Spec.UpdateService[R, E, M](self)
/**
* Updates a service at the specified key in the environment of this effect.
*/
final def updateServiceAt[Service]: Spec.UpdateServiceAt[R, E, Service] =
new Spec.UpdateServiceAt[R, E, Service](self)
/**
* Runs the spec only if the specified predicate is satisfied.
*/
final def when(
b: => Boolean
)(implicit trace: Trace): Spec[R, E] =
whenZIO(ZIO.succeed(b))
/**
* Runs the spec only if the specified effectual predicate is satisfied.
*/
final def whenZIO[R1 <: R, E1 >: E](
b: ZIO[R1, E1, Boolean]
)(implicit trace: Trace): Spec[R1, E1] =
caseValue match {
case ExecCase(exec, spec) =>
Spec.exec(exec, spec.whenZIO(b))
case LabeledCase(label, spec) =>
Spec.labeled(label, spec.whenZIO(b))
case ScopedCase(scoped) =>
Spec.scoped[R1](
b.mapError(TestFailure.fail).flatMap { b =>
if (b) scoped
else ZIO.succeed(Spec.empty)
}
)
case MultipleCase(specs) =>
Spec.multiple(specs.map(_.whenZIO(b)))
case TestCase(test, annotations) =>
Spec.test(
b.mapError(TestFailure.fail).flatMap { b =>
if (b) test
else Annotations.annotate(TestAnnotation.ignored, 1).as(TestSuccess.Ignored())
},
annotations
)
}
}
object Spec {
sealed abstract class SpecCase[-R, +E, +A] { self =>
final def map[B](f: A => B)(implicit trace: Trace): SpecCase[R, E, B] = self match {
case ExecCase(label, spec) => ExecCase(label, f(spec))
case LabeledCase(label, spec) => LabeledCase(label, f(spec))
case ScopedCase(scoped) => ScopedCase[R, E, B](scoped.map(f))
case MultipleCase(specs) => MultipleCase(specs.map(f))
case TestCase(test, annotations) => TestCase(test, annotations)
}
}
final case class ExecCase[+Spec](exec: ExecutionStrategy, spec: Spec) extends SpecCase[Any, Nothing, Spec]
final case class LabeledCase[+Spec](label: String, spec: Spec) extends SpecCase[Any, Nothing, Spec]
final case class ScopedCase[-R, +E, +Spec](scoped: ZIO[Scope with R, TestFailure[E], Spec])
extends SpecCase[R, E, Spec]
final case class MultipleCase[+Spec](specs: Chunk[Spec]) extends SpecCase[Any, Nothing, Spec]
final case class TestCase[-R, +E](test: ZIO[R, TestFailure[E], TestSuccess], annotations: TestAnnotationMap)
extends SpecCase[R, E, Nothing]
final def exec[R, E](exec: ExecutionStrategy, spec: Spec[R, E]): Spec[R, E] =
Spec(ExecCase(exec, spec))
final def labeled[R, E](label: String, spec: Spec[R, E]): Spec[R, E] =
Spec(LabeledCase(label, spec))
final def scoped[R]: ScopedPartiallyApplied[R] =
new ScopedPartiallyApplied[R]
final def multiple[R, E](specs: Chunk[Spec[R, E]]): Spec[R, E] =
Spec(MultipleCase(specs))
final def test[R, E](test: ZIO[R, TestFailure[E], TestSuccess], annotations: TestAnnotationMap)(implicit
trace: Trace
): Spec[R, E] =
Spec(TestCase(test, annotations))
val empty: Spec[Any, Nothing] =
Spec.multiple(Chunk.empty)
final class ProvideSomeLayer[R0, -R, +E](private val self: Spec[R, E]) extends AnyVal {
def apply[E1 >: E, R1](
layer: ZLayer[R0, E1, R1]
)(implicit
ev: R0 with R1 <:< R,
tagged: EnvironmentTag[R1],
trace: Trace
): Spec[R0, E1] =
self.asInstanceOf[Spec[R0 with R1, E]].provideLayer(ZLayer.environment[R0] ++ layer)
}
final class ProvideSomeLayerShared[R0, -R, +E](private val self: Spec[R, E]) extends AnyVal {
def apply[E1 >: E, R1](
layer: ZLayer[R0, E1, R1]
)(implicit
ev: R0 with R1 <:< R,
tagged: EnvironmentTag[R1],
trace: Trace
): Spec[R0, E1] =
self.caseValue match {
case ExecCase(exec, spec) => Spec.exec(exec, spec.provideSomeLayerShared(layer))
case LabeledCase(label, spec) => Spec.labeled(label, spec.provideSomeLayerShared(layer))
case ScopedCase(scoped) =>
Spec.scoped[R0](
layer.mapError(TestFailure.fail).build.flatMap { r =>
scoped
.map(_.provideSomeLayer[R0](ZLayer.succeedEnvironment(r)))
.provideSomeEnvironment[R0 with Scope](in => in.union[R1](r).asInstanceOf[ZEnvironment[R with Scope]])
}
)
case MultipleCase(specs) =>
Spec.scoped[R0](
layer
.mapError(TestFailure.fail)
.build
.map(r => Spec.multiple(specs.map(_.provideSomeLayer[R0](ZLayer.succeedEnvironment(r)))))
)
case TestCase(test, annotations) =>
Spec.test(test.provideSomeLayer(layer.mapError(TestFailure.fail)), annotations)
}
}
final class ScopedPartiallyApplied[R](private val dummy: Boolean = true) extends AnyVal {
def apply[E, T](scoped: => ZIO[Scope with R, TestFailure[E], Spec[R, E]]): Spec[R, E] =
Spec(ScopedCase[R, E, Spec[R, E]](scoped))
}
final class UpdateService[-R, +E, M](private val self: Spec[R, E]) extends AnyVal {
def apply[R1 <: R with M](
f: M => M
)(implicit tag: Tag[M], trace: Trace): Spec[R1, E] =
self.provideSomeEnvironment(_.update(f))
}
final class UpdateServiceAt[-R, +E, Service](private val self: Spec[R, E]) extends AnyVal {
def apply[R1 <: R with Map[Key, Service], Key](key: => Key)(
f: Service => Service
)(implicit tag: Tag[Map[Key, Service]], trace: Trace): Spec[R1, E] =
self.provideSomeEnvironment(_.updateAt(key)(f))
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy