Skip to content

Commit 75583ad

Browse files
author
Simon
committed
re-wrote tests to run on junit 5 jupiter; created custom exception type which is thrown for script loading errors; added feature to provide custom classloader to script loading
1 parent 4b58682 commit 75583ad

File tree

8 files changed

+134
-54
lines changed

8 files changed

+134
-54
lines changed

README.md

+19-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ println(loadedObj.x)
3333
// >> I was created in kts
3434
```
3535

36-
As shown, the `KtsObjectLoader` class can be used for executing a `.kts` script and return its result. The example shows a script that creates an instance of the `ClassFromScript` type that is loaded via ``KtsObjectLoader`` and then processed in the regular program.
36+
As shown, the `KtsObjectLoader` can be used for executing a `.kts` script and getting its result. The example shows a script that creates an instance of the `ClassFromScript` type that is loaded via ``KtsObjectLoader`` and then processed in the regular program.
3737

3838
### Executing scripts directly
3939

@@ -49,8 +49,25 @@ println(fromScript)
4949

5050
### Application Area
5151

52-
You might want to use **KtsRunner** when some part of your application's source has to be outsourced from the regular code. As an example, you can think of an application that provides a test suite runtime. The actual test cases are provided by technical testers who write their test scripts using a **domain specific language** that is provided by the main application. Since you don't want testers to add source files (defining new test cases) to your application all the time, the test case creation is made in independent `.kts` (Kotlin Scripting) files in which the DSL is utilized by the testing team. The test suite main application can use the presented **KtsRunner** library for loading the test cases provided in `.kts` files and process them further afterward.
52+
You might want to use **KtsRunner** when some part of your application's source has to be outsourced from the regular code. As an example, you can think of an application that provides a test suite runtime. The actual test cases are provided by a QA team which writes their test scripts using a **domain specific language** that is provided by the main application. Since you don't want QA to add source files (defining new test cases) to your application all the time, the test case creation is made via independent `.kts` (Kotlin Scripting) files in which the DSL is being utilized. The test suite main application can use the presented **KtsRunner** library for loading the test cases provided in `.kts` files and process them further afterward.
5353

54+
### Controlling the ClassLoader
55+
56+
When instantiating an `KtsObjectLoader`, you can provide an explicit classloader as shown in this test case:
57+
58+
```kotlin
59+
@Test
60+
fun `when passing a custom classloader, it should be used when loading the script`() {
61+
val myCl = object : ClassLoader() {
62+
override fun loadClass(name: String?): Class<*> {
63+
throw IllegalStateException()
64+
}
65+
}
66+
assertExceptionThrownBy<IllegalStateException> {
67+
KtsObjectLoader(myCl).load("anything")
68+
}
69+
}
70+
```
5471
## Getting Started
5572

5673
In your Gradle build, simply include the following repository and dependency:

build.gradle.kts

+10-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import org.gradle.api.publish.maven.MavenPom
66
val kotlinVersion = plugins.getPlugin(KotlinPluginWrapper::class.java).kotlinPluginVersion
77

88
project.group = "de.swirtz"
9-
project.version = "0.0.6"
9+
project.version = "0.0.7"
1010
val artifactID = "ktsRunner"
1111

1212
plugins {
@@ -15,6 +15,11 @@ plugins {
1515
`java-library`
1616
id("com.jfrog.bintray") version "1.8.0"
1717
}
18+
tasks {
19+
"test"(Test::class) {
20+
useJUnitPlatform()
21+
}
22+
}
1823

1924
dependencies {
2025
implementation(kotlin("stdlib-jdk8", kotlinVersion))
@@ -25,8 +30,9 @@ dependencies {
2530
implementation(kotlin("compiler-embeddable", kotlinVersion))
2631
implementation(kotlin("script-util", kotlinVersion))
2732

28-
testImplementation(kotlin("test-junit", kotlinVersion))
29-
testImplementation("junit:junit:4.11")
33+
testImplementation("org.junit.jupiter:junit-jupiter-api:5.3.2")
34+
testImplementation("org.assertj:assertj-core:3.11.1")
35+
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.3.2")
3036
}
3137

3238

@@ -82,5 +88,6 @@ tasks {
8288
withType<GenerateMavenPom> {
8389
destination = file("$buildDir/libs/$artifactID.pom")
8490
}
91+
8592
}
8693

src/main/kotlin/de/swirtz/ktsrunner/objectloader/KtsObjectLoader.kt

+14-9
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,26 @@ import java.io.Reader
55
import javax.script.ScriptEngineManager
66

77
/**
8-
*
98
* This class is not thread-safe, don't use it for parallel executions and create new instances instead.
109
*/
11-
class KtsObjectLoader {
10+
class KtsObjectLoader(classLoader: ClassLoader? = Thread.currentThread().contextClassLoader) {
1211

13-
val engine = ScriptEngineManager().getEngineByExtension("kts")
12+
val engine = ScriptEngineManager(classLoader).getEngineByExtension("kts")
1413

15-
inline fun <reified T> load(script: String): T = engine.eval(script).takeIf { it is T } as T
16-
?: throw IllegalStateException("Could not load script from .kts")
14+
inline fun <R> safeEval(evaluation: () -> R?) = try {
15+
evaluation()
16+
} catch (e: Exception) {
17+
throw LoadException("Cannot load script", e)
18+
}
1719

18-
inline fun <reified T> load(reader: Reader): T = engine.eval(reader).takeIf { it is T } as T
19-
?: throw IllegalStateException("Could not load script from .kts")
20+
inline fun <reified T> Any?.castOrError() = takeIf { it is T }?.let { it as T }
21+
?: throw IllegalArgumentException("Cannot cast $this to expected type ${T::class}")
2022

21-
inline fun <reified T> load(inputStream: InputStream): T = load<T>(inputStream.reader())
22-
?: throw IllegalStateException("Could not load script from .kts")
23+
inline fun <reified T> load(script: String): T = safeEval { engine.eval(script) }.castOrError()
24+
25+
inline fun <reified T> load(reader: Reader): T = safeEval { engine.eval(reader) }.castOrError()
26+
27+
inline fun <reified T> load(inputStream: InputStream): T = load(inputStream.reader())
2328

2429
inline fun <reified T> loadAll(vararg inputStream: InputStream): List<T> = inputStream.map(::load)
2530
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package de.swirtz.ktsrunner.objectloader
2+
3+
class LoadException(message: String? = null, cause: Throwable? = null) : RuntimeException(message, cause)
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
package de.swirtz.ktsrunner.objectloader
22

3-
data class ClassFromScript(val x: String) {
4-
fun printme() = println("ClassFromScript with x=$x")
3+
data class ClassFromScript(val text: String) {
4+
fun printMe() = println("ClassFromScript with text=$text")
55
}
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,124 @@
11
package de.swirtz.ktsrunner.objectloader
22

3+
import org.assertj.core.api.Assertions.assertThat
4+
import org.assertj.core.api.Assertions.fail
35
import org.jetbrains.kotlin.config.KotlinCompilerVersion
46
import org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngine
5-
import org.junit.Test
7+
import org.junit.jupiter.api.Test
68
import java.nio.file.Files
79
import java.nio.file.Paths
810
import javax.script.ScriptEngine
9-
import kotlin.test.assertEquals
10-
import kotlin.test.assertNull
11-
import kotlin.test.fail
1211

1312
class KtsObjectLoaderTest {
1413

1514
@Test
1615
fun `general ScriptEngineFactory test`() {
17-
KtsObjectLoader().engine.factory.apply {
18-
assertEquals("kotlin", languageName)
19-
assertEquals(KotlinCompilerVersion.VERSION, languageVersion)
20-
assertEquals("kotlin", engineName)
21-
assertEquals(KotlinCompilerVersion.VERSION, engineVersion)
22-
assertEquals(listOf("kts"), extensions)
23-
assertEquals(listOf("text/x-kotlin"), mimeTypes)
24-
assertEquals(listOf("kotlin"), names)
25-
assertEquals("obj.method(arg1, arg2, arg3)", getMethodCallSyntax("obj", "method", "arg1", "arg2", "arg3"))
26-
assertEquals("print(\"Hello, world!\")", getOutputStatement("Hello, world!"))
27-
assertEquals(KotlinCompilerVersion.VERSION, getParameter(ScriptEngine.LANGUAGE_VERSION))
16+
with(KtsObjectLoader().engine.factory) {
17+
assertThat(languageName).isEqualTo("kotlin")
18+
assertThat(languageVersion).isEqualTo(KotlinCompilerVersion.VERSION)
19+
assertThat(engineName).isEqualTo("kotlin")
20+
assertThat(engineVersion).isEqualTo(KotlinCompilerVersion.VERSION)
21+
assertThat(extensions).contains("kts")
22+
assertThat(mimeTypes).contains("text/x-kotlin")
23+
assertThat(names).contains("kotlin")
24+
assertThat(
25+
getMethodCallSyntax(
26+
"obj",
27+
"method",
28+
"arg1",
29+
"arg2",
30+
"arg3"
31+
)
32+
).isEqualTo("obj.method(arg1, arg2, arg3)")
33+
assertThat(getOutputStatement("Hello, world!")).isEqualTo("print(\"Hello, world!\")")
34+
assertThat(getParameter(ScriptEngine.LANGUAGE_VERSION)).isEqualTo(KotlinCompilerVersion.VERSION)
2835
val sep = System.getProperty("line.separator")
2936
val prog = arrayOf("val x: Int = 3", "var y = x + 2")
30-
assertEquals(prog.joinToString(sep) + sep, getProgram(*prog))
37+
assertThat(getProgram(*prog)).isEqualTo(prog.joinToString(sep) + sep)
3138
}
3239
}
3340

3441
@Test
3542
fun `simple evaluations should work`() {
3643
with(KtsObjectLoader().engine as KotlinJsr223JvmLocalScriptEngine) {
3744
val res1 = eval("val x = 3")
38-
assertNull(res1, "No returned value expected")
45+
assertThat(res1).isEqualTo(null)
3946
val res2 = eval("x + 2")
40-
assertEquals(5, res2, "Reusing x = 3 from prior statement.")
47+
assertThat(res2).isEqualTo(5).describedAs("Reusing x = 3 from prior statement.")
4148
val fromScript = compile("""listOf(1,2,3).joinToString(":")""")
42-
assertEquals(listOf(1, 2, 3).joinToString(":"), fromScript.eval())
49+
assertThat(fromScript.eval()).isEqualTo(listOf(1, 2, 3).joinToString(":"))
4350
}
4451
}
4552

4653
@Test
47-
fun `expression from script`() {
54+
fun `when loading expression from script it should result in an integer`() {
4855
val scriptContent = "5 + 10"
49-
50-
println(scriptContent)
51-
assertEquals(15, KtsObjectLoader().load(scriptContent))
56+
assertThat(KtsObjectLoader().load<Int>(scriptContent)).isEqualTo(15)
5257
}
5358

5459
@Test
55-
fun `class loaded from script`() {
60+
fun `when loading class from string script the content should be as expected`() {
61+
5662
val scriptContent = Files.readAllBytes(Paths.get("src/test/resources/testscript.kts"))?.let {
5763
String(it)
5864
} ?: fail("Cannot load script")
5965

60-
println(scriptContent)
61-
assertEquals(ClassFromScript("I was created in kts; äö"), KtsObjectLoader().load(scriptContent))
66+
val loaded = KtsObjectLoader().load<ClassFromScript>(scriptContent)
67+
assertThat(loaded.text).isEqualTo("I was created in kts; äö")
68+
assertThat(loaded::class).isEqualTo(ClassFromScript::class)
69+
}
70+
71+
@Test
72+
fun `when loading script with unexpected type, it should result in an IllegalArgumentException`() {
73+
assertExceptionThrownBy<IllegalArgumentException> {
74+
KtsObjectLoader().load<String>("5+1")
75+
}
6276
}
6377

6478
@Test
65-
fun `class loaded from script via Reader`() {
66-
val scriptContent = Files.newBufferedReader(Paths.get("src/test/resources/testscript.kts"))
67-
assertEquals(ClassFromScript::class, KtsObjectLoader().load<ClassFromScript>(scriptContent)::class)
79+
fun `when loading script with flawed script, then a LoadException should be raised`() {
80+
assertExceptionThrownBy<LoadException> {
81+
KtsObjectLoader().load<Int>("Hello World")
82+
}
6883
}
6984

85+
val script1 = "src/test/resources/testscript.kts"
86+
val script2 = "src/test/resources/testscript2.kts"
87+
7088
@Test
71-
fun `class loaded from script via InputStream`() {
72-
val scriptContent = Files.newInputStream(Paths.get("src/test/resources/testscript.kts"))
73-
assertEquals(ClassFromScript::class, KtsObjectLoader().load<ClassFromScript>(scriptContent)::class)
89+
fun `when loading class from script via Reader the content should be as expected`() {
90+
val scriptContent = Files.newBufferedReader(Paths.get(script1))
91+
val loaded = KtsObjectLoader().load<ClassFromScript>(scriptContent)
92+
assertThat(loaded.text).isEqualTo("I was created in kts; äö")
93+
assertThat(loaded::class).isEqualTo(ClassFromScript::class)
7494
}
7595

7696
@Test
77-
fun `multiple classes loaded from script via InputStream`() {
78-
val scriptContent = Files.newInputStream(Paths.get("src/test/resources/testscript.kts"))
79-
val scriptContent2 = Files.newInputStream(Paths.get("src/test/resources/testscript2.kts"))
80-
KtsObjectLoader()
81-
.loadAll<ClassFromScript>(scriptContent, scriptContent2).forEach {
82-
assertEquals(ClassFromScript::class, it::class)
97+
fun `when loading class from script via InputStream the content should be as expected`() {
98+
val scriptContent = Files.newInputStream(Paths.get(script1))
99+
val loaded = KtsObjectLoader().load<ClassFromScript>(scriptContent)
100+
assertThat(loaded.text).isEqualTo("I was created in kts; äö")
101+
assertThat(loaded::class).isEqualTo(ClassFromScript::class)
102+
}
103+
104+
@Test
105+
fun `when loading multiple classes from script via InputStream, all should have the expected type`() {
106+
val scriptContent = Files.newInputStream(Paths.get(script1))
107+
val scriptContent2 = Files.newInputStream(Paths.get(script2))
108+
assertThat(
109+
KtsObjectLoader().loadAll<ClassFromScript>(scriptContent, scriptContent2)
110+
).allMatch { it::class == ClassFromScript::class }
111+
}
112+
113+
@Test
114+
fun `when passing a custom classloader, it should be used when loading the script`() {
115+
val myCl = object : ClassLoader() {
116+
override fun loadClass(name: String?): Class<*> {
117+
throw IllegalStateException()
118+
}
119+
}
120+
assertExceptionThrownBy<IllegalStateException> {
121+
KtsObjectLoader(myCl).load("anything")
83122
}
84123
}
85124
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package de.swirtz.ktsrunner.objectloader
2+
3+
import org.assertj.core.api.Assertions
4+
5+
inline fun <reified T : Throwable> assertExceptionThrownBy(crossinline op: () -> Unit) =
6+
Assertions.assertThatExceptionOfType(T::class.java).isThrownBy {
7+
op()
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
junit.jupiter.testinstance.lifecycle.default=per_class

0 commit comments

Comments
 (0)