Skip to content

Commit

Permalink
Update docs based on feedback @ I/O
Browse files Browse the repository at this point in the history
  • Loading branch information
objcode authored and qwwdfsad committed May 20, 2019
1 parent e35637a commit 3fe7bd2
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 100 deletions.
16 changes: 10 additions & 6 deletions kotlinx-coroutines-test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ builder that provides extra test control to coroutines.
### Testing regular suspend functions

To test regular suspend functions, which may have a delay, you can use the [runBlockingTest] builder to start a testing
coroutine. Any calls to `delay` will automatically advance time.
coroutine. Any calls to `delay` will automatically advance virtual time by the amount delayed.

```kotlin
@Test
Expand All @@ -79,7 +79,7 @@ fun testFoo() = runBlockingTest { // a coroutine with an extra test control
}

suspend fun foo() {
delay(1_000) // auto-advances without delay due to runBlockingTest
delay(1_000) // auto-advances virtual time by 1_000ms due to runBlockingTest
// ...
}
```
Expand All @@ -92,7 +92,7 @@ Inside of [runBlockingTest], both [launch] and [async] will start a new coroutin
test case.

To make common testing situations easier, by default the body of the coroutine is executed *eagerly* until
the first [delay].
the first call to [delay] or [yield].

```kotlin
@Test
Expand All @@ -113,8 +113,10 @@ fun CoroutineScope.foo() {
suspend fun bar() {}
```

`runBlockingTest` will auto-progress time until all coroutines are completed before returning. If any coroutines are not
able to complete, an [UncompletedCoroutinesError] will be thrown.
`runBlockingTest` will auto-progress virtual time until all coroutines are completed before returning. If any coroutines
are not able to complete, an [UncompletedCoroutinesError] will be thrown.

*Note:* The default eager behavior of [runBlockingTest] will ignore [CoroutineStart] parameters.

### Testing `launch` or `async` with `delay`

Expand All @@ -130,7 +132,7 @@ fun testFooWithLaunchAndDelay() = runBlockingTest {
foo()
// the coroutine launched by foo has not completed here, it is suspended waiting for delay(1_000)
advanceTimeBy(1_000) // progress time, this will cause the delay to resume
// foo() coroutine launched by foo has completed here
// the coroutine launched by foo has completed here
// ...
}

