package testkit
- Source
- package.scala
- Alphabetic
- By Inheritance
- testkit
- AnyRef
- Any
- Hide All
- Show All
- Public
- Protected
Type Members
- type TestContext = kernel.testkit.TestContext
- final class TestControl[A] extends AnyRef
Implements a fully functional single-threaded runtime for a cats.effect.IO program.
Implements a fully functional single-threaded runtime for a cats.effect.IO program. When using this control system,
IO
programs will be executed on a single JVM thread, similar to how they would behave if the production runtime were configured to use a single worker thread regardless of underlying physical thread count. The results of the underlyingIO
will be produced by the results effect when ready, but nothing will actually evaluate until one of the tick effects on this class are sequenced. If the desired behavior is to simply run theIO
fully to completion within the mock environment, respecting monotonic time, then tickAll is likely the desired effect (or, alternatively, TestControl.executeEmbed).In other words,
TestControl
is sort of like a "handle" to the runtime internals within the context of a specificIO
's execution. It makes it possible for users to manipulate and observe the execution of theIO
under test from an external vantage point. It is important to understand that the outerIO
s (e.g. those returned by the tick or results methods) are not running under the test control environment, and instead they are meant to be run by some outer runtime. Interactions between the outer runtime and the inner runtime (potentially via mechanisms like cats.effect.std.Queue or cats.effect.kernel.Deferred) are quite tricky and should only be done with extreme care. The likely outcome in such scenarios is that theTestControl
runtime will detect the innerIO
as being deadlocked whenever it is actually waiting on the external runtime. This could result in strange effects such as tickAll orexecuteEmbed
terminating early. Do not construct such scenarios unless you're very confident you understand the implications of what you're doing.Where things differ from a single-threaded production runtime is in two critical areas.
First, whenever multiple fibers are outstanding and ready to be resumed, the
TestControl
runtime will randomly choose between them, rather than taking them in a first-in, first-out fashion as the default production runtime will. This semantic is intended to simulate different scheduling interleavings, ensuring that race conditions are not accidentally masked by deterministic execution order.Second, within the context of the
TestControl
, time is very carefully and artificially controlled. In a sense, this runtime behaves as if it is executing on a single CPU which performs all operations infinitely fast. Any fibers which are immediately available for execution will be executed until no further fibers are available to run (assuming the use oftickAll
). Through this entire process, the current clock (which is exposed to the program via IO.realTime and IO.monotonic) will remain fixed at the very beginning, meaning that no time is considered to have elapsed as a consequence of compute.Note that the above means that it is relatively easy to create a deadlock on this runtime with a program which would not deadlock on either the JVM or JavaScript:
// do not do this! IO.cede.foreverM.timeout(10.millis)
The above program spawns a fiber which yields forever, setting a timeout for 10 milliseconds which is intended to bring the loop to a halt. However, because the immediate task queue will never be empty, the test runtime will never advance time, meaning that the 10 milliseconds will never elapse and the timeout will not be hit. This will manifest as the tick and tickAll effects simply running forever and not returning if called. tickOne is safe to call on the above program, but it will always produce
true
.In order to advance time, you must use the advance effect to move the clock forward by a specified offset (which must be greater than 0). If you use the
tickAll
effect, the clock will be automatically advanced by the minimum amount necessary to reach the next pending task. For example, if the program contains an IO.sleep(delay* for500.millis
, and there are no shorter sleeps, then time will need to be advanced by 500 milliseconds in order to make that fiber eligible for execution.At this point, the process repeats until all tasks are exhausted. If the program has reached a concluding value or exception, then it will be produced from the
unsafeRun
method which scheduled theIO
on the runtime (pro tip: do not useunsafeRunSync
with this runtime, since it will always result in immediate deadlock). If the program does not produce a result but also has no further work to perform (such as a program like IO.never), thentickAll
will return but no result will have been produced by theunsafeRun
. If this happens, isDeadlocked will returntrue
and the program is in a "hung" state. This same situation on the production runtime would have manifested as an asynchronous deadlock.You should never use this runtime in a production code path. It is strictly meant for testing purposes, particularly testing programs that involve time functions and IO.sleep(delay*.
Due to the semantics of this runtime, time will behave entirely consistently with a plausible production execution environment provided that you never observe time via side-effects, and exclusively through the IO.realTime, IO.monotonic, and IO.sleep(delay* functions (and other functions built on top of these). From the perspective of these functions, all computation is infinitely fast, and the only effect which advances time is IO.sleep(delay* (or if something external, such as the test harness, sequences the advance effect). However, an effect such as
IO(System.currentTimeMillis())
will "see through" the illusion, since the system clock is unaffected by this runtime. This is one reason why it is important to always and exclusively rely onrealTime
andmonotonic
, either directly onIO
or via the typeclass abstractions.WARNING: Never use this runtime on programs which use the IO#evalOn method! The test runtime will detect this situation as an asynchronous deadlock.
- See also
- final case class TestException(i: Int) extends RuntimeException with Product with Serializable
- trait TestInstances extends ParallelFGenerators with OutcomeGenerators with SyncTypeGenerators
Value Members
- val TestContext: kernel.testkit.TestContext.type
- object TestControl