-
Couldn't load subscription status.
- Fork 1.4k
ZIO Test: Support Managed Resources Per Suite #1664
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
This can definitely work, but are we sure we want to go down the route of requiring a different "base class" for this sort of usecase? Looks pretty different compared to how the rest of the library is built around plain values :-) |
|
@iravid I hear you. I think we are limited by our current encoding right now because individual tests are effects but suites aren't. So if I want to perform effects across an entire suite I have to modify the logic of the executor that is executing the tests, instead of just being able to operate on the tests themselves. I think this is definitely an indicator that we may not have the optimal encoding yet. I think the solution would be to make suites themselves effectual in addition to tests, but I get the sense that @jdegoes is not bought into this. So I am conscious that you need a solution to this problem and I don't want to hold you up when I don't have a clear path forward on a better approach. |
|
@adamgfraser @iravid I am not a fan of new specs. I think effectful suites is preferable, because then we can accomplish per-suite provision with Already, we can do per-test provision with suiteM("my suite") { ... } @@ TestAspect.provided(env)The goal should be to enhance aspects until they can do this for the whole suite, rather than just per test, i.e. achieve cross-test sharing within a suite; and this requires effectful suites, which is something we have already discussed and seems to be broadly useful. What do you two think? |
|
@adamgfraser @jdegoes Yes, effectful suites definitely look like the right way to go. Thanks guys 🙏🏻 |
|
@jdegoes I just pushed a commit to make suites effectual. I think everything went relatively smoothly, but a couple of things I'm thinking about:
Appreciate any thoughts you have as always. |
| sealed trait SpecCase[+L, +T, +A] { self => | ||
| final def map[B](f: A => B): SpecCase[L, T, B] = self match { | ||
| case SuiteCase(label, specs, exec) => SuiteCase(label, specs.map(f), exec) | ||
| sealed trait SpecCase[-R, +E, +L, +T, +A] { self => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see you propagated R and E into SpecCase. A lot of type parameters but inference should be extremely good due to lower order nature + variance annotations.
What the design really wants to be is:
sealed trait SpecCase[F[_], +L, +T, +A]So we can have:
final case class Effect[F[_], +L, +T, +A](fa: F[A]) extends SpecCase[F, L, T, A]where in a common case, F[A] = ZIO[R, E, A]. And in another case, F[A] = A.
The problem is ZIO eschews higher-kinded types for teachability / inference, so doing so here would be rather inconsistent with the rest of ZIO, and is almost certain to diminish inference. Although I suppose we could test it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I was thinking the same thing but if we managed to do functional effects and streaming without higher order types it seems like we should be able to do make it work for testing. 😃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, we should keep trying. 😄
The tests are effectful, which means they need to be flattened into the overall suite effect before provision (otherwise out-of-ordering problems will occur). I think this would be simpler with a separate |
|
@jdegoes Yes, I think the next step is for me to rework this with the |
I think this makes sense... and this is the one thing to sacrifice without much user-level pain (users won't really notice this given the API; maybe power users). |
|
@jdegoes I refactored the effectual suites along the lines we discussed and now have a working prototype of providing a managed resource once to an entire suite. It is still a little rough around the edges but we are getting closer. A few things I am thinking about: 1. Handling suite level errors - Errors in suite level effects are pretty dangerous because unlike the prior iteration, if a suite fails we don't even have a label for the suite. In the previous version when we executed a Spec if there was a failure in the effect I took the label and the failure and constructed a new test node with a runtime exception. Now we can't really do anything with such a failure except log it since we are polymorphic in Other thoughts? |
|
@jdegoes The other thing I realized here is I don't think we can currently provide an environment through test aspects because the test aspect can enlarge the types but can't narrow them. |
|
@jdegoes This is ready for review. |
|
One idea on this. What if we said that all tests were effectual? Then we could have: final case class SuiteCase[-R, +E, +L, +T, +A](label: L, specs: ZIO[R, E, Vector[A]], exec: Option[ExecutionStrategy]) extends SpecCase[R, E, L, T, A]
final case class TestCase[-R, +E, L, T](label: L, test: ZIO[R, E, T]) extends SpecCase[R, E, L, T, Nothing]Advantages: (1) We unify the suite and test level environment and error types, (2) we maintain the guarantee that all test nodes in I know there is a ton of other stuff going on right now so no rush in responding. Just something to think about. |
This is true, although combinators can narrow them, e.g. Will address other things soon, sorry for delay, been super busy with hackathon! |
|
@jdegoes Yes, I don't think it is a huge limitation. It just means that some things like providing resources will need to be done through methods on No worries at all. I know that the hackathon and ZIO 1.0 release is top priority right now. There will be plenty of time for more work on ZIO Test after that. |
I think this makes a lot of sense. It's a simpler representation than the one that factors out the effect, and more uniform. Less flexile but the ergonomics will be higher. I think it's the right set of tradeoffs. |
|
@jdegoes I'm on it! |
| ): UIO[Seq[ZTestEvent]] = | ||
| executedSpec.mapLabel(_.toString).fold[UIO[Seq[ZTestEvent]]] { | ||
| case Spec.SuiteCase(_, results, _) => | ||
| results.flatMap(UIO.collectAll(_).map(_.flatten)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
collectAll makes a choice of sequentiality—intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My thought was that since we are already dealing with an executedSpec here the potentially compute intensive work of running the test has already been done so we don't need parallelism here.
| hasFailures = failures.exists(identity) | ||
| status = if (hasFailures) Failed else Passed | ||
| renderedLabel = if (hasFailures) renderFailureLabel(label, depth) else renderSuccessLabel(label, depth) | ||
| rest <- UIO.foreach(specs)(loop(_, depth + tabSize)).map(_.flatten) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed to sequential here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar thought to the above that since we've already run the tests we don't need parallelism in rendering the results.
| rendered(Suite, label, status, depth, renderedLabel) +: executedSpecs.flatMap(loop(_, depth + tabSize)) | ||
| for { | ||
| specs <- executedSpecs | ||
| failures <- UIO.foreach(specs)(_.exists { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed to sequential here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above.
| final def exists(f: SpecCase[L, T, Unit] => Boolean): Boolean = | ||
| fold[Boolean] { | ||
| case c @ SuiteCase(_, specs, _) => specs.exists(identity) || f(c.map(_ => ())) | ||
| final def exists[R1 <: R, E1 >: E](f: SpecCase[R, E, L, T, Any] => ZIO[R1, E1, Boolean]): ZIO[R1, E1, Boolean] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This maybe should be called existsM?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes we can definitely do that.
| case c @ SuiteCase(_, specs, _) => specs.forall(identity) && f(c.map(_ => ())) | ||
| final def forall[R1 <: R, E1 >: E](f: SpecCase[R, E, L, T, Any] => ZIO[R1, E1, Boolean]): ZIO[R1, E1, Boolean] = | ||
| fold[ZIO[R1, E1, Boolean]] { | ||
| case c @ SuiteCase(_, specs, _) => specs.flatMap(ZIO.collectAll(_).map(_.forall(identity))).zipWith(f(c))(_ && _) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sequential specialization. Possibly look at the strategy in the node?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could. The only time this method is used in the code base today is on executed specs. This goes a little to some of the other points about the value of these methods on specs that haven't been executed now that the suites are effectual.
| case TestCase(_, _) => 1 | ||
| final def provideManaged[E1 >: E](managed: Managed[E1, R]): Spec[Any, E1, L, T] = | ||
| transform[Any, E1, L, T] { | ||
| case SuiteCase(label, specs, exec) => SuiteCase(label, specs.provideManaged(managed), exec) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like transform is awfully low-level for this, do we have anything that just allows function application to specs and test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. How about something like:
final def mapTests[R1, E1, L1 >: L, T1](
suiteCase: ZIO[R, E, Vector[Spec[R1, E1, L1, T1]]] => ZIO[R1, E1, Vector[Spec[R1, E1, L1, T1]]],
testCase: ZIO[R, E, T] => ZIO[R1, E1, T1]
): Spec[R1, E1, L1, T1] =
caseValue match {
case SuiteCase(label, specs, exec) =>
Spec.suite(label, suiteCase(specs.map(_.map(_.mapTests(suiteCase, testCase)))), exec)
case TestCase(label, test) => Spec.test(label, testCase(test))
}Then we could do:
final def provideManaged[E1 >: E](managed: Managed[E1, R]): Spec[Any, E1, L, T] =
mapTests[Any, E1, L, T](_.provideManaged(managed), _.provideManaged(managed))| * act of creating the environment is expensive and should only be performed | ||
| * once. | ||
| */ | ||
| final def provideManagedSuite[E1 >: E](managed: Managed[E1, R]): Spec[Any, E1, L, T] = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about provideManagedShared if this is global sharing? provideManagedSuite suggests operation at the level of a suite, maybe each suite (i.e. each nested suite gets its own resource).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! Changed!
| * once. | ||
| */ | ||
| final def provideManagedSuite[E1 >: E](managed: Managed[E1, R]): Spec[Any, E1, L, T] = { | ||
| def loop(r: R)(spec: Spec[R, E, L, T]): ZIO[Any, E, Spec[Any, E, L, T]] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like this loop suggests the need for another traversal scheme.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. I've been struggling a bit to find the right encoding. I think what we want is a function that takes a ZIO[R, E, T] in the test case to a ZIO[R1, E1, T], where there are no constrains on R1 and E1 being subtypes / supertypes of R and E. And then I think for the suite case we basically need a universally quantified function that takes a ZIO[R, E, X] to a ZIO[R1, E1, X], so the suite case can change the effect but should delegate changing the child specs to recursively calling the traversal. Otherwise we run into issues with the environment and error types getting widened to the union of R and R1 and E and E1 instead of just getting R1 and E.
| /** | ||
| * Computes the size of the spec, i.e. the number of tests in the spec. | ||
| */ | ||
| final def size: ZIO[R, E, Int] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder just how useful all these become in an effectful world. Basically computing the size here could, in theory, allocate and de-allocate resources required only to run the tests!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I totally agree. I wonder if we move these to some type of syntax class for executed Specs or require implicit evidence that the spec has been executed. The only variants of these fold type methods we use are exists and forall in checking if all executed specs passed.
| * `ZTest` and the new encoding. We can remove them once we refactor test | ||
| * aspects to use the new encoding | ||
| */ | ||
| private def to[R, E, S]( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this be part of this pull request, or come along later?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was planning on doing it as part of this pull request, I just wanted to make sure we were aligned before I went through and changed all the test aspects. The one thing that may change this is if we are going to do a release today it could be nice to get this in so that projects that need the shared resource functionality can start using ZIO Test, even if we need to do a little cleanup on the backend afterwards.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will have this done shortly.
| */ | ||
| final def fail[E](cause: Cause[E]): ZTest[Any, E, Nothing] = | ||
| ZIO.fail(TestFailure.Runtime(cause)) | ||
| ZIO.halt(cause) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice simplification! 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a few minor questions / comments. Overall looks great!
We lose some power (on the consumer side) with effectful suites, but we knew that was going to happen. Obviously we gain power on the other side, not just managed for suites and so forth, but also dynamic tests / suites, something we can have combinators for eventually.
|
@jdegoes I updated the name of the method to |
|
@adamgfraser Agreed, done! Thanks for all your hard work on this. It gets us that much closer to ZIO Test 1.0! |
* implement ManagedRunnableSpec * add documentation * make suites effectual * linting change * initial work on new encoding * rework Spec combinators * add test * format * clean up * make all specs effectual * rename provideManagedSuite to provideManagedShared * move test aspects to new encoding * format * convert unchecked exceptions to test failures * ignore flaky and timing out supervision test * merge upstream changes * fix import
Implements a new
ManagedRunnableSpec, which takes aManagedas a parameter in addition to aSpecand provides the managed resource once to the entire suite.Resolves #1632.