Skip to content

Commit 731f0ad

Browse files
committed
Generated unit test for all examples in the guide
1 parent 8c86afb commit 731f0ad

File tree

10 files changed

+966
-76
lines changed

10 files changed

+966
-76
lines changed

coroutines-guide.md

Lines changed: 188 additions & 28 deletions
Large diffs are not rendered by default.

knit/src/Knit.kt

Lines changed: 142 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,37 @@
1414
* limitations under the License.
1515
*/
1616

17-
import java.io.File
18-
import java.io.IOException
19-
import java.io.LineNumberReader
20-
import java.io.Reader
17+
import java.io.*
2118

22-
val DIRECTIVE_START = "<!--- "
23-
val DIRECTIVE_END = "-->"
19+
const val DIRECTIVE_START = "<!--- "
20+
const val DIRECTIVE_END = "-->"
2421

25-
val TOC_DIRECTIVE = "TOC"
26-
val KNIT_DIRECTIVE = "KNIT"
27-
val INCLUDE_DIRECTIVE = "INCLUDE"
28-
val CLEAR_DIRECTIVE = "CLEAR"
22+
const val TOC_DIRECTIVE = "TOC"
23+
const val KNIT_DIRECTIVE = "KNIT"
24+
const val INCLUDE_DIRECTIVE = "INCLUDE"
25+
const val CLEAR_DIRECTIVE = "CLEAR"
26+
const val TEST_DIRECTIVE = "TEST"
2927

30-
val SITE_ROOT_DIRECTIVE = "SITE_ROOT"
31-
val DOCS_ROOT_DIRECTIVE = "DOCS_ROOT"
32-
val INDEX_DIRECTIVE = "INDEX"
28+
const val TEST_OUT_DIRECTIVE = "TEST_OUT"
3329

34-
val CODE_START = "```kotlin"
35-
val CODE_END = "```"
30+
const val SITE_ROOT_DIRECTIVE = "SITE_ROOT"
31+
const val DOCS_ROOT_DIRECTIVE = "DOCS_ROOT"
32+
const val INDEX_DIRECTIVE = "INDEX"
3633

37-
val SECTION_START = "##"
34+
const val CODE_START = "```kotlin"
35+
const val CODE_END = "```"
36+
37+
const val TEST_START = "```text"
38+
const val TEST_END = "```"
39+
40+
const val SECTION_START = "##"
41+
42+
const val PACKAGE_PREFIX = "package "
43+
const val STARTS_WITH_PREDICATE = "STARTS_WITH"
44+
const val FLEXIBLE_TIME_PREDICATE = "FLEXIBLE_TIME"
45+
const val FLEXIBLE_THREAD_PREDICATE = "FLEXIBLE_THREAD"
46+
const val LINES_START_UNORDERED_PREDICATE = "LINES_START_UNORDERED"
47+
const val LINES_START_PREDICATE = "LINES_START"
3848

3949
val API_REF_REGEX = Regex("(^|[ \\]])\\[([A-Za-z0-9_.]+)\\]($|[^\\[\\(])")
4050

