Skip to content
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

CoroutinesTimeout debug rule for JUnit 4 #991

Merged
merged 4 commits into from
Feb 20, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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
  • Loading branch information
qwwdfsad committed Feb 20, 2019
commit ccf5c260e83a96c50f766168eedc1e3bb861b4b7
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ public final class kotlinx/coroutines/debug/junit4/CoroutinesTimeout : org/junit
public fun <init> (JZ)V
public synthetic fun <init> (JZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun apply (Lorg/junit/runners/model/Statement;Lorg/junit/runner/Description;)Lorg/junit/runners/model/Statement;
public static final fun seconds (IZ)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout;
}

public final class kotlinx/coroutines/debug/junit4/CoroutinesTimeout$Companion {
Expand Down
15 changes: 5 additions & 10 deletions kotlinx-coroutines-debug/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ dependencies {
### Using in unit tests

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

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

```kotlin
class TestRuleExample {
Expand All @@ -37,21 +37,16 @@ class TestRuleExample {

private suspend fun someFunctionDeepInTheStack() {
withContext(Dispatchers.IO) {
delay(Long.MAX_VALUE)
println("This line is never executed")
}

println("This line is never executed as well")
delay(Long.MAX_VALUE) // Hang method
}
}

@Test
fun hangingTest() = runBlocking {
val job = launch {
someFunctionDeepInTheStack()
}

println("Doing some work...")
job.join()
job.join() // Join will hang
}
}
```
Expand Down
12 changes: 5 additions & 7 deletions kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,14 @@ public class CoroutinesTimeout(
/**
* Creates [CoroutinesTimeout] rule with the given timeout in seconds.
*/
@JvmStatic
public fun seconds(seconds: Int, cancelOnTimeout: Boolean = false): CoroutinesTimeout {
return CoroutinesTimeout(TimeUnit.SECONDS.toMillis(seconds.toLong()), cancelOnTimeout)
}
public fun seconds(seconds: Int, cancelOnTimeout: Boolean = false): CoroutinesTimeout =
CoroutinesTimeout(TimeUnit.SECONDS.toMillis(seconds.toLong()), cancelOnTimeout)

}

/**
* @suppress suppress from Dokka
*/
override fun apply(base: Statement, description: Description): Statement {
return CoroutinesTimeoutStatement(base, description, testTimeoutMs, cancelOnTimeout)
}
override fun apply(base: Statement, description: Description): Statement =
CoroutinesTimeoutStatement(base, description, testTimeoutMs, cancelOnTimeout)
}
39 changes: 18 additions & 21 deletions kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,51 @@

package kotlinx.coroutines.debug.junit4

import kotlinx.coroutines.*
import kotlinx.coroutines.debug.*
import org.junit.runner.*
import org.junit.runners.model.*
import java.util.concurrent.*

internal class CoroutinesTimeoutStatement(
private val testStatement: Statement, private val testDescription: Description,
testStatement: Statement,
private val testDescription: Description,
private val testTimeoutMs: Long,
private val cancelOnTimeout: Boolean = false
) : Statement() {

private val testExecutor = Executors.newSingleThreadExecutor {
Thread(it).apply {
name = "Timeout test executor"
isDaemon = true
}
private val testStartedLatch = CountDownLatch(1)

private val testResult = FutureTask<Unit> {
testStartedLatch.countDown()
testStatement.evaluate()
}

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

override fun evaluate() {
DebugProbes.install() // Fail-fast if probes are unavailable
val latch = CountDownLatch(1)
val testFuture = CompletableFuture.runAsync(Runnable {
testThread = Thread.currentThread()
latch.countDown()
testStatement.evaluate()
}, testExecutor)

latch.await() // Await until test is started
DebugProbes.install()
testThread.start()
// Await until test is started to take only test execution time into account
testStartedLatch.await()
try {
testFuture.get(testTimeoutMs, TimeUnit.MILLISECONDS)
testResult.get(testTimeoutMs, TimeUnit.MILLISECONDS)
return
} catch (e: TimeoutException) {
handleTimeout(testDescription)
} catch (e: ExecutionException) {
throw e.cause ?: e
} finally {
DebugProbes.uninstall()
testExecutor.shutdown()
}
}

private fun handleTimeout(description: Description) {
val units =
if (testTimeoutMs % 1000L == 0L)
if (testTimeoutMs % 1000 == 0L)
"${testTimeoutMs / 1000} seconds"
else "$testTimeoutMs milliseconds"

Expand Down
9 changes: 5 additions & 4 deletions kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class CoroutinesDumpTest : TestBase() {
fun testRunningCoroutineWithSuspensionPoint() = synchronized(monitor) {
val deferred = GlobalScope.async {
activeMethod(shouldSuspend = true)
yield() // tail-call
}

awaitCoroutineStarted()
Expand Down Expand Up @@ -143,11 +144,11 @@ class CoroutinesDumpTest : TestBase() {

private suspend fun activeMethod(shouldSuspend: Boolean) {
nestedActiveMethod(shouldSuspend)
delay(1)
assertTrue(true) // tail-call
}

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

private suspend fun sleepingOuterMethod() {
sleepingNestedMethod()
delay(1)
yield()
}

private suspend fun sleepingNestedMethod() {
delay(1)
yield()
notifyTest()
delay(Long.MAX_VALUE)
}
Expand Down