Skip to content

Commit 8bff72b

Browse files
committed
MPP: Common tests framework & CommonCoroutinesTest
1 parent 2a4fb06 commit 8bff72b

File tree

6 files changed

+462
-333
lines changed

6 files changed

+462
-333
lines changed

build.gradle

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,24 @@ configure(subprojects.findAll { !sourceless.contains(it.name) }) {
9090
}
9191

9292
def kotlin_stdlib = platformLib("kotlin-stdlib", platform)
93+
def kotlin_test = platformLib("kotlin-test", platform)
9394

9495
dependencies {
9596
compile "org.jetbrains.kotlin:$kotlin_stdlib:$kotlin_version"
96-
testCompile "junit:junit:$junit_version"
97+
testCompile "org.jetbrains.kotlin:$kotlin_test:$kotlin_version"
98+
}
99+
100+
if (platform == "common") {
101+
dependencies {
102+
testCompile "org.jetbrains.kotlin:kotlin-test-annotations-common:$kotlin_version"
103+
}
104+
}
105+
106+
if (platform == "jvm") {
107+
dependencies {
108+
testCompile "junit:junit:$junit_version"
109+
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
110+
}
97111
}
98112
}
99113

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
/*
2+
* Copyright 2016-2017 JetBrains s.r.o.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
@file:Suppress("NAMED_ARGUMENTS_NOT_ALLOWED") // KT-21913
18+
19+
package kotlinx.coroutines.experimental
20+
21+
import kotlin.test.*
22+
23+
class CommonCoroutinesTest : TestBase() {
24+
@Test
25+
fun testSimple() = runTest {
26+
expect(1)
27+
finish(2)
28+
}
29+
30+
@Test
31+
fun testYield() = runTest {
32+
expect(1)
33+
yield() // effectively does nothing, as we don't have other coroutines
34+
finish(2)
35+
}
36+
37+
@Test
38+
fun testLaunchAndYieldJoin() = runTest {
39+
expect(1)
40+
val job = launch(coroutineContext) {
41+
expect(3)
42+
yield()
43+
expect(4)
44+
}
45+
expect(2)
46+
check(job.isActive && !job.isCompleted)
47+
job.join()
48+
check(!job.isActive && job.isCompleted)
49+
finish(5)
50+
}
51+
52+
@Test
53+
fun testLaunchUndispatched() = runTest {
54+
expect(1)
55+
val job = launch(coroutineContext, start = CoroutineStart.UNDISPATCHED) {
56+
expect(2)
57+
yield()
58+
expect(4)
59+
}
60+
expect(3)
61+
check(job.isActive && !job.isCompleted)
62+
job.join()
63+
check(!job.isActive && job.isCompleted)
64+
finish(5)
65+
}
66+
67+
@Test
68+
fun testNested() = runTest {
69+
expect(1)
70+
val j1 = launch(coroutineContext) {
71+
expect(3)
72+
val j2 = launch(coroutineContext) {
73+
expect(5)
74+
}
75+
expect(4)
76+
j2.join()
77+
expect(6)
78+
}
79+
expect(2)
80+
j1.join()
81+
finish(7)
82+
}
83+
84+
@Test
85+
fun testWaitChild() = runTest {
86+
expect(1)
87+
launch(coroutineContext) {
88+
expect(3)
89+
yield() // to parent
90+
finish(5)
91+
}
92+
expect(2)
93+
yield()
94+
expect(4)
95+
// parent waits for child's completion
96+
}
97+
98+
@Test
99+
fun testCancelChildExplicit() = runTest {
100+
expect(1)
101+
val job = launch(coroutineContext) {
102+
expect(3)
103+
yield()
104+
expectUnreached()
105+
}
106+
expect(2)
107+
yield()
108+
expect(4)
109+
job.cancel()
110+
finish(5)
111+
}
112+
113+
@Test
114+
fun testCancelChildWithFinally() = runTest {
115+
expect(1)
116+
val job = launch(coroutineContext) {
117+
expect(3)
118+
try {
119+
yield()
120+
} finally {
121+
finish(6) // cancelled child will still execute finally
122+
}
123+
expectUnreached()
124+
}
125+
expect(2)
126+
yield()
127+
expect(4)
128+
job.cancel()
129+
expect(5)
130+
}
131+
132+
@Test
133+
fun testWaitNestedChild() = runTest {
134+
expect(1)
135+
launch(coroutineContext) {
136+
expect(3)
137+
launch(coroutineContext) {
138+
expect(6)
139+
yield() // to parent
140+
expect(9)
141+
}
142+
expect(4)
143+
yield()
144+
expect(7)
145+
yield() // to parent
146+
finish(10) // the last one to complete
147+
}
148+
expect(2)
149+
yield()
150+
expect(5)
151+
yield()
152+
expect(8)
153+
// parent waits for child
154+
}
155+
156+
@Test
157+
fun testExceptionPropagation() = runTest(
158+
expected = { it is TestException }
159+
) {
160+
finish(1)
161+
throw TestException()
162+
}
163+
164+
@Test
165+
fun testCancelParentOnChildException() = runTest(
166+
expected = { it is JobCancellationException && it.cause is TestException },
167+
unhandled = listOf({ it -> it is TestException })
168+
) {
169+
expect(1)
170+
launch(coroutineContext) {
171+
finish(3)
172+
throwTestException() // does not propagate exception to launch, but cancels parent (!)
173+
expectUnreached()
174+
}
175+
expect(2)
176+
yield()
177+
expectUnreached() // because of exception in child
178+
}
179+
180+
@Test
181+
fun testCancelParentOnNestedException() = runTest(
182+
expected = { it is JobCancellationException && it.cause is TestException },
183+
unhandled = listOf(
184+
{ it -> it is TestException },
185+
{ it -> it is TestException }
186+
)
187+
) {
188+
expect(1)
189+
launch(coroutineContext) {
190+
expect(3)
191+
launch(coroutineContext) {
192+
finish(6)
193+
throwTestException() // unhandled exception kills all parents
194+
expectUnreached()
195+
}
196+
expect(4)
197+
yield()
198+
expectUnreached() // because of exception in child
199+
}
200+
expect(2)
201+
yield()
202+
expect(5)
203+
yield()
204+
expectUnreached() // because of exception in child
205+
}
206+
207+
@Test
208+
fun testJoinWithFinally() = runTest {
209+
expect(1)
210+
val job = launch(coroutineContext) {
211+
expect(3)
212+
try {
213+
yield() // to main, will cancel us
214+
} finally {
215+
expect(7) // join is waiting
216+
}
217+
}
218+
expect(2)
219+
yield() // to job
220+
expect(4)
221+
check(job.isActive && !job.isCompleted && !job.isCancelled)
222+
check(job.cancel()) // cancels job
223+
expect(5) // still here
224+
check(!job.isActive && !job.isCompleted && job.isCancelled)
225+
check(!job.cancel()) // second attempt returns false
226+
expect(6) // we're still here
227+
job.join() // join the job, let job complete its "finally" section
228+
expect(8)
229+
check(!job.isActive && job.isCompleted && job.isCancelled)
230+
finish(9)
231+
}
232+
233+
@Test
234+
fun testCancelAndJoin() = runTest {
235+
expect(1)
236+
val job = launch(coroutineContext, CoroutineStart.UNDISPATCHED) {
237+
try {
238+
expect(2)
239+
yield()
240+
expectUnreached() // will get cancelled
241+
} finally {
242+
expect(4)
243+
}
244+
}
245+
expect(3)
246+
job.cancelAndJoin()
247+
finish(5)
248+
}
249+
250+
@Test
251+
fun testCancelAndJoinChildCrash() = runTest(
252+
expected = { it is JobCancellationException && it.cause is TestException },
253+
unhandled = listOf({it -> it is TestException })
254+
) {
255+
expect(1)
256+
val job = launch(coroutineContext, CoroutineStart.UNDISPATCHED) {
257+
expect(2)
258+
throwTestException()
259+
expectUnreached()
260+
}
261+
// now we have a failed job with TestException
262+
finish(3)
263+
try {
264+
job.cancelAndJoin() // join should crash on child's exception but it will be wrapped into JobCancellationException
265+
} catch (e: Throwable) {
266+
e as JobCancellationException // type assertion
267+
check(e.cause is TestException)
268+
check(e.job === coroutineContext[Job])
269+
throw e
270+
}
271+
expectUnreached()
272+
}
273+
274+
@Test
275+
fun testYieldInFinally() = runTest(
276+
expected = { it is TestException }
277+
) {
278+
expect(1)
279+
try {
280+
expect(2)
281+
throwTestException()
282+
} finally {
283+
expect(3)
284+
yield()
285+
finish(4)
286+
}
287+
expectUnreached()
288+
}
289+
290+
@Test
291+
fun testCancelManyCompletedAttachedChildren() = runTest {
292+
val parent = launch(coroutineContext) { /* do nothing */ }
293+
val n = 10_000 * stressTestMultiplier
294+
repeat(n) {
295+
// create a child that already completed
296+
val child = launch(coroutineContext, CoroutineStart.UNDISPATCHED) { /* do nothing */ }
297+
// attach it manually
298+
parent.attachChild(child)
299+
}
300+
parent.cancelAndJoin() // cancel parent, make sure no stack overflow
301+
}
302+
303+
@Test
304+
fun testCancelAndJoinChildren() = runTest {
305+
expect(1)
306+
val parent = Job()
307+
launch(coroutineContext, CoroutineStart.UNDISPATCHED, parent = parent) {
308+
expect(2)
309+
try {
310+
yield() // to be cancelled
311+
} finally {
312+
expect(5)
313+
}
314+
expectUnreached()
315+
}
316+
expect(3)
317+
parent.cancelChildren()
318+
expect(4)
319+
parent.joinChildren() // will yield to child
320+
check(parent.isActive) // make sure it did not cancel parent
321+
finish(6)
322+
}
323+
324+
private fun throwTestException() { throw TestException() }
325+
326+
private class TestException : Exception {
327+
constructor(message: String): super(message)
328+
constructor(): super()
329+
}
330+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package kotlinx.coroutines.experimental
2+
3+
public expect open class TestBase constructor() {
4+
public val isStressTest: Boolean
5+
public val stressTestMultiplier: Int
6+
7+
@Suppress("EXPECTED_DECLARATION_WITH_DEFAULT_PARAMETER")
8+
public fun error(message: Any, cause: Throwable? = null): Nothing
9+
public fun expect(index: Int)
10+
public fun expectUnreached()
11+
public fun finish(index: Int)
12+
13+
@Suppress("EXPECTED_DECLARATION_WITH_DEFAULT_PARAMETER")
14+
public fun runTest(
15+
expected: ((Throwable) -> Boolean)? = null,
16+
unhandled: List<(Throwable) -> Boolean> = emptyList(),
17+
block: suspend CoroutineScope.() -> Unit
18+
)
19+
}

0 commit comments

Comments
 (0)