Skip to content

Commit ccf5c26

Browse files
committed
Use plain old thread instead of single-thread executor in order to be able to safely interrupt it in the end of a test
Replace delay(1) with yield in tests to make them timing-independent
1 parent 596a421 commit ccf5c26

File tree

5 files changed

+33
-43
lines changed

5 files changed

+33
-43
lines changed

binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt

-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ public final class kotlinx/coroutines/debug/junit4/CoroutinesTimeout : org/junit
4343
public fun <init> (JZ)V
4444
public synthetic fun <init> (JZILkotlin/jvm/internal/DefaultConstructorMarker;)V
4545
public fun apply (Lorg/junit/runners/model/Statement;Lorg/junit/runner/Description;)Lorg/junit/runners/model/Statement;
46-
public static final fun seconds (IZ)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout;
4746
}
4847

4948
public final class kotlinx/coroutines/debug/junit4/CoroutinesTimeout$Companion {

kotlinx-coroutines-debug/README.md

+5-10
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ dependencies {
2525
### Using in unit tests
2626

2727
For JUnit4 debug module provides special test rule, [CoroutinesTimeout], for installing debug probes
28-
and dump coroutines on timeout to simplify tests debugging.
28+
and to dump coroutines on timeout to simplify tests debugging.
2929

30-
Its usage is better to demonstrate by the example (runnable code is [here](test/TestRuleExample.kt)):
30+
Its usage is better demonstrated by the example (runnable code is [here](test/TestRuleExample.kt)):
3131

3232
```kotlin
3333
class TestRuleExample {
@@ -37,21 +37,16 @@ class TestRuleExample {
3737

3838
private suspend fun someFunctionDeepInTheStack() {
3939
withContext(Dispatchers.IO) {
40-
delay(Long.MAX_VALUE)
41-
println("This line is never executed")
42-
}
43-
44-
println("This line is never executed as well")
40+
delay(Long.MAX_VALUE) // Hang method
41+
}
4542
}
4643

4744
@Test
4845
fun hangingTest() = runBlocking {
4946
val job = launch {
5047
someFunctionDeepInTheStack()
5148
}
52-
53-
println("Doing some work...")
54-
job.join()
49+
job.join() // Join will hang
5550
}
5651
}
5752
```

kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt

+5-7
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,14 @@ public class CoroutinesTimeout(
4848
/**
4949
* Creates [CoroutinesTimeout] rule with the given timeout in seconds.
5050
*/
51-
@JvmStatic
52-
public fun seconds(seconds: Int, cancelOnTimeout: Boolean = false): CoroutinesTimeout {
53-
return CoroutinesTimeout(TimeUnit.SECONDS.toMillis(seconds.toLong()), cancelOnTimeout)
54-
}
51+
public fun seconds(seconds: Int, cancelOnTimeout: Boolean = false): CoroutinesTimeout =
52+
CoroutinesTimeout(TimeUnit.SECONDS.toMillis(seconds.toLong()), cancelOnTimeout)
53+
5554
}
5655

5756
/**
5857
* @suppress suppress from Dokka
5958
*/
60-
override fun apply(base: Statement, description: Description): Statement {
61-
return CoroutinesTimeoutStatement(base, description, testTimeoutMs, cancelOnTimeout)
62-
}
59+
override fun apply(base: Statement, description: Description): Statement =
60+
CoroutinesTimeoutStatement(base, description, testTimeoutMs, cancelOnTimeout)
6361
}

kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt

+18-21
Original file line numberDiff line numberDiff line change
@@ -4,54 +4,51 @@
44

55
package kotlinx.coroutines.debug.junit4
66

7-
import kotlinx.coroutines.*
87
import kotlinx.coroutines.debug.*
98
import org.junit.runner.*
109
import org.junit.runners.model.*
1110
import java.util.concurrent.*
1211

1312
internal class CoroutinesTimeoutStatement(
14-
private val testStatement: Statement, private val testDescription: Description,
13+
testStatement: Statement,
14+
private val testDescription: Description,
1515
private val testTimeoutMs: Long,
1616
private val cancelOnTimeout: Boolean = false
1717
) : Statement() {
1818

19-
private val testExecutor = Executors.newSingleThreadExecutor {
20-
Thread(it).apply {
21-
name = "Timeout test executor"
22-
isDaemon = true
23-
}
19+
private val testStartedLatch = CountDownLatch(1)
20+
21+
private val testResult = FutureTask<Unit> {
22+
testStartedLatch.countDown()
23+
testStatement.evaluate()
2424
}
2525

26-
// Thread to dump stack from, captured by testExecutor
27-
private lateinit var testThread: Thread
26+
/*
27+
* We are using hand-rolled thread instead of single thread executor
28+
* in order to be able to safely interrupt thread in the end of a test
29+
*/
30+
private val testThread = Thread(testResult, "Timeout test thread").apply { isDaemon = true }
2831

2932
override fun evaluate() {
30-
DebugProbes.install() // Fail-fast if probes are unavailable
31-
val latch = CountDownLatch(1)
32-
val testFuture = CompletableFuture.runAsync(Runnable {
33-
testThread = Thread.currentThread()
34-
latch.countDown()
35-
testStatement.evaluate()
36-
}, testExecutor)
37-
38-
latch.await() // Await until test is started
33+
DebugProbes.install()
34+
testThread.start()
35+
// Await until test is started to take only test execution time into account
36+
testStartedLatch.await()
3937
try {
40-
testFuture.get(testTimeoutMs, TimeUnit.MILLISECONDS)
38+
testResult.get(testTimeoutMs, TimeUnit.MILLISECONDS)
4139
return
4240
} catch (e: TimeoutException) {
4341
handleTimeout(testDescription)
4442
} catch (e: ExecutionException) {
4543
throw e.cause ?: e
4644
} finally {
4745
DebugProbes.uninstall()
48-
testExecutor.shutdown()
4946
}
5047
}
5148

5249
private fun handleTimeout(description: Description) {
5350
val units =
54-
if (testTimeoutMs % 1000L == 0L)
51+
if (testTimeoutMs % 1000 == 0L)
5552
"${testTimeoutMs / 1000} seconds"
5653
else "$testTimeoutMs milliseconds"
5754

kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt

+5-4
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class CoroutinesDumpTest : TestBase() {
8181
fun testRunningCoroutineWithSuspensionPoint() = synchronized(monitor) {
8282
val deferred = GlobalScope.async {
8383
activeMethod(shouldSuspend = true)
84+
yield() // tail-call
8485
}
8586

8687
awaitCoroutineStarted()
@@ -143,11 +144,11 @@ class CoroutinesDumpTest : TestBase() {
143144

144145
private suspend fun activeMethod(shouldSuspend: Boolean) {
145146
nestedActiveMethod(shouldSuspend)
146-
delay(1)
147+
assertTrue(true) // tail-call
147148
}
148149

149150
private suspend fun nestedActiveMethod(shouldSuspend: Boolean) {
150-
if (shouldSuspend) delay(1)
151+
if (shouldSuspend) yield()
151152
notifyTest()
152153
while (coroutineContext[Job]!!.isActive) {
153154
Thread.sleep(100)
@@ -156,11 +157,11 @@ class CoroutinesDumpTest : TestBase() {
156157

157158
private suspend fun sleepingOuterMethod() {
158159
sleepingNestedMethod()
159-
delay(1)
160+
yield()
160161
}
161162

162163
private suspend fun sleepingNestedMethod() {
163-
delay(1)
164+
yield()
164165
notifyTest()
165166
delay(Long.MAX_VALUE)
166167
}

0 commit comments

Comments
 (0)