Expand Down Expand Up @@ -434,6 +436,8 @@ If you have any suggestions for improvements to this experimental API please sha
[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
[async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
[delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
[yield]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html
[CoroutineStart]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/index.html
[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
[ExperimentalCoroutinesApi]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-experimental-coroutines-api/index.html
<!--- MODULE kotlinx-coroutines-test -->
Expand Down
125 changes: 125 additions & 0 deletions kotlinx-coroutines-test/src/DelayController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package kotlinx.coroutines.test

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi

/**
* Control the virtual clock time of a [CoroutineDispatcher].
*
* Testing libraries may expose this interface to tests instead of [TestCoroutineDispatcher].
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public interface DelayController {
/**
* Returns the current virtual clock-time as it is known to this Dispatcher.
*
* @return The virtual clock-time
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public val currentTime: Long

/**
* Moves the Dispatcher's virtual clock forward by a specified amount of time.
*
* The amount the clock is progressed may be larger than the requested `delayTimeMillis` if the code under test uses
* blocking coroutines.
*
* The virtual clock time will advance once for each delay resumed until the next delay exceeds the requested
* `delayTimeMills`. In the following test, the virtual time will progress by 2_000 then 1 to resume three different
* calls to delay.
*
* ```
* @Test
* fun advanceTimeTest() = runBlockingTest {
* foo()
* advanceTimeBy(2_000) // advanceTimeBy(2_000) will progress through the first two delays
* // virtual time is 2_000, next resume is at 2_001
* advanceTimeBy(2) // progress through the last delay of 501 (note 500ms were already advanced)
* // virtual time is 2_0002
* }
*
* fun CoroutineScope.foo() {
* launch {
* delay(1_000) // advanceTimeBy(2_000) will progress through this delay (resume @ virtual time 1_000)
* // virtual time is 1_000
* delay(500) // advanceTimeBy(2_000) will progress through this delay (resume @ virtual time 1_500)
* // virtual time is 1_500
* delay(501) // advanceTimeBy(2_000) will not progress through this delay (resume @ virtual time 2_001)
* // virtual time is 2_001
* }
* }
* ```
*
* @param delayTimeMillis The amount of time to move the CoroutineContext's clock forward.
* @return The amount of delay-time that this Dispatcher's clock has been forwarded.
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public fun advanceTimeBy(delayTimeMillis: Long): Long

/**
* Immediately execute all pending tasks and advance the virtual clock-time to the last delay.
*
* If new tasks are scheduled due to advancing virtual time, they will be executed before `advanceUntilIdle`
* returns.
*
* @return the amount of delay-time that this Dispatcher's clock has been forwarded in milliseconds.
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public fun advanceUntilIdle(): Long

/**
* Run any tasks that are pending at or before the current virtual clock-time.
*
* Calling this function will never advance the clock.
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public fun runCurrent()

/**
* Call after test code completes to ensure that the dispatcher is properly cleaned up.
*
* @throws UncompletedCoroutinesError if any pending tasks are active, however it will not throw for suspended
* coroutines.
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
@Throws(UncompletedCoroutinesError::class)
public fun cleanupTestCoroutines()

/**
* Run a block of code in a paused dispatcher.
*
* By pausing the dispatcher any new coroutines will not execute immediately. After block executes, the dispatcher
* will resume auto-advancing.
*
* This is useful when testing functions that start a coroutine. By pausing the dispatcher assertions or
* setup may be done between the time the coroutine is created and started.
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public suspend fun pauseDispatcher(block: suspend () -> Unit)

/**
* Pause the dispatcher.
*
* When paused, the dispatcher will not execute any coroutines automatically, and you must call [runCurrent] or
* [advanceTimeBy], or [advanceUntilIdle] to execute coroutines.
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public fun pauseDispatcher()

/**
* Resume the dispatcher from a paused state.
*
* Resumed dispatchers will automatically progress through all coroutines scheduled at the current time. To advance
* time and execute coroutines scheduled in the future use, one of [advanceTimeBy],
* or [advanceUntilIdle].
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public fun resumeDispatcher()
}

/**
* Thrown when a test has completed and there are tasks that are not completed or cancelled.
*/
// todo: maybe convert into non-public class in 1.3.0 (need use-cases for a public exception type)
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public class UncompletedCoroutinesError(message: String, cause: Throwable? = null): AssertionError(message, cause)
92 changes: 0 additions & 92 deletions kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,98 +10,6 @@ import kotlinx.coroutines.internal.*
import kotlin.coroutines.*
import kotlin.math.*

/**
* Control the virtual clock time of a [CoroutineDispatcher].
*
* Testing libraries may expose this interface to tests instead of [TestCoroutineDispatcher].
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public interface DelayController {
/**
* Returns the current virtual clock-time as it is known to this Dispatcher.
*
* @return The virtual clock-time
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public val currentTime: Long

/**
* Moves the Dispatcher's virtual clock forward by a specified amount of time.
*
* The amount the clock is progressed may be larger than the requested delayTimeMillis if the code under test uses
* blocking coroutines.
*
* @param delayTimeMillis The amount of time to move the CoroutineContext's clock forward.
* @return The amount of delay-time that this Dispatcher's clock has been forwarded.
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public fun advanceTimeBy(delayTimeMillis: Long): Long

/**
* Immediately execute all pending tasks and advance the virtual clock-time to the last delay.
*
* @return the amount of delay-time that this Dispatcher's clock has been forwarded in milliseconds.
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public fun advanceUntilIdle(): Long

/**
* Run any tasks that are pending at or before the current virtual clock-time.
*
* Calling this function will never advance the clock.
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public fun runCurrent()

/**
* Call after test code completes to ensure that the dispatcher is properly cleaned up.
*
* @throws UncompletedCoroutinesError if any pending tasks are active, however it will not throw for suspended
* coroutines.
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
@Throws(UncompletedCoroutinesError::class)
public fun cleanupTestCoroutines()

/**
* Run a block of code in a paused dispatcher.
*
* By pausing the dispatcher any new coroutines will not execute immediately. After block executes, the dispatcher
* will resume auto-advancing.
*
* This is useful when testing functions that start a coroutine. By pausing the dispatcher assertions or
* setup may be done between the time the coroutine is created and started.
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public suspend fun pauseDispatcher(block: suspend () -> Unit)

/**
* Pause the dispatcher.
*
* When paused, the dispatcher will not execute any coroutines automatically, and you must call [runCurrent] or
* [advanceTimeBy], or [advanceUntilIdle] to execute coroutines.
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public fun pauseDispatcher()

/**
* Resume the dispatcher from a paused state.
*
* Resumed dispatchers will automatically progress through all coroutines scheduled at the current time. To advance
* time and execute coroutines scheduled in the future use, one of [advanceTimeBy],
* or [advanceUntilIdle].
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public fun resumeDispatcher()
}

/**
* Thrown when a test has completed and there are tasks that are not completed or cancelled.
*/
// todo: maybe convert into non-public class in 1.3.0 (need use-cases for a public exception type)
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public class UncompletedCoroutinesError(message: String, cause: Throwable? = null): AssertionError(message, cause)

/**
* [CoroutineDispatcher] that performs both immediate and lazy execution of coroutines in tests
* and implements [DelayController] to control its virtual clock.
Expand Down
39 changes: 39 additions & 0 deletions kotlinx-coroutines-test/test/TestCoroutineDispatcherOrderTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package kotlinx.coroutines.test

import kotlinx.coroutines.*
import org.junit.*
import kotlin.coroutines.*
import kotlin.test.assertEquals

class TestCoroutineDispatcherOrderTest : TestBase() {

@Test
fun testAdvanceTimeBy_progressesOnEachDelay() {
val dispatcher = TestCoroutineDispatcher()
val scope = TestCoroutineScope(dispatcher)

expect(1)
scope.launch {
expect(2)
delay(1_000)
assertEquals(1_000, dispatcher.currentTime)
expect(4)
delay(5_00)
assertEquals(1_500, dispatcher.currentTime)
expect(5)
delay(501)
assertEquals(2_001, dispatcher.currentTime)
expect(7)
}
expect(3)
assertEquals(0, dispatcher.currentTime)
dispatcher.advanceTimeBy(2_000)
expect(6)
assertEquals(2_000, dispatcher.currentTime)
dispatcher.advanceTimeBy(2)
expect(8)
assertEquals(2_002, dispatcher.currentTime)
scope.cleanupTestCoroutines()
finish(9)
}
}
4 changes: 2 additions & 2 deletions kotlinx-coroutines-test/test/TestModuleHelpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ const val SLOW = 10_000L
*/
suspend fun CoroutineScope.assertRunsFast(block: suspend CoroutineScope.() -> Unit) {
val start = Instant.now().toEpochMilli()
// don''t need to be fancy with timeouts here since anything longer than a few ms is an error
this.block()
// don't need to be fancy with timeouts here since anything longer than a few ms is an error
block()
val duration = Instant.now().minusMillis(start).toEpochMilli()
Assert.assertTrue("All tests must complete within 2000ms (use longer timeouts to cause failure)", duration < 2_000)
}
9 changes: 9 additions & 0 deletions kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,13 @@ class TestRunBlockingOrderTest : TestBase() {
}
expect(2)
}

@Test
fun testAdvanceUntilIdle_inRunBlocking() = runBlockingTest {
expect(1)
assertRunsFast {
advanceUntilIdle() // ensure this doesn't block forever
}
finish(2)
}
}

0 comments on commit 3fe7bd2

Please sign in to comment.