Skip to content

Commit

Permalink
re-wrote tests to run on junit 5 jupiter; created custom exception ty…
Browse files Browse the repository at this point in the history
…pe which is thrown for script loading errors; added feature to provide custom classloader to script loading
  • Loading branch information
Simon committed Nov 27, 2018
1 parent 4b58682 commit 75583ad
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 54 deletions.
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ println(loadedObj.x)
// >> I was created in kts
```

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.
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.

### Executing scripts directly

Expand All @@ -49,8 +49,25 @@ println(fromScript)

### Application Area

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.
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.

### Controlling the ClassLoader

When instantiating an `KtsObjectLoader`, you can provide an explicit classloader as shown in this test case:

```kotlin
@Test
fun `when passing a custom classloader, it should be used when loading the script`() {
val myCl = object : ClassLoader() {
override fun loadClass(name: String?): Class<*> {
throw IllegalStateException()
}
}
assertExceptionThrownBy<IllegalStateException> {
KtsObjectLoader(myCl).load("anything")
}
}
```
## Getting Started

In your Gradle build, simply include the following repository and dependency:
Expand Down
13 changes: 10 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import org.gradle.api.publish.maven.MavenPom
val kotlinVersion = plugins.getPlugin(KotlinPluginWrapper::class.java).kotlinPluginVersion

project.group = "de.swirtz"
project.version = "0.0.6"
project.version = "0.0.7"
val artifactID = "ktsRunner"

plugins {
Expand All @@ -15,6 +15,11 @@ plugins {
`java-library`
id("com.jfrog.bintray") version "1.8.0"
}
tasks {
"test"(Test::class) {
useJUnitPlatform()
}
}

dependencies {
implementation(kotlin("stdlib-jdk8", kotlinVersion))
Expand All @@ -25,8 +30,9 @@ dependencies {
implementation(kotlin("compiler-embeddable", kotlinVersion))
implementation(kotlin("script-util", kotlinVersion))

testImplementation(kotlin("test-junit", kotlinVersion))
testImplementation("junit:junit:4.11")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.3.2")
testImplementation("org.assertj:assertj-core:3.11.1")
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.3.2")
}


Expand Down Expand Up @@ -82,5 +88,6 @@ tasks {
withType<GenerateMavenPom> {
destination = file("$buildDir/libs/$artifactID.pom")
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,26 @@ import java.io.Reader
import javax.script.ScriptEngineManager

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

val engine = ScriptEngineManager().getEngineByExtension("kts")
val engine = ScriptEngineManager(classLoader).getEngineByExtension("kts")

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

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

inline fun <reified T> load(inputStream: InputStream): T = load<T>(inputStream.reader())
?: throw IllegalStateException("Could not load script from .kts")
inline fun <reified T> load(script: String): T = safeEval { engine.eval(script) }.castOrError()

inline fun <reified T> load(reader: Reader): T = safeEval { engine.eval(reader) }.castOrError()

inline fun <reified T> load(inputStream: InputStream): T = load(inputStream.reader())

inline fun <reified T> loadAll(vararg inputStream: InputStream): List<T> = inputStream.map(::load)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package de.swirtz.ktsrunner.objectloader

class LoadException(message: String? = null, cause: Throwable? = null) : RuntimeException(message, cause)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package de.swirtz.ktsrunner.objectloader

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

import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.fail
import org.jetbrains.kotlin.config.KotlinCompilerVersion
import org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngine
import org.junit.Test
import org.junit.jupiter.api.Test
import java.nio.file.Files
import java.nio.file.Paths
import javax.script.ScriptEngine
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.fail

class KtsObjectLoaderTest {

@Test
fun `general ScriptEngineFactory test`() {
KtsObjectLoader().engine.factory.apply {
assertEquals("kotlin", languageName)
assertEquals(KotlinCompilerVersion.VERSION, languageVersion)
assertEquals("kotlin", engineName)
assertEquals(KotlinCompilerVersion.VERSION, engineVersion)
assertEquals(listOf("kts"), extensions)
assertEquals(listOf("text/x-kotlin"), mimeTypes)
assertEquals(listOf("kotlin"), names)
assertEquals("obj.method(arg1, arg2, arg3)", getMethodCallSyntax("obj", "method", "arg1", "arg2", "arg3"))
assertEquals("print(\"Hello, world!\")", getOutputStatement("Hello, world!"))
assertEquals(KotlinCompilerVersion.VERSION, getParameter(ScriptEngine.LANGUAGE_VERSION))
with(KtsObjectLoader().engine.factory) {
assertThat(languageName).isEqualTo("kotlin")
assertThat(languageVersion).isEqualTo(KotlinCompilerVersion.VERSION)
assertThat(engineName).isEqualTo("kotlin")
assertThat(engineVersion).isEqualTo(KotlinCompilerVersion.VERSION)
assertThat(extensions).contains("kts")
assertThat(mimeTypes).contains("text/x-kotlin")
assertThat(names).contains("kotlin")
assertThat(
getMethodCallSyntax(
"obj",
"method",
"arg1",
"arg2",
"arg3"
)
).isEqualTo("obj.method(arg1, arg2, arg3)")
assertThat(getOutputStatement("Hello, world!")).isEqualTo("print(\"Hello, world!\")")
assertThat(getParameter(ScriptEngine.LANGUAGE_VERSION)).isEqualTo(KotlinCompilerVersion.VERSION)
val sep = System.getProperty("line.separator")
val prog = arrayOf("val x: Int = 3", "var y = x + 2")
assertEquals(prog.joinToString(sep) + sep, getProgram(*prog))
assertThat(getProgram(*prog)).isEqualTo(prog.joinToString(sep) + sep)
}
}

@Test
fun `simple evaluations should work`() {
with(KtsObjectLoader().engine as KotlinJsr223JvmLocalScriptEngine) {
val res1 = eval("val x = 3")
assertNull(res1, "No returned value expected")
assertThat(res1).isEqualTo(null)
val res2 = eval("x + 2")
assertEquals(5, res2, "Reusing x = 3 from prior statement.")
assertThat(res2).isEqualTo(5).describedAs("Reusing x = 3 from prior statement.")
val fromScript = compile("""listOf(1,2,3).joinToString(":")""")
assertEquals(listOf(1, 2, 3).joinToString(":"), fromScript.eval())
assertThat(fromScript.eval()).isEqualTo(listOf(1, 2, 3).joinToString(":"))
}
}

@Test
fun `expression from script`() {
fun `when loading expression from script it should result in an integer`() {
val scriptContent = "5 + 10"

println(scriptContent)
assertEquals(15, KtsObjectLoader().load(scriptContent))
assertThat(KtsObjectLoader().load<Int>(scriptContent)).isEqualTo(15)
}

@Test
fun `class loaded from script`() {
fun `when loading class from string script the content should be as expected`() {

val scriptContent = Files.readAllBytes(Paths.get("src/test/resources/testscript.kts"))?.let {
String(it)
} ?: fail("Cannot load script")

println(scriptContent)
assertEquals(ClassFromScript("I was created in kts; äö"), KtsObjectLoader().load(scriptContent))
val loaded = KtsObjectLoader().load<ClassFromScript>(scriptContent)
assertThat(loaded.text).isEqualTo("I was created in kts; äö")
assertThat(loaded::class).isEqualTo(ClassFromScript::class)
}

@Test
fun `when loading script with unexpected type, it should result in an IllegalArgumentException`() {
assertExceptionThrownBy<IllegalArgumentException> {
KtsObjectLoader().load<String>("5+1")
}
}

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

val script1 = "src/test/resources/testscript.kts"
val script2 = "src/test/resources/testscript2.kts"

@Test
fun `class loaded from script via InputStream`() {
val scriptContent = Files.newInputStream(Paths.get("src/test/resources/testscript.kts"))
assertEquals(ClassFromScript::class, KtsObjectLoader().load<ClassFromScript>(scriptContent)::class)
fun `when loading class from script via Reader the content should be as expected`() {
val scriptContent = Files.newBufferedReader(Paths.get(script1))
val loaded = KtsObjectLoader().load<ClassFromScript>(scriptContent)
assertThat(loaded.text).isEqualTo("I was created in kts; äö")
assertThat(loaded::class).isEqualTo(ClassFromScript::class)
}

@Test
fun `multiple classes loaded from script via InputStream`() {
val scriptContent = Files.newInputStream(Paths.get("src/test/resources/testscript.kts"))
val scriptContent2 = Files.newInputStream(Paths.get("src/test/resources/testscript2.kts"))
KtsObjectLoader()
.loadAll<ClassFromScript>(scriptContent, scriptContent2).forEach {
assertEquals(ClassFromScript::class, it::class)
fun `when loading class from script via InputStream the content should be as expected`() {
val scriptContent = Files.newInputStream(Paths.get(script1))
val loaded = KtsObjectLoader().load<ClassFromScript>(scriptContent)
assertThat(loaded.text).isEqualTo("I was created in kts; äö")
assertThat(loaded::class).isEqualTo(ClassFromScript::class)
}

@Test
fun `when loading multiple classes from script via InputStream, all should have the expected type`() {
val scriptContent = Files.newInputStream(Paths.get(script1))
val scriptContent2 = Files.newInputStream(Paths.get(script2))
assertThat(
KtsObjectLoader().loadAll<ClassFromScript>(scriptContent, scriptContent2)
).allMatch { it::class == ClassFromScript::class }
}

@Test
fun `when passing a custom classloader, it should be used when loading the script`() {
val myCl = object : ClassLoader() {
override fun loadClass(name: String?): Class<*> {
throw IllegalStateException()
}
}
assertExceptionThrownBy<IllegalStateException> {
KtsObjectLoader(myCl).load("anything")
}
}
}
8 changes: 8 additions & 0 deletions src/test/kotlin/de/swirtz/ktsrunner/objectloader/testutils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package de.swirtz.ktsrunner.objectloader

import org.assertj.core.api.Assertions

inline fun <reified T : Throwable> assertExceptionThrownBy(crossinline op: () -> Unit) =
Assertions.assertThatExceptionOfType(T::class.java).isThrownBy {
op()
}
1 change: 1 addition & 0 deletions src/test/resources/junit-platform.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
junit.jupiter.testinstance.lifecycle.default=per_class

0 comments on commit 75583ad

Please sign in to comment.