@@ -53,6 +63,9 @@ fun knit(markdownFileName: String) {
5363
var knitRegex: Regex? = null
5464
val includes = arrayListOf<Include>()
5565
val code = arrayListOf<String>()
66+
val test = arrayListOf<String>()
67+
var testOut: PrintWriter? = null
68+
var lastPgk: String? = null
5669
val files = mutableSetOf<String>()
5770
val allApiRefs = arrayListOf<ApiRef>()
5871
val remainingApiRefNames = mutableSetOf<String>()
@@ -72,39 +85,59 @@ fun knit(markdownFileName: String) {
7285
when (directive?.name) {
7386
TOC_DIRECTIVE -> {
7487
requireSingleLine(directive)
75-
require(directive.param.isEmpty()) { "TOC directive must not have parameters" }
88+
require(directive.param.isEmpty()) { "$TOC_DIRECTIVE directive must not have parameters" }
7689
require(markdownPart == MarkdownPart.PRE_TOC) { "Only one TOC directive is supported" }
7790
markdownPart = MarkdownPart.TOC
7891
}
7992
KNIT_DIRECTIVE -> {
8093
requireSingleLine(directive)
81-
require(!directive.param.isEmpty()) { "KNIT directive must include regex parameter" }
94+
require(!directive.param.isEmpty()) { "$KNIT_DIRECTIVE directive must include regex parameter" }
8295
require(knitRegex == null) { "Only one KNIT directive is supported"}
8396
knitRegex = Regex("\\((" + directive.param + ")\\)")
8497
continue@mainLoop
8598
}
8699
INCLUDE_DIRECTIVE -> {
87-
require(!directive.param.isEmpty()) { "INCLUDE directive must include regex parameter" }
100+
require(!directive.param.isEmpty()) { "$INCLUDE_DIRECTIVE directive must include regex parameter" }
88101
val include = Include(Regex(directive.param))
89102
if (directive.singleLine) {
90103
include.lines += code
91104
code.clear()
92105
} else {
93-
while (true) {
94-
val includeLine = readLine() ?: break
95-
if (includeLine.startsWith(DIRECTIVE_END)) break
96-
include.lines += includeLine
97-
}
106+
readUntilTo(DIRECTIVE_END, include.lines)
98107
}
99108
includes += include
100109
continue@mainLoop
101110
}
102111
CLEAR_DIRECTIVE -> {
103112
requireSingleLine(directive)
104-
require(directive.param.isEmpty()) { "CLEAR directive must not have parameters" }
113+
require(directive.param.isEmpty()) { "$CLEAR_DIRECTIVE directive must not have parameters" }
105114
code.clear()
106115
continue@mainLoop
107116
}
117+
TEST_OUT_DIRECTIVE -> {
118+
require(!directive.param.isEmpty()) { "$TEST_OUT_DIRECTIVE directive must include file name parameter" }
119+
val file = File(directive.param)
120+
file.parentFile?.mkdirs()
121+
closeTestOut(testOut)
122+
println("Writing tests to ${directive.param}")
123+
testOut = PrintWriter(file)
124+
readUntil(DIRECTIVE_END).forEach { testOut!!.println(it) }
125+
}
126+
TEST_DIRECTIVE -> {
127+
require(lastPgk != null) { "'$PACKAGE_PREFIX' prefix was not found in emitted code"}
128+
require(testOut != null) { "$TEST_OUT_DIRECTIVE directive was not specified" }
129+
var predicate = directive.param
130+
if (test.isEmpty()) {
131+
if (directive.singleLine) {
132+
require(!predicate.isEmpty()) { "$TEST_OUT_DIRECTIVE must be preceded by $TEST_START block or contain test predicate"}
133+
} else
134+
test += readUntil(DIRECTIVE_END)
135+
} else {
136+
requireSingleLine(directive)
137+
}
138+
writeTest(testOut!!, lastPgk!!, test, predicate)
139+
test.clear()
140+
}
108141
SITE_ROOT_DIRECTIVE -> {
109142
requireSingleLine(directive)
110143
siteRoot = directive.param
@@ -132,12 +165,14 @@ fun knit(markdownFileName: String) {
132165
}
133166
}
134167
if (inLine.startsWith(CODE_START)) {
168+
require(test.isEmpty()) { "Previous test was not emitted with $TEST_DIRECTIVE" }
135169
code += ""
136-
while (true) {
137-
val codeLine = readLine() ?: break
138-
if (codeLine.startsWith(CODE_END)) break
139-
code += codeLine
140-
}
170+
readUntilTo(CODE_END, code)
171+
continue@mainLoop
172+
}
173+
if (inLine.startsWith(TEST_START)) {
174+
require(test.isEmpty()) { "Previous test was not emitted with $TEST_DIRECTIVE" }
175+
readUntilTo(TEST_END, test)
141176
continue@mainLoop
142177
}
143178
if (inLine.startsWith(SECTION_START) && markdownPart == MarkdownPart.POST_TOC) {
@@ -160,12 +195,10 @@ fun knit(markdownFileName: String) {
160195
for (include in includes) {
161196
val includeMatch = include.regex.matchEntire(fileName) ?: continue
162197
include.lines.forEach { includeLine ->
163-
var toOutLine = includeLine
164-
for ((id, group) in includeMatch.groups.withIndex()) {
165-
if (group != null)
166-
toOutLine = toOutLine.replace("\$\$$id", group.value)
167-
}
168-
outLines += toOutLine
198+
val line = makeReplacements(includeLine, includeMatch)
199+
if (line.startsWith(PACKAGE_PREFIX))
200+
lastPgk = line.substring(PACKAGE_PREFIX.length).trim()
201+
outLines += line
169202
}
170203
}
171204
outLines += code
@@ -176,6 +209,8 @@ fun knit(markdownFileName: String) {
176209
}
177210
}
178211
}
212+
// close test output
213+
closeTestOut(testOut)
179214
// update markdown file with toc
180215
val newLines = buildList<String> {
181216
addAll(markdown.preTocText)
@@ -195,6 +230,77 @@ fun knit(markdownFileName: String) {
195230
}
196231
}
197232

233+
fun writeTest(testOut: PrintWriter, pgk: String, test: List<String>, predicate: String) {
234+
val funName = buildString {
235+
var cap = true
236+
for (c in pgk) {
237+
if (c == '.') {
238+
cap = true
239+
} else {
240+
append(if (cap) c.toUpperCase() else c)
241+
cap = false
242+
}
243+
}
244+
}
245+
with (testOut) {
246+
println()
247+
println(" @Test")
248+
println(" fun test$funName() {")
249+
print (" test { $pgk.main(emptyArray()) }")
250+
when (predicate) {
251+
"" -> writeTestLines("verifyLines", test)
252+
STARTS_WITH_PREDICATE -> writeTestLines("verifyLinesStartWith", test)
253+
FLEXIBLE_TIME_PREDICATE -> writeTestLines("verifyLinesFlexibleTime", test)
254+
FLEXIBLE_THREAD_PREDICATE -> writeTestLines("verifyLinesFlexibleThread", test)
255+
LINES_START_UNORDERED_PREDICATE -> writeTestLines("verifyLinesStartUnordered", test)
256+
LINES_START_PREDICATE -> writeTestLines("verifyLinesStart", test)
257+
else -> {
258+
println(".also { lines ->")
259+
println(" check($predicate)")
260+
println(" }")
261+
}
262+
}
263+
println(" }")
264+
}
265+
}
266+
267+
private fun PrintWriter.writeTestLines(method: String, test: List<String>) {
268+
println(".$method(")
269+
for ((index, testLine) in test.withIndex()) {
270+
val commaOpt = if (index < test.size - 1) "," else ""
271+
val escapedLine = testLine.replace("\"", "\\\"")
272+
println(" \"$escapedLine\"$commaOpt")
273+
}
274+
println(" )")
275+
}
276+
277+
private fun makeReplacements(line: String, match: MatchResult): String {
278+
var result = line
279+
for ((id, group) in match.groups.withIndex()) {
280+
if (group != null)
281+
result = result.replace("\$\$$id", group.value)
282+
}
283+
return result
284+
}
285+
286+
private fun closeTestOut(testOut: PrintWriter?) {
287+
if (testOut != null) {
288+
testOut.println("}")
289+
testOut.close()
290+
}
291+
}
292+
293+
private fun MarkdownTextReader.readUntil(marker: String): List<String> =
294+
arrayListOf<String>().also { readUntilTo(marker, it) }
295+
296+
private fun MarkdownTextReader.readUntilTo(marker: String, list: MutableList<String>) {
297+
while (true) {
298+
val line = readLine() ?: break
299+
if (line.startsWith(marker)) break
300+
list += line
301+
}
302+
}
303+
198304
private inline fun <T> buildList(block: ArrayList<T>.() -> Unit): List<T> {
199305
val result = arrayListOf<T>()
200306
result.block()

kotlinx-coroutines-core/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Top-level suspending functions:
4141

4242
| **Receiver** | **Suspending function** | **Select clause** | **Non-suspending version**
4343
| ---------------- | --------------------------------------------- | ------------------------------------------------ | --------------------------
44-
| [Job] | [join][Job.join] | [onJoin][SelectBuilder.onJoin] | [isCompleted][Job.isCompleted]
44+
| [Job] | [join][Job.join] | [onJoin][kotlinx.coroutines.experimental.selects.SelectBuilder.onJoin] | [isCompleted][Job.isCompleted]
4545
| [Deferred] | [await][Deferred.await] | [onAwait][kotlinx.coroutines.experimental.selects.SelectBuilder.onAwait] | [isCompleted][Job.isCompleted]
4646
| [SendChannel][kotlinx.coroutines.experimental.channels.SendChannel] | [send][kotlinx.coroutines.experimental.channels.SendChannel.send] | [onSend][kotlinx.coroutines.experimental.selects.SelectBuilder.onSend] | [offer][kotlinx.coroutines.experimental.channels.SendChannel.offer]
4747
| [ReceiveChannel][kotlinx.coroutines.experimental.channels.ReceiveChannel] | [receive][kotlinx.coroutines.experimental.channels.ReceiveChannel.receive] | [onReceive][kotlinx.coroutines.experimental.selects.SelectBuilder.onReceive] | [poll][kotlinx.coroutines.experimental.channels.ReceiveChannel.poll]
@@ -90,8 +90,9 @@ Select expression to perform multiple suspending operations simultaneously until
9090
[yield]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/yield.html
9191
[run]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/run.html
9292
[withTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/with-timeout.html
93-
[Deferred.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/await.html
93+
[Job.join]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/join.html
9494
[Job.isCompleted]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/is-completed.html
95+
[Deferred.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/await.html
9596
[suspendCancellableCoroutine]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/suspend-cancellable-coroutine.html
9697
[NonCancellable]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-non-cancellable/index.html
9798
[newCoroutineContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/new-coroutine-context.html
@@ -112,6 +113,7 @@ Select expression to perform multiple suspending operations simultaneously until
112113
[kotlinx.coroutines.experimental.channels.ReceiveChannel.receiveOrNull]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental.channels/receive-or-null.html
113114
<!--- INDEX kotlinx.coroutines.experimental.selects -->
114115
[kotlinx.coroutines.experimental.selects.select]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental.selects/select.html
116+
[kotlinx.coroutines.experimental.selects.SelectBuilder.onJoin]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental.selects/on-join.html
115117
[kotlinx.coroutines.experimental.selects.SelectBuilder.onAwait]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental.selects/on-await.html
116118
[kotlinx.coroutines.experimental.selects.SelectBuilder.onSend]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental.selects/on-send.html
117119
[kotlinx.coroutines.experimental.selects.SelectBuilder.onReceive]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental.selects/on-receive.html

kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CommonPool.kt

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package kotlinx.coroutines.experimental
1818

1919
import java.util.concurrent.ExecutorService
2020
import java.util.concurrent.Executors
21+
import java.util.concurrent.TimeUnit
2122
import java.util.concurrent.atomic.AtomicInteger
2223
import kotlin.coroutines.experimental.CoroutineContext
2324

@@ -28,15 +29,20 @@ import kotlin.coroutines.experimental.CoroutineContext
2829
* When available, it wraps `ForkJoinPool.commonPool` and provides a similar shared pool where not.
2930
*/
3031
object CommonPool : CoroutineDispatcher() {
31-
private val pool: ExecutorService = findPool()
32+
private var usePrivatePool = false
33+
34+
@Volatile
35+
private var _pool: ExecutorService? = null
3236

3337
private inline fun <T> Try(block: () -> T) = try { block() } catch (e: Throwable) { null }
3438

35-
private fun findPool(): ExecutorService {
39+
private fun createPool(): ExecutorService {
3640
val fjpClass = Try { Class.forName("java.util.concurrent.ForkJoinPool") }
3741
?: return createPlainPool()
38-
Try { fjpClass.getMethod("commonPool")?.invoke(null) as? ExecutorService }
39-
?. let { return it }
42+
if (!usePrivatePool) {
43+
Try { fjpClass.getMethod("commonPool")?.invoke(null) as? ExecutorService }
44+
?.let { return it }
45+
}
4046
Try { fjpClass.getConstructor(Int::class.java).newInstance(defaultParallelism()) as? ExecutorService }
4147
?. let { return it }
4248
return createPlainPool()
@@ -51,5 +57,29 @@ object CommonPool : CoroutineDispatcher() {
5157

5258
private fun defaultParallelism() = (Runtime.getRuntime().availableProcessors() - 1).coerceAtLeast(1)
5359

54-
override fun dispatch(context: CoroutineContext, block: Runnable) = pool.execute(block)
60+
@Synchronized
61+
private fun getOrCreatePoolSync(): ExecutorService =
62+
_pool ?: createPool().also { _pool = it }
63+
64+
override fun dispatch(context: CoroutineContext, block: Runnable) =
65+
(_pool ?: getOrCreatePoolSync()).execute(block)
66+
67+
// used for tests
68+
@Synchronized
69+
internal fun usePrivatePool() {
70+
shutdownAndRelease(0)
71+
usePrivatePool = true
72+
}
73+
74+
// used for tests
75+
@Synchronized
76+
internal fun shutdownAndRelease(timeout: Long) {
77+
_pool?.apply {
78+
shutdown()
79+
if (timeout > 0)
80+
awaitTermination(timeout, TimeUnit.MILLISECONDS)
81+
_pool = null
82+
}
83+
usePrivatePool = false
84+
}
5585
}

kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CoroutineContext.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ private val DEBUG = run {
3434

3535
private val COROUTINE_ID = AtomicLong()
3636

37+
// for tests only
38+
internal fun resetCoroutineId() {
39+
COROUTINE_ID.set(0)
40+
}
41+
3742
/**
3843
* A coroutine dispatcher that is not confined to any specific thread.
3944
* It executes initial continuation of the coroutine _right here_ in the current call-frame

0 commit comments

Comments
 (0)