Skip to content

Commit 3fe7bd2

Browse files
objcodeqwwdfsad
authored andcommitted
Update docs based on feedback @ I/O
1 parent e35637a commit 3fe7bd2

6 files changed

+185
-100
lines changed

kotlinx-coroutines-test/README.md

+10-6
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ builder that provides extra test control to coroutines.
6969
### Testing regular suspend functions
7070

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

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

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

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

9797
```kotlin
9898
@Test
@@ -113,8 +113,10 @@ fun CoroutineScope.foo() {
113113
suspend fun bar() {}
114114
```
115115

116-
`runBlockingTest` will auto-progress time until all coroutines are completed before returning. If any coroutines are not
117-
able to complete, an [UncompletedCoroutinesError] will be thrown.
116+
`runBlockingTest` will auto-progress virtual time until all coroutines are completed before returning. If any coroutines
117+
are not able to complete, an [UncompletedCoroutinesError] will be thrown.
118+
119+
*Note:* The default eager behavior of [runBlockingTest] will ignore [CoroutineStart] parameters.
118120

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

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

@@ -434,6 +436,8 @@ If you have any suggestions for improvements to this experimental API please sha
434436
[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
435437
[async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
436438
[delay]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
439+
[yield]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html
440+
[CoroutineStart]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/index.html
437441
[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
438442
[ExperimentalCoroutinesApi]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-experimental-coroutines-api/index.html
439443
<!--- MODULE kotlinx-coroutines-test -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package kotlinx.coroutines.test
2+
3+
import kotlinx.coroutines.CoroutineDispatcher
4+
import kotlinx.coroutines.ExperimentalCoroutinesApi
5+
6+
/**
7+
* Control the virtual clock time of a [CoroutineDispatcher].
8+
*
9+
* Testing libraries may expose this interface to tests instead of [TestCoroutineDispatcher].
10+
*/
11+
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
12+
public interface DelayController {
13+
/**
14+
* Returns the current virtual clock-time as it is known to this Dispatcher.
15+
*
16+
* @return The virtual clock-time
17+
*/
18+
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
19+
public val currentTime: Long
20+
21+
/**
22+
* Moves the Dispatcher's virtual clock forward by a specified amount of time.
23+
*
24+
* The amount the clock is progressed may be larger than the requested `delayTimeMillis` if the code under test uses
25+
* blocking coroutines.
26+
*
27+
* The virtual clock time will advance once for each delay resumed until the next delay exceeds the requested
28+
* `delayTimeMills`. In the following test, the virtual time will progress by 2_000 then 1 to resume three different
29+
* calls to delay.
30+
*
31+
* ```
32+
* @Test
33+
* fun advanceTimeTest() = runBlockingTest {
34+
* foo()
35+
* advanceTimeBy(2_000) // advanceTimeBy(2_000) will progress through the first two delays
36+
* // virtual time is 2_000, next resume is at 2_001
37+
* advanceTimeBy(2) // progress through the last delay of 501 (note 500ms were already advanced)
38+
* // virtual time is 2_0002
39+
* }
40+
*
41+
* fun CoroutineScope.foo() {
42+
* launch {
43+
* delay(1_000) // advanceTimeBy(2_000) will progress through this delay (resume @ virtual time 1_000)
44+
* // virtual time is 1_000
45+
* delay(500) // advanceTimeBy(2_000) will progress through this delay (resume @ virtual time 1_500)
46+
* // virtual time is 1_500
47+
* delay(501) // advanceTimeBy(2_000) will not progress through this delay (resume @ virtual time 2_001)
48+
* // virtual time is 2_001
49+
* }
50+
* }
51+
* ```
52+
*
53+
* @param delayTimeMillis The amount of time to move the CoroutineContext's clock forward.
54+
* @return The amount of delay-time that this Dispatcher's clock has been forwarded.
55+
*/
56+
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
57+
public fun advanceTimeBy(delayTimeMillis: Long): Long
58+
59+
/**
60+
* Immediately execute all pending tasks and advance the virtual clock-time to the last delay.
61+
*
62+
* If new tasks are scheduled due to advancing virtual time, they will be executed before `advanceUntilIdle`
63+
* returns.
64+
*
65+
* @return the amount of delay-time that this Dispatcher's clock has been forwarded in milliseconds.
66+
*/
67+
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
68+
public fun advanceUntilIdle(): Long
69+
70+
/**
71+
* Run any tasks that are pending at or before the current virtual clock-time.
72+
*
73+
* Calling this function will never advance the clock.
74+
*/
75+
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
76+
public fun runCurrent()
77+
78+
/**
79+
* Call after test code completes to ensure that the dispatcher is properly cleaned up.
80+
*
81+
* @throws UncompletedCoroutinesError if any pending tasks are active, however it will not throw for suspended
82+
* coroutines.
83+
*/
84+
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
85+
@Throws(UncompletedCoroutinesError::class)
86+
public fun cleanupTestCoroutines()
87+
88+
/**
89+
* Run a block of code in a paused dispatcher.
90+
*
91+
* By pausing the dispatcher any new coroutines will not execute immediately. After block executes, the dispatcher
92+
* will resume auto-advancing.
93+
*
94+
* This is useful when testing functions that start a coroutine. By pausing the dispatcher assertions or
95+
* setup may be done between the time the coroutine is created and started.
96+
*/
97+
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
98+
public suspend fun pauseDispatcher(block: suspend () -> Unit)
99+
100+
/**
101+
* Pause the dispatcher.
102+
*
103+
* When paused, the dispatcher will not execute any coroutines automatically, and you must call [runCurrent] or
104+
* [advanceTimeBy], or [advanceUntilIdle] to execute coroutines.
105+
*/
106+
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
107+
public fun pauseDispatcher()
108+
109+
/**
110+
* Resume the dispatcher from a paused state.
111+
*
112+
* Resumed dispatchers will automatically progress through all coroutines scheduled at the current time. To advance
113+
* time and execute coroutines scheduled in the future use, one of [advanceTimeBy],
114+
* or [advanceUntilIdle].
115+
*/
116+
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
117+
public fun resumeDispatcher()
118+
}
119+
120+
/**
121+
* Thrown when a test has completed and there are tasks that are not completed or cancelled.
122+
*/
123+
// todo: maybe convert into non-public class in 1.3.0 (need use-cases for a public exception type)
124+
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
125+
public class UncompletedCoroutinesError(message: String, cause: Throwable? = null): AssertionError(message, cause)

kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt

-92
Original file line numberDiff line numberDiff line change
@@ -10,98 +10,6 @@ import kotlinx.coroutines.internal.*
1010
import kotlin.coroutines.*
1111
import kotlin.math.*
1212

13-
/**
14-
* Control the virtual clock time of a [CoroutineDispatcher].
15-
*
16-
* Testing libraries may expose this interface to tests instead of [TestCoroutineDispatcher].
17-
*/
18-
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
19-
public interface DelayController {
20-
/**
21-
* Returns the current virtual clock-time as it is known to this Dispatcher.
22-
*
23-
* @return The virtual clock-time
24-
*/
25-
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
26-
public val currentTime: Long
27-
28-
/**
29-
* Moves the Dispatcher's virtual clock forward by a specified amount of time.
30-
*
31-
* The amount the clock is progressed may be larger than the requested delayTimeMillis if the code under test uses
32-
* blocking coroutines.
33-
*
34-
* @param delayTimeMillis The amount of time to move the CoroutineContext's clock forward.
35-
* @return The amount of delay-time that this Dispatcher's clock has been forwarded.
36-
*/
37-
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
38-
public fun advanceTimeBy(delayTimeMillis: Long): Long
39-
40-
/**
41-
* Immediately execute all pending tasks and advance the virtual clock-time to the last delay.
42-
*
43-
* @return the amount of delay-time that this Dispatcher's clock has been forwarded in milliseconds.
44-
*/
45-
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
46-
public fun advanceUntilIdle(): Long
47-
48-
/**
49-
* Run any tasks that are pending at or before the current virtual clock-time.
50-
*
51-
* Calling this function will never advance the clock.
52-
*/
53-
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
54-
public fun runCurrent()
55-
56-
/**
57-
* Call after test code completes to ensure that the dispatcher is properly cleaned up.
58-
*
59-
* @throws UncompletedCoroutinesError if any pending tasks are active, however it will not throw for suspended
60-
* coroutines.
61-
*/
62-
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
63-
@Throws(UncompletedCoroutinesError::class)
64-
public fun cleanupTestCoroutines()
65-
66-
/**
67-
* Run a block of code in a paused dispatcher.
68-
*
69-
* By pausing the dispatcher any new coroutines will not execute immediately. After block executes, the dispatcher
70-
* will resume auto-advancing.
71-
*
72-
* This is useful when testing functions that start a coroutine. By pausing the dispatcher assertions or
73-
* setup may be done between the time the coroutine is created and started.
74-
*/
75-
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
76-
public suspend fun pauseDispatcher(block: suspend () -> Unit)
77-
78-
/**
79-
* Pause the dispatcher.
80-
*
81-
* When paused, the dispatcher will not execute any coroutines automatically, and you must call [runCurrent] or
82-
* [advanceTimeBy], or [advanceUntilIdle] to execute coroutines.
83-
*/
84-
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
85-
public fun pauseDispatcher()
86-
87-
/**
88-
* Resume the dispatcher from a paused state.
89-
*
90-
* Resumed dispatchers will automatically progress through all coroutines scheduled at the current time. To advance
91-
* time and execute coroutines scheduled in the future use, one of [advanceTimeBy],
92-
* or [advanceUntilIdle].
93-
*/
94-
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
95-
public fun resumeDispatcher()
96-
}
97-
98-
/**
99-
* Thrown when a test has completed and there are tasks that are not completed or cancelled.
100-
*/
101-
// todo: maybe convert into non-public class in 1.3.0 (need use-cases for a public exception type)
102-
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
103-
public class UncompletedCoroutinesError(message: String, cause: Throwable? = null): AssertionError(message, cause)
104-
10513
/**
10614
* [CoroutineDispatcher] that performs both immediate and lazy execution of coroutines in tests
10715
* and implements [DelayController] to control its virtual clock.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package kotlinx.coroutines.test
2+
3+
import kotlinx.coroutines.*
4+
import org.junit.*
5+
import kotlin.coroutines.*
6+
import kotlin.test.assertEquals
7+
8+
class TestCoroutineDispatcherOrderTest : TestBase() {
9+
10+
@Test
11+
fun testAdvanceTimeBy_progressesOnEachDelay() {
12+
val dispatcher = TestCoroutineDispatcher()
13+
val scope = TestCoroutineScope(dispatcher)
14+
15+
expect(1)
16+
scope.launch {
17+
expect(2)
18+
delay(1_000)
19+
assertEquals(1_000, dispatcher.currentTime)
20+
expect(4)
21+
delay(5_00)
22+
assertEquals(1_500, dispatcher.currentTime)
23+
expect(5)
24+
delay(501)
25+
assertEquals(2_001, dispatcher.currentTime)
26+
expect(7)
27+
}
28+
expect(3)
29+
assertEquals(0, dispatcher.currentTime)
30+
dispatcher.advanceTimeBy(2_000)
31+
expect(6)
32+
assertEquals(2_000, dispatcher.currentTime)
33+
dispatcher.advanceTimeBy(2)
34+
expect(8)
35+
assertEquals(2_002, dispatcher.currentTime)
36+
scope.cleanupTestCoroutines()
37+
finish(9)
38+
}
39+
}

kotlinx-coroutines-test/test/TestModuleHelpers.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ const val SLOW = 10_000L
1515
*/
1616
suspend fun CoroutineScope.assertRunsFast(block: suspend CoroutineScope.() -> Unit) {
1717
val start = Instant.now().toEpochMilli()
18-
// don''t need to be fancy with timeouts here since anything longer than a few ms is an error
19-
this.block()
18+
// don't need to be fancy with timeouts here since anything longer than a few ms is an error
19+
block()
2020
val duration = Instant.now().minusMillis(start).toEpochMilli()
2121
Assert.assertTrue("All tests must complete within 2000ms (use longer timeouts to cause failure)", duration < 2_000)
2222
}

kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt

+9
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,13 @@ class TestRunBlockingOrderTest : TestBase() {
6767
}
6868
expect(2)
6969
}
70+
71+
@Test
72+
fun testAdvanceUntilIdle_inRunBlocking() = runBlockingTest {
73+
expect(1)
74+
assertRunsFast {
75+
advanceUntilIdle() // ensure this doesn't block forever
76+
}
77+
finish(2)
78+
}
7079
}

0 commit comments

Comments
 (0)