From 7f55627a06a774321cdf59c2e3781299d57435fc Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 18 Feb 2019 13:17:33 +0300 Subject: [PATCH 1/4] CoroutinesTimeout test rule in debug module Fixes #938 --- .../kotlinx-coroutines-debug.txt | 13 +++ kotlinx-coroutines-debug/build.gradle | 1 + .../src/junit4/CoroutinesTimeout.kt | 63 +++++++++++ .../src/junit4/CoroutinesTimeoutStatement.kt | 92 ++++++++++++++++ .../test/junit4/CoroutinesTimeoutTest.kt | 56 ++++++++++ .../test/junit4/TestFailureValidation.kt | 104 ++++++++++++++++++ 6 files changed, 329 insertions(+) create mode 100644 kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt create mode 100644 kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt create mode 100644 kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutTest.kt create mode 100644 kotlinx-coroutines-debug/test/junit4/TestFailureValidation.kt diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt index 3b05e69abc..ef5153dee2 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt @@ -38,3 +38,16 @@ public final class kotlinx/coroutines/debug/State : java/lang/Enum { public static fun values ()[Lkotlinx/coroutines/debug/State; } +public final class kotlinx/coroutines/debug/junit4/CoroutinesTimeout : org/junit/rules/TestRule { + public static final field Companion Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout$Companion; + public fun (JZ)V + public synthetic fun (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 { + public final fun seconds (IZ)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout; + public static synthetic fun seconds$default (Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout$Companion;IZILjava/lang/Object;)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout; +} + diff --git a/kotlinx-coroutines-debug/build.gradle b/kotlinx-coroutines-debug/build.gradle index d54b2b67df..e96cc6c634 100644 --- a/kotlinx-coroutines-debug/build.gradle +++ b/kotlinx-coroutines-debug/build.gradle @@ -5,6 +5,7 @@ apply plugin: "com.github.johnrengelman.shadow" dependencies { + compileOnly "junit:junit:$junit_version" compile "net.bytebuddy:byte-buddy:$byte_buddy_version" compile "net.bytebuddy:byte-buddy-agent:$byte_buddy_version" } diff --git a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt new file mode 100644 index 0000000000..285a7bca1b --- /dev/null +++ b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug.junit4 + +import kotlinx.coroutines.debug.* +import org.junit.rules.* +import org.junit.runner.* +import org.junit.runners.model.* +import java.util.concurrent.* + +/** + * Coroutines timeout rule for JUnit4 that is applied to all methods in the class. + * This rule is very similar to [Timeout] rule: it runs tests in a separate thread, + * fails tests after the given timeout and interrupts test thread. + * + * Additionally, this rule installs [DebugProbes] and dumps all coroutines at the moment of the timeout. + * It may cancel coroutines on timeout if [cancelOnTimeout] set to `true`. + * + * Example of usage: + * ``` + * class HangingTest { + * + * @Rule + * @JvmField + * val timeout = CoroutinesTimeout.seconds(5) + * + * @Test + * fun testThatHangs() = runBlocking { + * ... + * delay(Long.MAX_VALUE) // somewhere deep in the stack + * ... + * } + * } + * ``` + */ +public class CoroutinesTimeout( + private val testTimeoutMs: Long, + private val cancelOnTimeout: Boolean = false +) : TestRule { + + init { + require(testTimeoutMs > 0) { "Expected positive test timeout, but had $testTimeoutMs" } + } + + companion object { + /** + * 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) + } + } + + /** + * @suppress suppress from Dokka + */ + override fun apply(base: Statement, description: Description): Statement { + return CoroutinesTimeoutStatement(base, description, testTimeoutMs, cancelOnTimeout) + } +} diff --git a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt new file mode 100644 index 0000000000..32cf08843c --- /dev/null +++ b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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, + private val testTimeoutMs: Long, + private val cancelOnTimeout: Boolean = false +) : Statement() { + + private val testExecutor = Executors.newSingleThreadExecutor { + Thread(it).apply { + name = "Timeout test executor" + isDaemon = true + } + } + + // Thread to dump stack from, captured by testExecutor + private lateinit var testThread: Thread + + 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 + try { + testFuture.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) + "${testTimeoutMs / 1000} seconds" + else "$testTimeoutMs milliseconds" + + val message = "Test ${description.methodName} timed out after $units" + System.err.println("\n$message\n") + System.err.flush() + + DebugProbes.dumpCoroutines() + System.out.flush() // Synchronize serr/sout + + /* + * Order is important: + * 1) Create exception with a stacktrace of hang test + * 2) Cancel all coroutines via debug agent API (changing system state!) + * 3) Throw created exception + */ + val exception = createTimeoutException(message, testThread) + cancelIfNecessary() + // If timed out test throws an exception, we can't do much except ignoring it + throw exception + } + + private fun cancelIfNecessary() { + if (cancelOnTimeout) { + DebugProbes.dumpCoroutinesState().forEach { + it.jobOrNull?.cancel() + } + } + } + + private fun createTimeoutException(message: String, thread: Thread): Exception { + val stackTrace = thread.stackTrace + val exception = TestTimedOutException(testTimeoutMs, TimeUnit.MILLISECONDS) + exception.stackTrace = stackTrace + thread.interrupt() + return exception + } +} diff --git a/kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutTest.kt b/kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutTest.kt new file mode 100644 index 0000000000..8d50c723cc --- /dev/null +++ b/kotlinx-coroutines-debug/test/junit4/CoroutinesTimeoutTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug.junit4 + +import junit4.* +import kotlinx.coroutines.* +import org.junit.* +import org.junit.runners.model.* + +class CoroutinesTimeoutTest : TestBase() { + + @Rule + @JvmField + public val validation = TestFailureValidation( + 1000, false, + TestResultSpec("throwingTest", error = RuntimeException::class.java), + TestResultSpec("successfulTest"), + TestResultSpec( + "hangingTest", expectedOutParts = listOf( + "Coroutines dump", + "Test hangingTest timed out after 1 seconds", + "BlockingCoroutine{Active}", + "runBlocking", + "at kotlinx.coroutines.debug.junit4.CoroutinesTimeoutTest.suspendForever", + "at kotlinx.coroutines.debug.junit4.CoroutinesTimeoutTest\$hangingTest\$1.invokeSuspend"), + notExpectedOutParts = listOf("delay", "throwingTest"), + error = TestTimedOutException::class.java) + ) + + @Test + fun hangingTest() = runBlocking { + suspendForever() + expectUnreached() + } + + private suspend fun suspendForever() { + delay(Long.MAX_VALUE) + expectUnreached() + } + + @Test + fun throwingTest() = runBlocking { + throw RuntimeException() + } + + @Test + fun successfulTest() = runBlocking { + val job = launch { + yield() + } + + job.join() + } +} diff --git a/kotlinx-coroutines-debug/test/junit4/TestFailureValidation.kt b/kotlinx-coroutines-debug/test/junit4/TestFailureValidation.kt new file mode 100644 index 0000000000..9084926993 --- /dev/null +++ b/kotlinx-coroutines-debug/test/junit4/TestFailureValidation.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package junit4 + +import kotlinx.coroutines.debug.* +import kotlinx.coroutines.debug.junit4.* +import org.junit.rules.* +import org.junit.runner.* +import org.junit.runners.model.* +import java.io.* +import kotlin.test.* + +internal fun TestFailureValidation(timeoutMs: Long, cancelOnTimeout: Boolean, vararg specs: TestResultSpec): RuleChain = + RuleChain + .outerRule(TestFailureValidation(specs.associateBy { it.testName })) + .around( + CoroutinesTimeout( + timeoutMs, + cancelOnTimeout + ) + ) + +/** + * Rule that captures test result, serr and sout and validates it against provided [testsSpec] + */ +internal class TestFailureValidation(private val testsSpec: Map) : TestRule { + + companion object { + init { + DebugProbes.sanitizeStackTraces = false + } + } + override fun apply(base: Statement, description: Description): Statement { + return TestFailureStatement(base, description) + } + + inner class TestFailureStatement(private val test: Statement, private val description: Description) : Statement() { + private lateinit var sout: PrintStream + private lateinit var serr: PrintStream + private val capturedOut = ByteArrayOutputStream() + + override fun evaluate() { + try { + replaceOut() + test.evaluate() + } catch (e: Throwable) { + validateFailure(e) + return + } finally { + resetOut() + } + + validateSuccess() // To avoid falling into catch + } + + private fun validateSuccess() { + val spec = testsSpec[description.methodName] ?: error("Test spec not found: ${description.methodName}") + require(spec.error == null) { "Expected exception of type ${spec.error}, but test successfully passed" } + + val captured = capturedOut.toString() + assertFalse(captured.contains("Coroutines dump")) + assertTrue(captured.isEmpty(), captured) + } + + private fun validateFailure(e: Throwable) { + val spec = testsSpec[description.methodName] ?: error("Test spec not found: ${description.methodName}") + if (spec.error == null || !spec.error.isInstance(e)) { + throw IllegalStateException("Unexpected failure, expected ${spec.error}, had ${e::class}", e) + } + + if (e !is TestTimedOutException) return + + val captured = capturedOut.toString() + assertTrue(captured.contains("Coroutines dump")) + for (part in spec.expectedOutParts) { + assertTrue(captured.contains(part), "Expected $part to be part of the\n$captured") + } + + for (part in spec.notExpectedOutParts) { + assertFalse(captured.contains(part), "Expected $part not to be part of the\n$captured") + } + } + + private fun replaceOut() { + sout = System.out + serr = System.err + + System.setOut(PrintStream(capturedOut)) + System.setErr(PrintStream(capturedOut)) + } + + private fun resetOut() { + System.setOut(sout) + System.setErr(serr) + } + } +} + +data class TestResultSpec( + val testName: String, val expectedOutParts: List = listOf(), val notExpectedOutParts: + List = listOf(), val error: Class? = null +) From 596a421f2311c87fb79a1dfe83a77ca04c5a6396 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 18 Feb 2019 14:17:57 +0300 Subject: [PATCH 2/4] Update debug module documentation --- README.md | 1 + .../jvm/test/guide/example-compose-05.kt | 6 +-- .../jvm/test/guide/example-compose-06.kt | 2 +- kotlinx-coroutines-debug/README.md | 39 +++++++++++++++++ .../src/junit4/CoroutinesTimeout.kt | 2 +- .../test/TestRuleExample.kt | 42 +++++++++++++++++++ 6 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 kotlinx-coroutines-debug/test/TestRuleExample.kt diff --git a/README.md b/README.md index 2cfd4475f5..d22b500218 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ GlobalScope.launch { * `Dispatchers.setMain` to override `Dispatchers.Main` in tests. * [debug](kotlinx-coroutines-debug/README.md) — debug utilities for coroutines. * `DebugProbes` API to probe, keep track of, print and dump active coroutines. + * `CoroutinesTimeout` test rule to automatically dump coroutines on test timeout. * [reactive](reactive/README.md) — modules that provide builders and iteration support for various reactive streams libraries: * Reactive Streams, RxJava 2.x, and Project Reactor. * [ui](ui/README.md) — modules that provide coroutine dispatchers for various single-threaded UI libraries: diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-compose-05.kt b/kotlinx-coroutines-core/jvm/test/guide/example-compose-05.kt index 07c0e97d9e..1801dbccad 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-compose-05.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-compose-05.kt @@ -9,18 +9,18 @@ import kotlinx.coroutines.* import kotlin.system.* fun main() = runBlocking { -//sampleStart + //sampleStart val time = measureTimeMillis { println("The answer is ${concurrentSum()}") } println("Completed in $time ms") -//sampleEnd + //sampleEnd } suspend fun concurrentSum(): Int = coroutineScope { val one = async { doSomethingUsefulOne() } val two = async { doSomethingUsefulTwo() } - one.await() + two.await() + one.await() + two.await() } suspend fun doSomethingUsefulOne(): Int { diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-compose-06.kt b/kotlinx-coroutines-core/jvm/test/guide/example-compose-06.kt index 6fc92a2be1..63468421a9 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-compose-06.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-compose-06.kt @@ -28,5 +28,5 @@ suspend fun failedConcurrentSum(): Int = coroutineScope { println("Second child throws an exception") throw ArithmeticException() } - one.await() + two.await() + one.await() + two.await() } diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index df5d6c3bee..60bbd1dcf9 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -22,6 +22,43 @@ 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. + +Its usage is better to demonstrate by the example (runnable code is [here](test/TestRuleExample.kt)): + +```kotlin +class TestRuleExample { + @Rule + @JvmField + public val timeout = CoroutinesTimeout.seconds(1) + + 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") + } + + @Test + fun hangingTest() = runBlocking { + val job = launch { + someFunctionDeepInTheStack() + } + + println("Doing some work...") + job.join() + } +} +``` + +After 1 second, test will fail with `TestTimeoutException` and all coroutines (`runBlocking` and `launch`) and their +stacktraces will be dumped to the console. + ### Using as JVM agent It is possible to use this module as a standalone JVM agent to enable debug probes on the application startup. @@ -112,4 +149,6 @@ Do not use this module in production environment and do not rely on the format o [DebugProbes.dumpCoroutinesState]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines-state.html [DebugProbes.printJob]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/print-job.html [DebugProbes.printScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/print-scope.html + +[CoroutinesTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug.junit4/-coroutines-timeout/index.html diff --git a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt index 285a7bca1b..c5af544a1a 100644 --- a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt +++ b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt @@ -22,7 +22,7 @@ import java.util.concurrent.* * ``` * class HangingTest { * - * @Rule + * @Rule * @JvmField * val timeout = CoroutinesTimeout.seconds(5) * diff --git a/kotlinx-coroutines-debug/test/TestRuleExample.kt b/kotlinx-coroutines-debug/test/TestRuleExample.kt new file mode 100644 index 0000000000..b5d1c262bf --- /dev/null +++ b/kotlinx-coroutines-debug/test/TestRuleExample.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import kotlinx.coroutines.* +import kotlinx.coroutines.debug.junit4.* +import org.junit.* + +@Ignore // do not run it on CI +class TestRuleExample { + + @JvmField + @Rule + public val timeout = CoroutinesTimeout.seconds(1) + + 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") + } + + @Test + fun hangingTest() = runBlocking { + val job = launch { + someFunctionDeepInTheStack() + } + + println("Doing some work...") + job.join() + } + + @Test + fun successfulTest() = runBlocking { + launch { + delay(10) + }.join() + } + +} From ccf5c260e83a96c50f766168eedc1e3bb861b4b7 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 20 Feb 2019 13:01:34 +0300 Subject: [PATCH 3/4] 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 --- .../kotlinx-coroutines-debug.txt | 1 - kotlinx-coroutines-debug/README.md | 15 +++---- .../src/junit4/CoroutinesTimeout.kt | 12 +++--- .../src/junit4/CoroutinesTimeoutStatement.kt | 39 +++++++++---------- .../test/CoroutinesDumpTest.kt | 9 +++-- 5 files changed, 33 insertions(+), 43 deletions(-) diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt index ef5153dee2..96a35c7f83 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt @@ -43,7 +43,6 @@ public final class kotlinx/coroutines/debug/junit4/CoroutinesTimeout : org/junit public fun (JZ)V public synthetic fun (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 { diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md index 60bbd1dcf9..f969b96502 100644 --- a/kotlinx-coroutines-debug/README.md +++ b/kotlinx-coroutines-debug/README.md @@ -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 { @@ -37,11 +37,8 @@ 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 @@ -49,9 +46,7 @@ class TestRuleExample { val job = launch { someFunctionDeepInTheStack() } - - println("Doing some work...") - job.join() + job.join() // Join will hang } } ``` diff --git a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt index c5af544a1a..3d551b7d8a 100644 --- a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt +++ b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt @@ -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) } diff --git a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt index 32cf08843c..fd74fa96ec 100644 --- a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt +++ b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt @@ -4,40 +4,38 @@ 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 { + 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) @@ -45,13 +43,12 @@ internal class CoroutinesTimeoutStatement( 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" diff --git a/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt b/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt index 8a4f4e6962..dd4dc79b80 100644 --- a/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt +++ b/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt @@ -81,6 +81,7 @@ class CoroutinesDumpTest : TestBase() { fun testRunningCoroutineWithSuspensionPoint() = synchronized(monitor) { val deferred = GlobalScope.async { activeMethod(shouldSuspend = true) + yield() // tail-call } awaitCoroutineStarted() @@ -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) @@ -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) } From 3f1f13998de6d3496b6fc7bc6ec1f9d079d29e4d Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 20 Feb 2019 16:59:59 +0300 Subject: [PATCH 4/4] Revert knit changes --- .../jvm/test/guide/example-compose-05.kt | 6 +++--- .../jvm/test/guide/example-compose-06.kt | 2 +- kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-compose-05.kt b/kotlinx-coroutines-core/jvm/test/guide/example-compose-05.kt index 1801dbccad..4e0967926c 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-compose-05.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-compose-05.kt @@ -9,18 +9,18 @@ import kotlinx.coroutines.* import kotlin.system.* fun main() = runBlocking { - //sampleStart +//sampleStart val time = measureTimeMillis { println("The answer is ${concurrentSum()}") } println("Completed in $time ms") - //sampleEnd +//sampleEnd } suspend fun concurrentSum(): Int = coroutineScope { val one = async { doSomethingUsefulOne() } val two = async { doSomethingUsefulTwo() } - one.await() + two.await() + one.await() + two.await() } suspend fun doSomethingUsefulOne(): Int { diff --git a/kotlinx-coroutines-core/jvm/test/guide/example-compose-06.kt b/kotlinx-coroutines-core/jvm/test/guide/example-compose-06.kt index 63468421a9..6fc92a2be1 100644 --- a/kotlinx-coroutines-core/jvm/test/guide/example-compose-06.kt +++ b/kotlinx-coroutines-core/jvm/test/guide/example-compose-06.kt @@ -28,5 +28,5 @@ suspend fun failedConcurrentSum(): Int = coroutineScope { println("Second child throws an exception") throw ArithmeticException() } - one.await() + two.await() + one.await() + two.await() } diff --git a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt index 3d551b7d8a..c69becb24c 100644 --- a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt +++ b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt @@ -50,7 +50,6 @@ public class CoroutinesTimeout( */ public fun seconds(seconds: Int, cancelOnTimeout: Boolean = false): CoroutinesTimeout = CoroutinesTimeout(TimeUnit.SECONDS.toMillis(seconds.toLong()), cancelOnTimeout) - } /**