Add context receiver extensions for DSLs
This commit is contained in:
1 parent
e107296bca
commit
b2200e71ff
22 files changed
+860
-71
No files matched your search
@@ -15,17 +15,21 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlin.test.Test
|
||||
import net.sergeych.lyng.eval as lyngEval
|
||||
|
||||
class LaunchPoolTest {
|
||||
|
||||
private suspend fun eval(code: String) = withTimeout(2_000L) { lyngEval(code) }
|
||||
private suspend fun eval(code: String) = withContext(Dispatchers.Default) {
|
||||
withTimeout(2_000L) { lyngEval(code) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBasicExecution() = runBlocking<Unit> {
|
||||
fun testBasicExecution() = runTest {
|
||||
eval("""
|
||||
val pool = LaunchPool(2)
|
||||
val d1 = pool.launch { 1 + 1 }
|
||||
@@ -37,7 +41,7 @@ class LaunchPoolTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testResultsCollected() = runBlocking<Unit> {
|
||||
fun testResultsCollected() = runTest {
|
||||
eval("""
|
||||
val pool = LaunchPool(4)
|
||||
val jobs = (1..10).map { n -> pool.launch { n * n } }
|
||||
@@ -48,7 +52,7 @@ class LaunchPoolTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testConcurrencyLimit() = runBlocking<Unit> {
|
||||
fun testConcurrencyLimit() = runTest {
|
||||
eval("""
|
||||
// With maxWorkers=2, at most 2 tasks run at the same time.
|
||||
val mu = Mutex()
|
||||
@@ -70,7 +74,7 @@ class LaunchPoolTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExceptionCapturedInDeferred() = runBlocking<Unit> {
|
||||
fun testExceptionCapturedInDeferred() = runTest {
|
||||
eval("""
|
||||
val pool = LaunchPool(2)
|
||||
val good = pool.launch { 42 }
|
||||
@@ -83,7 +87,7 @@ class LaunchPoolTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPoolContinuesAfterLambdaException() = runBlocking<Unit> {
|
||||
fun testPoolContinuesAfterLambdaException() = runTest {
|
||||
eval("""
|
||||
val pool = LaunchPool(1)
|
||||
val bad = pool.launch { throw IllegalArgumentException("fail") }
|
||||
@@ -96,7 +100,7 @@ class LaunchPoolTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLaunchAfterCloseAndJoinThrows() = runBlocking<Unit> {
|
||||
fun testLaunchAfterCloseAndJoinThrows() = runTest {
|
||||
eval("""
|
||||
val pool = LaunchPool(2)
|
||||
pool.launch { 1 }
|
||||
@@ -107,7 +111,7 @@ class LaunchPoolTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLaunchAfterCancelThrows() = runBlocking<Unit> {
|
||||
fun testLaunchAfterCancelThrows() = runTest {
|
||||
eval("""
|
||||
val pool = LaunchPool(2)
|
||||
pool.cancel()
|
||||
@@ -117,7 +121,7 @@ class LaunchPoolTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCancelAndJoinWaitsForWorkers() = runBlocking<Unit> {
|
||||
fun testCancelAndJoinWaitsForWorkers() = runTest {
|
||||
eval("""
|
||||
val pool = LaunchPool(2)
|
||||
pool.launch { delay(5) }
|
||||
@@ -128,7 +132,7 @@ class LaunchPoolTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCloseAndJoinDrainsQueue() = runBlocking<Unit> {
|
||||
fun testCloseAndJoinDrainsQueue() = runTest {
|
||||
eval("""
|
||||
val mu = Mutex()
|
||||
val results = []
|
||||
@@ -147,7 +151,7 @@ class LaunchPoolTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBoundedQueueSuspendsProducer() = runBlocking<Unit> {
|
||||
fun testBoundedQueueSuspendsProducer() = runTest {
|
||||
eval("""
|
||||
// queue of 2 + 1 worker; producer can only be 1 ahead of what's running
|
||||
val pool = LaunchPool(1, 2)
|
||||
@@ -165,7 +169,7 @@ class LaunchPoolTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUnlimitedQueueDefault() = runBlocking<Unit> {
|
||||
fun testUnlimitedQueueDefault() = runTest {
|
||||
eval("""
|
||||
val pool = LaunchPool(4)
|
||||
val jobs = (1..50).map { n -> pool.launch { n } }
|
||||
@@ -177,7 +181,7 @@ class LaunchPoolTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIdempotentClose() = runBlocking<Unit> {
|
||||
fun testIdempotentClose() = runTest {
|
||||
eval("""
|
||||
val pool = LaunchPool(2)
|
||||
pool.closeAndJoin()
|
||||
|
||||
@@ -190,4 +190,63 @@ class ScriptImportPreparationTest {
|
||||
session.cancelAndJoin()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun importedContextReceiverExtensionIsAvailableInReceiverDsl() = runTest {
|
||||
val manager = Script.defaultImportManager.copy().apply {
|
||||
addTextPackages(
|
||||
"""
|
||||
package imported.ctxdsl
|
||||
|
||||
class Tag(name: String) {
|
||||
val name = name
|
||||
var inner = ""
|
||||
|
||||
fun child(tagName: String, block: Tag.()->void) {
|
||||
val child = Tag(tagName)
|
||||
child.apply { block(this) }
|
||||
inner += child.render()
|
||||
}
|
||||
|
||||
fun h3(block: Tag.()->void) { child("h3", block) }
|
||||
fun addText(text: String) { inner += text }
|
||||
fun render() = "<" + name + ">" + inner + "</" + name + ">"
|
||||
}
|
||||
|
||||
context(Tag)
|
||||
fun String.unaryPlus() {
|
||||
this@Tag.addText(this)
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
val script = Compiler.compile(
|
||||
Source(
|
||||
"<ctx-dsl-import>",
|
||||
"""
|
||||
import imported.ctxdsl
|
||||
|
||||
fun html(block: Tag.()->void) {
|
||||
val root = Tag("html")
|
||||
root.apply { block(this) }
|
||||
root.render()
|
||||
}
|
||||
|
||||
val page = html {
|
||||
h3 {
|
||||
+"Imported"
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals("<html><h3>Imported</h3></html>", page)
|
||||
assertEquals("plain", +"plain")
|
||||
page
|
||||
""".trimIndent()
|
||||
),
|
||||
manager
|
||||
)
|
||||
|
||||
val result = script.execute(manager.newStdScope()) as ObjString
|
||||
assertEquals("<html><h3>Imported</h3></html>", result.value)
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.eval
|
||||
import kotlin.test.Test
|
||||
|
||||
@@ -30,7 +30,7 @@ class TypeInferenceTest {
|
||||
|
||||
/** Channel field type inferred from constructor — accessed in a launch closure */
|
||||
@Test
|
||||
fun testChannelFieldInLaunchClosure() = runBlocking<Unit> {
|
||||
fun testChannelFieldInLaunchClosure() = runTest {
|
||||
eval("""
|
||||
class Foo {
|
||||
private val ch = Channel(Channel.UNLIMITED)
|
||||
@@ -52,7 +52,7 @@ class TypeInferenceTest {
|
||||
|
||||
/** Mutex field type inferred from constructor — used directly in a method body */
|
||||
@Test
|
||||
fun testMutexFieldDirectUse() = runBlocking<Unit> {
|
||||
fun testMutexFieldDirectUse() = runTest {
|
||||
eval("""
|
||||
class Bar {
|
||||
private val mu = Mutex()
|
||||
@@ -69,7 +69,7 @@ class TypeInferenceTest {
|
||||
|
||||
/** CompletableDeferred field type inferred — complete/await used directly */
|
||||
@Test
|
||||
fun testCompletableDeferredFieldDirectUse() = runBlocking<Unit> {
|
||||
fun testCompletableDeferredFieldDirectUse() = runTest {
|
||||
eval("""
|
||||
class Baz {
|
||||
private val d = CompletableDeferred()
|
||||
@@ -84,7 +84,7 @@ class TypeInferenceTest {
|
||||
|
||||
/** Channel field accessed inside a map closure within class initializer */
|
||||
@Test
|
||||
fun testChannelFieldInMapAndLaunchClosure() = runBlocking<Unit> {
|
||||
fun testChannelFieldInMapAndLaunchClosure() = runTest {
|
||||
eval("""
|
||||
class Pool(n) {
|
||||
private val ch = Channel(Channel.UNLIMITED)
|
||||
@@ -106,7 +106,7 @@ class TypeInferenceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIterableFirstPreservesElementTypeForBlockReturnInference() = runBlocking<Unit> {
|
||||
fun testIterableFirstPreservesElementTypeForBlockReturnInference() = runTest {
|
||||
eval("""
|
||||
class Item(title: String)
|
||||
|
||||
@@ -121,7 +121,7 @@ class TypeInferenceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCallableLocalInitializedFromFunctionCallPreservesReturnType() = runBlocking<Unit> {
|
||||
fun testCallableLocalInitializedFromFunctionCallPreservesReturnType() = runTest {
|
||||
eval("""
|
||||
fun makeAdder(base) {
|
||||
return { x -> x + base + 0.5 }
|
||||
|
||||
@@ -53,6 +53,197 @@ class OperatorOverloadingTest {
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUnaryPlusDefaultIdentity() = runTest {
|
||||
eval("""
|
||||
assertEquals(42, +42)
|
||||
assertEquals(3.5, +3.5)
|
||||
assertEquals("abc", +"abc")
|
||||
|
||||
class Box(val text: String) {
|
||||
fun upper() = text.upper()
|
||||
}
|
||||
|
||||
assertEquals("ABC", (+Box("abc")).upper())
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUnaryPlusOverloading() = runTest {
|
||||
eval("""
|
||||
class Counter(val n: Int) {
|
||||
fun unaryPlus() = Counter(this.n + 1)
|
||||
fun equals(other: Counter) = this.n == other.n
|
||||
}
|
||||
|
||||
assertEquals(Counter(6), Counter(5).unaryPlus())
|
||||
assertEquals(Counter(6), +Counter(5))
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUnaryPlusExtensionOverloading() = runTest {
|
||||
eval("""
|
||||
var out = ""
|
||||
fun String.unaryPlus() {
|
||||
out = out + this
|
||||
}
|
||||
|
||||
"Hello".unaryPlus()
|
||||
" ".unaryPlus()
|
||||
"Lyng".unaryPlus()
|
||||
assertEquals("Hello Lyng", out)
|
||||
out = ""
|
||||
|
||||
+"Hello"
|
||||
+" "
|
||||
+"Lyng"
|
||||
assertEquals("Hello Lyng", out)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUnaryPlusDslBuilderStyle() = runTest {
|
||||
eval("""
|
||||
class Tag(name: String) {
|
||||
val name = name
|
||||
var inner = ""
|
||||
|
||||
fun child(tagName: String, block: Tag.()->void) {
|
||||
val child = Tag(tagName)
|
||||
with(child) { block(this) }
|
||||
inner += child.render()
|
||||
}
|
||||
|
||||
fun head(block: Tag.()->void) { child("head", block) }
|
||||
fun body(block: Tag.()->void) { child("body", block) }
|
||||
fun title(block: Tag.()->void) { child("title", block) }
|
||||
fun h1(block: Tag.()->void) { child("h1", block) }
|
||||
|
||||
fun addText(text: String) {
|
||||
inner += text
|
||||
}
|
||||
|
||||
fun render() {
|
||||
"<" + name + ">" + inner + "</" + name + ">"
|
||||
}
|
||||
}
|
||||
|
||||
context(Tag)
|
||||
fun String.unaryPlus() {
|
||||
this@Tag.addText(this)
|
||||
}
|
||||
|
||||
fun html(block: Tag.()->void) {
|
||||
val root = Tag("html")
|
||||
with(root) { block(this) }
|
||||
root.render()
|
||||
}
|
||||
|
||||
val page = html {
|
||||
head {
|
||||
title {
|
||||
+"Demo"
|
||||
}
|
||||
}
|
||||
body {
|
||||
h1 {
|
||||
+"Heading 1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals("<html><head><title>Demo</title></head><body><h1>Heading 1</h1></body></html>", page)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testContextReceiverUnaryPlusDslBuilderStyle() = runTest {
|
||||
eval("""
|
||||
class Tag(name: String) {
|
||||
val name = name
|
||||
var inner = ""
|
||||
|
||||
fun child(tagName: String, block: Tag.()->void) {
|
||||
val child = Tag(tagName)
|
||||
with(child) { block(this) }
|
||||
inner += child.render()
|
||||
}
|
||||
|
||||
fun h3(block: Tag.()->void) { child("h3", block) }
|
||||
|
||||
fun addText(text: String) {
|
||||
inner += text
|
||||
}
|
||||
|
||||
fun render() {
|
||||
"<" + name + ">" + inner + "</" + name + ">"
|
||||
}
|
||||
}
|
||||
|
||||
context(Tag)
|
||||
fun String.unaryPlus() {
|
||||
this@Tag.addText(this)
|
||||
}
|
||||
|
||||
fun html(block: Tag.()->void) {
|
||||
val root = Tag("html")
|
||||
with(root) { block(this) }
|
||||
root.render()
|
||||
}
|
||||
|
||||
val page = html {
|
||||
h3 {
|
||||
+"Heading 3"
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals("<html><h3>Heading 3</h3></html>", page)
|
||||
assertEquals("plain", +"plain")
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testContextReceiverExtensionIsHiddenOutsideContext() = runTest {
|
||||
val ex = assertFailsWith<Throwable> {
|
||||
eval("""
|
||||
class Tag {
|
||||
fun wrap(text: String) = "[" + text + "]"
|
||||
}
|
||||
|
||||
context(Tag)
|
||||
fun String.mark() = this@Tag.wrap(this)
|
||||
|
||||
"x".mark()
|
||||
""".trimIndent())
|
||||
}
|
||||
assertContains(ex.message ?: "", "no such member: mark on String")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testContextReceiverExtensionIsHiddenInWrongContext() = runTest {
|
||||
val ex = assertFailsWith<Throwable> {
|
||||
eval("""
|
||||
class Tag {
|
||||
fun wrap(text: String) = "[" + text + "]"
|
||||
}
|
||||
class Other
|
||||
|
||||
context(Tag)
|
||||
fun String.mark() = this@Tag.wrap(this)
|
||||
|
||||
fun other(block: Other.()->void) {
|
||||
with(Other()) { block(this) }
|
||||
}
|
||||
|
||||
other {
|
||||
"x".mark()
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
assertContains(ex.message ?: "", "no such member: mark on String")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPlusAssignOverloading() = runTest {
|
||||
eval("""
|
||||
|
||||
Reference in new issue
Block a user