Skip to content

Commit 4518104

Browse files
committed
Additional checks to ensure that there are no leaking threads after tests
1 parent 35d2c34 commit 4518104

File tree

14 files changed

+143
-49
lines changed

14 files changed

+143
-49
lines changed

integration/kotlinx-coroutines-guava/src/test/kotlin/kotlinx/coroutines/experimental/guava/ListenableFutureTest.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,23 @@ package kotlinx.coroutines.experimental.guava
1818

1919
import com.google.common.util.concurrent.MoreExecutors
2020
import com.google.common.util.concurrent.SettableFuture
21+
import guide.test.ignoreLostThreads
2122
import kotlinx.coroutines.experimental.*
2223
import org.hamcrest.core.IsEqual
2324
import org.hamcrest.core.IsInstanceOf
2425
import org.junit.Assert.*
26+
import org.junit.Before
2527
import org.junit.Test
2628
import java.util.concurrent.Callable
2729
import java.util.concurrent.ExecutionException
2830
import java.util.concurrent.ForkJoinPool
2931

3032
class ListenableFutureTest : TestBase() {
33+
@Before
34+
fun setup() {
35+
ignoreLostThreads("ForkJoinPool.commonPool-worker-")
36+
}
37+
3138
@Test
3239
fun testSimpleAwait() {
3340
val service = MoreExecutors.listeningDecorator(ForkJoinPool.commonPool())

integration/kotlinx-coroutines-jdk8/src/test/kotlin/kotlinx/coroutines/experimental/future/FutureTest.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616

1717
package kotlinx.coroutines.experimental.future
1818

19+
import guide.test.ignoreLostThreads
1920
import kotlinx.coroutines.experimental.*
2021
import org.hamcrest.core.IsEqual
2122
import org.junit.Assert.*
23+
import org.junit.Before
2224
import org.junit.Test
2325
import java.util.concurrent.CompletableFuture
2426
import java.util.concurrent.CompletionStage
@@ -27,6 +29,11 @@ import java.util.concurrent.atomic.AtomicInteger
2729
import kotlin.coroutines.experimental.CoroutineContext
2830

2931
class FutureTest : TestBase() {
32+
@Before
33+
fun setup() {
34+
ignoreLostThreads("ForkJoinPool.commonPool-worker-")
35+
}
36+
3037
@Test
3138
fun testSimpleAwait() {
3239
val future = future {

kotlinx-coroutines-core/src/test/kotlin/guide/test/TestUtil.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,35 @@ import java.util.concurrent.ConcurrentHashMap
2727
import java.util.concurrent.TimeUnit
2828
import java.util.concurrent.locks.LockSupport
2929

30+
private val ignoreLostThreads = mutableSetOf<String>()
31+
32+
fun ignoreLostThreads(vararg s: String) { ignoreLostThreads += s }
33+
34+
fun threadNames(): Set<String> {
35+
val arrayOfThreads = Array<Thread?>(Thread.activeCount()) { null }
36+
val n = Thread.enumerate(arrayOfThreads)
37+
val names = hashSetOf<String>()
38+
for (i in 0 until n)
39+
names.add(arrayOfThreads[i]!!.name)
40+
return names
41+
}
42+
43+
fun checkTestThreads(threadNamesBefore: Set<String>) {
44+
// give threads some time to shutdown
45+
val waitTill = System.currentTimeMillis() + 1000L
46+
var diff: List<String>
47+
do {
48+
val threadNamesAfter = threadNames()
49+
diff = (threadNamesAfter - threadNamesBefore).filter { name ->
50+
ignoreLostThreads.none { prefix -> name.startsWith(prefix) }
51+
}
52+
if (diff.isEmpty()) break
53+
} while (System.currentTimeMillis() <= waitTill)
54+
ignoreLostThreads.clear()
55+
diff.forEach { println("Lost thread '$it'") }
56+
check(diff.isEmpty()) { "Lost ${diff.size} threads" }
57+
}
58+
3059
fun trackTask(block: Runnable) = timeSource.trackTask(block)
3160

3261
// helper function to dump exception to stdout for ease of debugging failed tests
@@ -53,6 +82,7 @@ fun test(name: String, block: () -> Unit): List<String> = outputException(name)
5382
resetCoroutineId()
5483
// shutdown execution with old time source (in case it was working)
5584
DefaultExecutor.shutdown(SHUTDOWN_TIMEOUT)
85+
val threadNamesBefore = threadNames()
5686
val testTimeSource = TestTimeSource(oldOut)
5787
timeSource = testTimeSource
5888
DefaultExecutor.ensureStarted() // should start with new time source
@@ -77,6 +107,7 @@ fun test(name: String, block: () -> Unit): List<String> = outputException(name)
77107
oldOut.println("--- done")
78108
System.setOut(oldOut)
79109
System.setErr(oldErr)
110+
checkTestThreads(threadNamesBefore)
80111
}
81112
return ByteArrayInputStream(bytes).bufferedReader().readLines()
82113
}

kotlinx-coroutines-core/src/test/kotlin/kotlinx/coroutines/experimental/ExecutorsTest.kt

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,42 +16,10 @@
1616

1717
package kotlinx.coroutines.experimental
1818

19-
import org.junit.After
20-
import org.junit.Before
2119
import org.junit.Test
2220
import java.util.concurrent.Executors
2321

24-
class ExecutorsTest {
25-
fun threadNames(): Set<String> {
26-
val arrayOfThreads = Array<Thread?>(Thread.activeCount()) { null }
27-
val n = Thread.enumerate(arrayOfThreads)
28-
val names = hashSetOf<String>()
29-
for (i in 0 until n)
30-
names.add(arrayOfThreads[i]!!.name)
31-
return names
32-
}
33-
34-
lateinit var threadNamesBefore: Set<String>
35-
36-
@Before
37-
fun before() {
38-
threadNamesBefore = threadNames()
39-
}
40-
41-
@After
42-
fun after() {
43-
// give threads some time to shutdown
44-
val waitTill = System.currentTimeMillis() + 1000L
45-
var diff: Set<String>
46-
do {
47-
val threadNamesAfter = threadNames()
48-
diff = threadNamesAfter - threadNamesBefore
49-
if (diff.isEmpty()) break
50-
} while (System.currentTimeMillis() <= waitTill)
51-
diff.forEach { println("Lost thread '$it'") }
52-
check(diff.isEmpty()) { "Lost ${diff.size} threads"}
53-
}
54-
22+
class ExecutorsTest : TestBase() {
5523
fun checkThreadName(prefix: String) {
5624
val name = Thread.currentThread().name
5725
check(name.startsWith(prefix)) { "Expected thread name to start with '$prefix', found: '$name'" }

kotlinx-coroutines-core/src/test/kotlin/kotlinx/coroutines/experimental/TestBase.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616

1717
package kotlinx.coroutines.experimental
1818

19+
import guide.test.checkTestThreads
20+
import guide.test.threadNames
1921
import org.junit.After
22+
import org.junit.Before
2023
import java.util.concurrent.atomic.AtomicBoolean
2124
import java.util.concurrent.atomic.AtomicInteger
2225
import java.util.concurrent.atomic.AtomicReference
@@ -96,9 +99,21 @@ open class TestBase {
9699
check(!finished.getAndSet(true)) { "Should call 'finish(...)' at most once" }
97100
}
98101

102+
private lateinit var threadNamesBefore: Set<String>
103+
private val SHUTDOWN_TIMEOUT = 5000L // 5 sec at most to wait
104+
105+
@Before
106+
fun before() {
107+
CommonPool.usePrivatePool()
108+
threadNamesBefore = threadNames()
109+
}
110+
99111
@After
100112
fun onCompletion() {
101113
error.get()?.let { throw it }
102114
check(actionIndex.get() == 0 || finished.get()) { "Expecting that 'finish(...)' was invoked, but it was not" }
115+
CommonPool.shutdown(SHUTDOWN_TIMEOUT)
116+
DefaultExecutor.shutdown(SHUTDOWN_TIMEOUT)
117+
checkTestThreads(threadNamesBefore)
103118
}
104119
}

reactive/kotlinx-coroutines-reactor/src/test/kotlin/kotlinx/coroutines/experimental/reactor/MonoTest.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,32 @@
1616

1717
package kotlinx.coroutines.experimental.reactor
1818

19+
import guide.test.ignoreLostThreads
1920
import kotlinx.coroutines.experimental.CommonPool
20-
import kotlinx.coroutines.experimental.runBlocking
21-
import org.junit.Assert.assertEquals
22-
import org.junit.Test
2321
import kotlinx.coroutines.experimental.TestBase
2422
import kotlinx.coroutines.experimental.reactive.awaitFirst
2523
import kotlinx.coroutines.experimental.reactive.awaitLast
2624
import kotlinx.coroutines.experimental.reactive.awaitSingle
25+
import kotlinx.coroutines.experimental.runBlocking
2726
import kotlinx.coroutines.experimental.yield
2827
import org.hamcrest.core.IsEqual
2928
import org.hamcrest.core.IsInstanceOf
3029
import org.junit.Assert
30+
import org.junit.Assert.assertEquals
31+
import org.junit.Before
32+
import org.junit.Test
3133
import reactor.core.publisher.Flux
3234
import reactor.core.publisher.Mono
3335

3436
/**
3537
* Tests emitting single item with [mono].
3638
*/
3739
class MonoTest : TestBase() {
40+
@Before
41+
fun setup() {
42+
ignoreLostThreads("timer-")
43+
}
44+
3845
@Test
3946
fun testBasicSuccess() = runBlocking {
4047
expect(1)

reactive/kotlinx-coroutines-reactor/src/test/kotlin/kotlinx/coroutines/experimental/reactor/SchedulerTest.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,24 @@
1616

1717
package kotlinx.coroutines.experimental.reactor
1818

19+
import guide.test.ignoreLostThreads
1920
import kotlinx.coroutines.experimental.TestBase
2021
import kotlinx.coroutines.experimental.delay
2122
import kotlinx.coroutines.experimental.run
2223
import kotlinx.coroutines.experimental.runBlocking
2324
import org.hamcrest.core.IsEqual
2425
import org.hamcrest.core.IsNot
2526
import org.junit.Assert.assertThat
27+
import org.junit.Before
2628
import org.junit.Test
2729
import reactor.core.scheduler.Schedulers
2830

2931
class SchedulerTest : TestBase() {
32+
@Before
33+
fun setup() {
34+
ignoreLostThreads("single-")
35+
}
36+
3037
@Test
3138
fun testSingleScheduler(): Unit = runBlocking {
3239
expect(1)

reactive/kotlinx-coroutines-rx1/src/test/kotlin/kotlinx/coroutines/experimental/rx1/SchedulerTest.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,24 @@
1616

1717
package kotlinx.coroutines.experimental.rx1
1818

19-
import kotlinx.coroutines.experimental.*
19+
import guide.test.ignoreLostThreads
20+
import kotlinx.coroutines.experimental.TestBase
21+
import kotlinx.coroutines.experimental.delay
22+
import kotlinx.coroutines.experimental.run
23+
import kotlinx.coroutines.experimental.runBlocking
2024
import org.hamcrest.core.IsEqual
2125
import org.hamcrest.core.IsNot
2226
import org.junit.Assert.assertThat
27+
import org.junit.Before
2328
import org.junit.Test
2429
import rx.schedulers.Schedulers
2530

2631
class SchedulerTest : TestBase() {
32+
@Before
33+
fun setup() {
34+
ignoreLostThreads("RxIoScheduler-")
35+
}
36+
2737
@Test
2838
fun testIoScheduler(): Unit = runBlocking {
2939
expect(1)

reactive/kotlinx-coroutines-rx1/src/test/kotlin/kotlinx/coroutines/experimental/rx1/SingleTest.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616

1717
package kotlinx.coroutines.experimental.rx1
1818

19+
import guide.test.ignoreLostThreads
1920
import kotlinx.coroutines.experimental.*
2021
import org.hamcrest.core.IsEqual
2122
import org.hamcrest.core.IsInstanceOf
2223
import org.hamcrest.core.IsNull
23-
import org.junit.Assert.*
24+
import org.junit.Assert.assertEquals
25+
import org.junit.Assert.assertThat
26+
import org.junit.Before
2427
import org.junit.Test
2528
import rx.Observable
2629
import rx.Single
@@ -30,6 +33,11 @@ import java.util.concurrent.TimeUnit
3033
* Tests emitting single item with [rxSingle].
3134
*/
3235
class SingleTest : TestBase() {
36+
@Before
37+
fun setup() {
38+
ignoreLostThreads("RxComputationScheduler-", "RxIoScheduler-")
39+
}
40+
3341
@Test
3442
fun testBasicSuccess() = runBlocking<Unit> {
3543
expect(1)

reactive/kotlinx-coroutines-rx2/src/test/kotlin/guide/test/ReactiveTestBase.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ open class ReactiveTestBase {
2828
fun setup() {
2929
RxJavaPlugins.setIoSchedulerHandler(Handler)
3030
RxJavaPlugins.setComputationSchedulerHandler(Handler)
31+
ignoreLostThreads(
32+
"RxComputationThreadPool-",
33+
"RxCachedThreadScheduler-",
34+
"RxCachedWorkerPoolEvictor-",
35+
"RxSchedulerPurge-")
3136
}
3237

3338
@After

0 commit comments

Comments
 (0)